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] >>> |