9. UnetSocket API
The command shell is great for manual configuration and interaction, but often we require programmatic interaction from an external application. For this, we have the UnetSocket API (available in Java, Groovy, Python, Julia and C). While the exact syntax differs across languages, the basic concepts remain the same. We focus on the use of the API in Groovy in this section, but also show some examples in other languages.
9.1. Connecting to UnetStack
If you recall from Section 2.4 , you opened a socket connection to UnetStack on the command shell with:
> s = new UnetSocket(this);
Since the command shell was running on the node you wanted to connect to, the meaning of
this
was clear. However, in general, you’ll probably be running your application in a different process, or even on a different computer. You’ll therefore need to provide details on how to connect to the node when opening a socket.
The examples in this chapter assume that you are running:
bin/unet samples/2-node-network.groovy
|
For example, to connect to UnetStack from an application over TCP/IP, we need to know the IP address and port of the API connector on UnetStack. Simply type
iface
on the command shell of node A to find this information:
> iface
tcp://192.168.1.9:1101 [API]
ws://192.168.1.9:8081/ws [API]
websh: ws://192.168.1.9:8081/fjage/shell/ws [GroovyScriptEngine]
The first entry starting with
tcp://
is the API connector available over TCP/IP. The IP address and port number in this case are
192.168.1.9
and
1101
respectively. The IP address on your setup might differ, so remember to replace it in the example code below when you try it.
To connect to UnetStack from a Groovy application, typical code might look something like this:
import org.arl.unet.api.UnetSocket
def sock = new UnetSocket('192.168.1.9', 1101) (1)
// do things with sock here
sock.close()
1 |
Note that the
def
is typically not used in the shell, as we usually want the
sock
variable to be created in the shell’s context. However, we use
def
in Groovy scripts or closures to keep the
sock
variable in the local context.
|
External applications interact with UnetStack via a UnetSocket API using fjåge’s connector framework. This allows the API to access UnetStack over a TCP/IP connection, a serial port, or any other fjåge connector that may be available. |
The code in other languages looks similar. For example, in Python:
from unetpy import UnetSocket
sock = UnetSocket('192.168.1.9', 1102)
# do things with sock here
sock.close()
A simple example application in Python using the UnetSocket API was illustrated previously in Section 2.5 .
9.2. Sending data
To send datagrams using a socket, we first specify the destination address and protocol number using the
connect()
method, and then use the
send()
method to send data (byte array). In Groovy:
def to = sock.host('B') (1)
sock.connect(to, 0) (2)
sock.send('hello!' as byte[]) (3)
sock.send('more data!' as byte[])
1 | Resolve node name to address. If the destination address is already known, this step can be skipped. |
2 |
Connect using protcol 0 (generic data). Constant
org.arl.unet.Protocol.DATA
may be used instead of 0 for improved readability.
|
3 |
Data has to be converted into a
byte[]
for transmission using the
send()
method.
|
If only a single
send()
is desired, the
connect()
call may be omitted and the destination and protocol number can be provided as parameters to
send()
:
sock.send('hello!' as byte[], to, 0)
9.3. Receiving data
On the receiving end, we specify the protocol number to listen to using
bind()
, and then receive a datagram using the
receive()
method:
sock.bind(0)
def rx = sock.receive()
println(rx.from, rx.to, rx.data)
Unbound sockets listen to all unreserved protocols. So the
bind()
call above could be skipped, if we would like to listen to all application datagrams.
|
The
receive()
method above is blocking by default. The blocking behavior can be controlled using the
setTimeout()
method, where the blocking timeout can be specified in milliseconds. A timeout of 0 makes the call non-blocking. If no message is available at timeout, a
null
value is returned. When the
receive()
call is blocked, a call to
cancel()
can unblock and cause the
receive()
call to return immediately.
9.4. Getting & setting parameters
You have already been introduced to agent parameters in Chapter 3 . Applications can obtain information about an agent by reading its parameters, and can control the behavior of the agent by modifying its parameters.
To access agent parameters, you first have to look up the relevant agent based on its name or a service that it provides. For example:
def phy = sock.agentForService(org.arl.unet.Services.PHYSICAL) (1)
println(phy.MTU)
println(phy[1].dataRate)
1 |
Looking up an agent based on a services it provides is recommended, rather than specify the agent by name. We will explore services in more detail in
Chapter 13
. However, if you wished to reference an agent by name, you could have done that as:
def phy = sock.agent('phy')
|
This will print the value of parameter
MTU
(maximum transfer unit) of the physical layer, and the physical layer
dataRate
of the CONTROL (1) channel. You could also change some of the parameters:
println(phy[2].frameLength)
phy[2].frameLength = 32
println(phy[2].frameLength)
phy[2].frameLength = 64
Developers may wish to consider using constants
org.arl.unet.phy.Physical.CONTROL
and
org.arl.unet.phy.Physical.DATA
instead of hard coding 1 and 2, for readability.
|
The
phy
object that you received back from
sock.agentForService()
or
sock.agent()
is an
AgentID
. You can think of this as a reference to the agent. Setting and getting parameters on the agent ID sends
ParameterReq
messsages to the agent to read/modify the relevant parameters. You can also use agent IDs to send messages to the agent explicitly, as you will see next.
|
9.5. Accessing agent services
As we have already seen in Section 3.2 , the full functionality of UnetStack can be harnessed by sending/receiving messages to/from various agents in the stack. We earlier saw how to do that from the shell. We now look at how to use the UnetSocket API to send/receive messages to/from agents.
To request broadcast of a CONTROL frame, like we did before from the shell, we need to lookup the agent providing the PHYSICAL service and send a
TxFrameReq
to it:
import org.arl.unet.phy.TxFrameReq
def phy = sock.agentForService(org.arl.unet.Services.PHYSICAL)
phy << new TxFrameReq()
For lower level transactions, we obtain a fjåge Gateway instance from the UnetSocket API, and use it directly. For example, we can subscribe to event notifications from the physical layer and print them:
def gw = sock.gateway
gw.subscribe(phy)
def msg = gw.receive(10000) (1)
if (msg) println(msg)
1 |
Receive a message from the gateway with a timeout of 10000 ms. If no message is received during this period,
null
is returned.
|
9.6. Python and other languages
In Groovy and Java, services, parameters and messages are defined using enums and classes. These are made available to the client application by putting the relevant jars in the classpath. In other languages (e.g. Python, Julia, Javascript), services and parameters are simply referred to as strings with fully qualified names (e.g.
'org.arl.unet.Services.PHYSICAL'
). Messages are represented by dictionaries, but have to be declared before use.
For example, in Python:
from unetpy import *
sock = UnetSocket('192.168.1.9', 1102)
phy = sock.agentForService(Services.PHYSICAL)
phy << TxFrameReq()
sock.close()
If you recall from
Section 2.5
,
from
is a keyword in Python and so the
from
field in messages is replaced by
from_
. Other than this minor change, the fields in all the Python message classes are the same as the Java/Groovy versions.
|
<<< [Interfacing with UnetStack] | [Portals] >>> |