Remote Procedure Calls¶
Remote Procedure Call (RPC)-related functionality is a fundamental part of SNAP and SNAPpy, and there are many types of SNAPpy built-in functions that can be used to invoke a function on another device.
Introduction¶
All public functions, including the SNAPpy built-in functions, are remotely callable using the SNAP RPC protocol. Non-public functions (prefixed with underscore) are not remotely callable, but they can be called by other functions within the same script.
These are non-blocking functions. They only enqueue the packet for transmission and do not wait for the packet to be sent (let alone wait for it to be processed by the receiving device(s)). Each of these functions can be used to invoke any public function on another SNAP device (either a user-defined function or a built-in function), but each has additional strengths, weaknesses, and capabilities.
It is important to provide the correct number of arguments when calling a remote function. Issuing a command with the wrong number of arguments will not work, because the receiving device will not be able to find a function signature that matches.
This also applies to SNAPstack-related programming. Make sure that any RPC handlers defined in SNAPstack applications accept the same number and type of arguments that the remote callers are providing. For example, if your script takes two arguments:
def displayStatus(msg1, msg2):
print(msg1 + msg2)
But the SNAPpy script makes RPC calls with three parameters:
rpc(SNAPSTACK_ADDR, "displayStatus", 1, 2, 3) # <- too many parameters provided
Or just one parameter:
rpc(SNAPSTACK_ADDR, "displayStatus", 1) # <- too few parameters provided
Then the displayStatus()
function will not be invoked function at all.
Calling By Name¶
You cannot invoke a function that a device does not have loaded. SNAP devices without scripts only support the “call by number” method to call RPC functions. The name lookup table that allow devices to use “call by name” is sent to the node when a script is loaded. This means that if you want to call a SNAPpy built-in function by name, a remote device needs a script loaded, even if the script is empty.
Note
In the case of mcastRpc()
, the device will silently ignore any function that it does
not know how to call. If sent via one of the unicast mechanisms (rpc()
,
callback()
or callout()
) the packet will be acknowledged but then
ignored.
Realize that if you multicast an RPC call to function “foo”, all devices in that multicast group that have a foo()
function will execute it, even if their foo()
function does something different from what your target device’s
foo()
function is expected to do. Giving your SNAPpy functions distinct and meaningful names is recommended.
Selecting the Type of RPC¶
The reliability of message delivery is an important consideration in selecting the best way to send the message. Multicast messages generate no explicit confirmation of receipt from any other device in your network. If you want to be sure that a particular device has heard a multicast RPC call, you must provide that confirmation as part of your own application, generally in the form of a message sent back to the originating device.
Unicast RPC calls are addressed to a single target device, rather than being open to all devices that can hear the request. The reliability of these calls is bolstered by a series of acknowledgement messages sent back along the path, but even that is no guarantee of a final receipt of the message, especially in a dynamic environment.
Multicast RPC¶
The built-in function mcastRpc()
is best at invoking the same function on multiple devices from a
single invocation. The trade-off is that the actual packet is usually only sent once 1. How many devices actually
perform the requested function call is a function of three factors:
Number of hops
How many hops (forwards or retransmissions of the message) away are the other devices?
How many hops did you specify in the
mcastRpc()
call? (TTL parameter)
Script existence
Is there a function with that name in the device’s currently loaded script?
Are you providing the correct number of arguments?
What function name and arguments did you specify in the
mcastRpc()
call?
Group membership
What Multicast Groups do the other devices belong to?
What candidate groups did you specify in the
mcastRpc()
call?If the destination device is more than one hop away, do intermediate devices forward the specified group?
The following is an example of using multicast RPC to increase the synchronization of the devices. Not all of the devices
will necessarily be running the startDataCapture()
function at the same moment, because the closer devices will
be hearing the command sooner than the devices that require more mesh network hops. All devices in group 2 that can be
reached within 4 or fewer hops and that have a function named startDataCapture()
in their script will invoke that
function:
mcast_groups = 2
hop_count = 4
mcastRpc(mcast_groups, hop_count, "startDataCapture")
This next example is a little exaggerated, but sometimes you will need to send an RPC to all of the devices because of the importance of the message:
reactor_temperature = get_reactor_temperature()
if reactor_temperature > CRITICAL_TEMPERATURE:
mcastRpc(1, 255, "runForYourLives")
It is important to note that the function being called might make an RPC call of its own. For the sake of the next example, imagine a network of exactly two SNAP devices, devices A and B, and that device B contains the following script snippet:
def askSensorReading():
value = readSensor()
mcastRpc(1, 1, "tellSensorReading", value)
Now imagine that device A contains the following script snippet:
def tellSensorReading(value):
print("I heard the sensor reading was ", value)
If device A executes the following command, this would result in two-way communication:
mcastRpc(1, 1, "askSensorReading")
First, device A will send askSensorReading()
to device B, and then device B will send tellSensorReading()
back to
node A.
You should also be aware that even though an RPC call is made via multicast, it is still possible to have only a single device completely process that call. Imagine the above two-node network is expanded by adding devices C-Z and that we upload the following script to devices B-Z:
def askSensorReading(who):
if who == localAddr(): # is this command meant for ME?
value = readSensor()
mcastRpc(1, 1, "tellSensorReading", value)
Now, if device A executes the following command:
mcastRpc(1, 1, "askSensorReading", address_of_node_B)
Even though all devices within a one-hop radius will invoke function askSensorReading()
, only device B will actually
take a reading and report it back.
Footnotes
- 1
The exception to that rule is if you have enabled the optional collision detect feature of SNAP, in which case the packet might be sent more than once if a packet collision was detected.
Directed Multicast RPC¶
The built-in function dmcastRpc()
functions like mcastRpc()
but with two
additional features:
You may target one or more specific remote device(s) on which your function should execute.
If a device does not find its own address in the incoming message, it will still forward the message to other devices (subject to remaining TTL and the restrictions placed by the specified multicast groups), but it will not execute the function, even if the specified multicast groups would otherwise instruct the device to do so.
You can define a delay (in milliseconds) so that the multiple remote devices can respond in a sequential manner rather than all at once.
The delay only applies to radio communications directly invoked by the remote devices. There is no delay applied to serial communications directly invoked by the remote device. A check of
getInfo(25)
in the called function on the remote device returns the delay value specified, but it also tells the remote device to ignore the transmission delay that would have normally been enforced.
For example, if you wanted to target devices with addresses 01.02.03, 04.05.06, 07.08.09, and AA.BB.CC, you would need to pass these addresses as a concatenated string:
dmcastRpc('\x01\x02\x03\x04\x05\x06\x07\x08\x09\xaa\xbb\xcc', 0x0001, 5, 40, 'readAdc' , 0)
In the example above, the delay is set to 40
ms, so any radio traffic generated by device 01.02.03 would be
released immediately, radio traffic generated by 04.05.06 would be queued and held for 40 ms, radio traffic generated by
07.08.09 would be delayed for 80 ms, and radio traffic generated by AA.BB.CC would be held for 120 ms before release.
Unicast RPC¶
The built-in function rpc()
is like mcastRpc()
but with two differences:
Instead of specifying a group and a number of hops (TTL), with the rpc() function you specify the actual SNAP address of the intended target device.
The SNAP mesh routing protocol will take care of “finding” the device (if it can be found).
Other devices (with different SNAP addresses) will not perform the
rpc()
call, even if their currently loaded SNAPpy script also contains the requested function. However, they will (by default) assist in delivering therpc()
call to the addressed device.
Instead of only sending the RPC call a single time (blindly) as
mcastRpc()
does, therpc()
function expects a special ACK (acknowledgement) packet in return.When the target device hears the
rpc()
call, the ACK packet is sent automatically (by the SNAP firmware – you do not send the ACK from your script).If the target device does not hear the
rpc()
call, then it does not know to send the ACK packet. This means the source device will not hear an ACK, and so it will timeout and retry a configurable number of times.
Going back two examples, instead of modifying the askSensorReading()
function in device B’s script to take an
additional who
parameter and calling:
mcastRpc(1, 1, "askSensorReading", address_of_node_B)
Node A could simply call:
rpc(address_of_node_B, "askSensorReading")
Devices C-Z would ignore the function call, although they may be helping route the function call to device B without any additional configuration.
The askSensorReading()
function could also benefit from the use of rpc()
instead of
mcastRpc()
. Instead of telling the sensor reading to all devices in group 1 within 1 hop away via:
mcastRpc(1, 1, "tellSensorReading", value)
The script could instead only send the results back to the original requester via:
rpc(rpcSourceAddr(), "tellSensorReading", value)
Function rpcSourceAddr()
is another built-in function that, when called from a function that
was invoked remotely, returns the SNAP address of the calling device.
Note
If you call rpcSourceAddr()
locally at some arbitrary point in time, such as
within the HOOK_STARTUP
or HOOK_GPIO
handler, then it simply returns None
.
Callback¶
The previous examples allowed one device to ask another device to perform a function and then send the result of
that function back to the first device. In each case, the first device called the askSensorReading()
function,
whose only purpose was to call a separate function readSensor()
and then send the value back. It turns out that
SNAP has a built-in function to do just that, the snappy.BuiltIn.callback()
function.
Expanding a little on the previous example, device B’s readSensor()
function is pulling its own weight – it is
encapsulating some of the sensor complexity, thus hiding it from the rest of the system:
def readSensor():
return readAdc(0) * SENSOR_GAIN + SENSOR_OFFSET
def askSensorReading():
value = readSensor()
mcastRpc(1, 1, "tellSensorReading", value)
Sometimes, the raw sensor readings are sufficient or possibly the calculations are so complex that they need to be offloaded to a bigger processor. In that case, we could change the code to this:
def readSensor():
return readAdc(0)
def askSensorReading():
value = readSensor()
rpc(rpcSourceAddr(), "tellSensorReading", value)
Which can be simplified even further to:
def askSensorReading():
value = readAdc(0)
rpc(rpcSourceAddr(), "tellSensorReading", value)
You might wonder why device A could not skip tellSensorReading()
all together and just remotely call
readAdc(0)
:
rpc(address_of_node_B, "readAdc", 0)
Although this will result in device B calling readAdc(0)
, it will not cause any results to be sent back to
node A. This is where the callback()
function comes in. Let’s replace readAdc
with callback
:
rpc(address_of_node_B, "callback", "tellSensorReading", "readAdc", 0)
This will result in device B calling readAdc(0)
, and the results will be automatically reported back to device A
via the tellSensorReading()
function. The callback()
function requires you to provide
the final function to be invoked (“called back”), in addition to the remote function to be called and its parameters.
Notice that we only had to add code to device A’s script – we did not have to create an askSensorReading()
function on device B at all.
It’s also important to note that callback()
is not limited to invoking built-in functions.
For example, if we had retained the original readSensor()
routine, it could be remotely invoked and the result
automatically returned via:
rpc(address_of_node_B, "callback", "tellSensorReading", "readSensor")
Callout¶
The function callout()
just takes the callback()
concept one
step further. Instead of asking a device to invoke a function and then call you back with the result,
callout()
is used to ask a device to call a function and then report the result to
a third device.
For example, device A could ask device B to read analog input channel 0
and tell device C what the answer is.
Imagine that device C has a graphical LCD display that the other devices lack:
rpc(node_b_address, "callout", device_c_address, "tellSensorReading", "readAdc", 0)
In a more complex example, device A could ask device B to find out how well all the devices one hop away could hear node B, and then ask them to send the answers to device A:
rpc(node_b_address, "mcastRpc", 1, 1, "callout", device_a_address, "tellLinkQuality", "getLq")
Node A is asking device B to send a multicast. All devices within one hop of device B will receive a
callout()
instructing them to call their getLq()
function. Each device
will take the result of the getLq()
and send it to device A as a parameter of the
tellLinkQuality()
, via an rpc()
function. Node A would need to have the function for
node B’s neighbors to send their results:
def tellLinkQuality(lq):
who = rpcSourceAddr()
# do something with the address and the reported link quality
Depending on the network, device A could expect to receive many messages which will answer the question “how well did node B’s neighbors hear the multicast transmission?”
Directed Multicast Callout¶
The dmCallout()
function was added in SNAP 2.7. This function works very similarly to the
callout()
function, except that when the remote device invokes a function, it sends the return
value to the list of devices as a dmcastRpc()
function. As with callout()
,
this allows you to have the target device ask a remote device to do something and then tell other device(s) how it turned
out. The difference being that the remote device will use a dmcastRpc()
function instead of an
rpc()
function to report the result.