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 1102 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', 1102)    (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 12 . 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[1].frameLength = 64
println(phy[2].frameLength)
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] >>>