> ps
scheduler: org.arl.unet.scheduler.Scheduler
rdp: org.arl.unet.net.RouteDiscoveryProtocol
caddy: org.arl.unet.transport.Caddy
ranging: org.arl.unet.localization.Ranging
uwlink: org.arl.unet.link.ECLink
node: org.arl.unet.nodeinfo.NodeInfo
simulator: org.arl.unet.sim.SimulationAgent
phy: org.arl.unet.sim.HalfDuplexModem
bbmon: org.arl.unet.bb.BasebandSignalMonitor
arp: org.arl.unet.addr.AddressResolution
device: org.arl.unet.device.SimpleDevice
profiles: org.arl.unet.profiles.Profiles
router: org.arl.unet.net.Router
shell: org.arl.fjage.shell.ShellAgent
speedtest: org.arl.unet.utils.SpeedTest
mac: org.arl.unet.mac.CSMA3 UnetStack basics
UnetStack is an agent-based network stack. Each agent is similar to a layer in a traditional network stack, but has more flexibility to use the scarce resources (bandwidth, energy, etc.) in the Unet more efficiently. In order to develop Unet applications, we need to understand some basic concepts in UnetStack.
3.1 The command shell
The simplest way to interact with UnetStack is via the command shell (or simply shell). The shell may be accessed on the console, a TCP/IP port or via the web interface. In Chapter 2, we have already seen how to set up a 2-node network and access the command shell for each of the nodes using a web browser. For the rest of this chapter, we assume that you started a new simulation and have shells open on nodes A and B.
The command shell you get from UnetStack uses the Groovy scripting language. Don’t worry if you are not familiar with Groovy – it is a Java-based dynamic high-level language like Python that you will get comfortable with after a few examples. While you may eventually interact with the modem using API calls from other languages (such as Python, Java, C, etc.), the Groovy shell provides a powerful way to interact with the modems in the field, and so it is well worth getting familiar with it!
Let’s get familiar with the shell…
On node A, we can ask for a list of agents running:
We can further ask more details about a specific agent:
> phy
« Half-duplex modem »
Generic half duplex modem simulator.
[org.arl.unet.DatagramParam]
MTU ⤇ 123
RTU ⤇ 123
[org.arl.unet.UnetParam]
queueLength ⤇ 0
[org.arl.unet.bb.BasebandParam]
adcChannels ⤇ 1
adcrate ⤇ 96000.0
analogInterface ⤇ HalfDuplexModem
basebandRate = 24000.0
bbsblk ⤇ 0
bbscnt ⤇ 0
bbsport ⤇ 0
busy ⤇ false
carrierFrequency = 24000.0
clockCalib ⤇ 1.0
dacChannels ⤇ 1
dacrate ⤇ 96000.0
depthToConsiderInWater ⤇ 0.0
enable = true
fullduplex ⤇ false
gain ⤇ [0.0]
ip ⤇ 0.0.0.0
maxInAirPowerLevel ⤇ 0.0
maxPowerLevel = 0.0
maxPreambleID ⤇ 8
maxSignalLength = 65536
minPowerLevel = -96.0
mute ⤇ true
noise ⤇ [96.1236]
pbsblk ⤇ 0
pbscnt ⤇ 0
pbsport ⤇ 0
powerLevel = [-10.0]
record ⤇ false
refPowerLevel = 185.0
rxSensitivity = -185.0
standby ⤇ 0.0
syncBasebandTime ⤇ null
syncSystemTime ⤇ null
time ⤇ 2901687907
wakeupDelay ⤇ 0
[org.arl.unet.phy.PhysicalParam]
baseband ⤇ phy
busy ⤇ false
maxPowerLevel = 0.0
minPowerLevel = -96.0
rxEnable = true
time ⤇ 2901684907
timestampedTxDelay = 1.0
[org.arl.unet.sim.HalfDuplexModemParam]
clockOffset = 2897.2753.1.1 Agent parameters
We asked for details of the agent phy, and we got a list of parameters supported by the agent. We can get or set individual parameters of the agent:
> phy.MTU
123
> phy.rxEnable
true
> phy.rxEnable = false
false
> phy.rxEnable
false
> phy.rxEnable = true
trueTo find out more about a specific parameter, we can ask for help on the parameter:
> help phy.MTU
phy.MTU - maximum transmission unit (MTU) in bytes
> help phy.rxEnable
phy.rxEnable - true if reception enabledWe can also ask for help on an agent:
> help phy
# phy - physical service
The physical service is responsible for managing the physical layer of the
acoustic modem. It provides multiple communication channels supporting
different modulation techniques, forward error correction and CRC checks.
It is responsible for transmitting and receiving data over the underwater
acoustic medium.
Examples:
phy // access physical parameters
phy[CONTROL] // access control channel parameters
phy[DATA] // access data channel parameters
phy[AUX] // access auxiliary channel parameters
phy << msg // send request msg to physical agent
phy.rxEnable = false // disable reception of frames
Commands:
* pclr - clear PHY queues
* plvl - get/set TX power level for all PHY channel types
* aux - setup auxiliary channel
Parameters:
The following parameters are available on all modems. Additional modem
dependent parameters may also be available.
* phy.MTU - maximum transmission unit (MTU) in bytes
* phy.RTU - recommended data transfer size in bytes
* phy.rxEnable - true if reception enabled
* phy.timestampedTxDelay - delay before TX of timestamped frames
* phy.time - physical layer time (microseconds)
* phy.busy - true if modem is TX/RX a frame, false if idle
* phy.maxPowerLevel - maximum supported power level (relative to reference)
* phy.minPowerLevel - minimum supported power level (relative to reference)
* phy.baseband - name of baseband service provider
Channel Parameters:
The following parameters are available on all modems. Additional modem
dependent parameters may also be available.
* phy[].MTU - maximum transmission unit (MTU) in bytes
* phy[].RTU - recommended data transfer size in bytes
* phy[].dataRate - effective frame data rate (bps)
* phy[].frameDuration - frame duration (seconds)
* phy[].powerLevel - transmission power level (dB re reference)
* phy[].frameLength - frame length (bytes)
* phy[].maxFrameLength - maximum settable frame length (bytes)
* phy[].fec - forward error correction code
* phy[].fecList - list of available forward error correction codes
* phy[].framing - framing type name (from `phy[].framingList`)
* phy[].framingList - list of available framing types
* phy[].modulation - modulation type name (from `phy[].modulationList`)
* phy[].modulationList - list of available modulation types
* phy[].schemeCode - short code encoding all modulation/fec detailsFrom this help, we see that phy agent also supports channel parameters (also known as indexed parameters). It supports four logical channels, CONTROL (1), DATA (2), AUX (3), and CUSTOM (4). The CONTROL channel is meant for low-rate robust data transmission, whereas the DATA channel is typically configured for higher rate data transmission. AUX and CUSTOM are used to support special communication schemes such as JANUS, and for user-defined use-cases. Channel parameters work in the same way as normal parameters, but with an index:
> phy[CONTROL]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 13
RTU ⤇ 13
[org.arl.unet.bb.BasebandDetectorParam]
capture ⤇ 13488
coherence ⤇ 0
inhibit ⤇ 0
metricStatistics ⤇ 0.0
preamble = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0 ... , 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
stride ⤇ 1
threshold ⤇ 0.0
[org.arl.unet.phy.PhysicalChannelParam]
dataRate = 247.42267
fec ⤇ default
fecList ⤇ null
frameDuration ⤇ 0.582
frameLength = 18
framing ⤇ default
framingList ⤇ null
maxFrameLength = 128
modulation ⤇ default
modulationList ⤇ null
powerLevel = -10.0
schemeCode ⤇ null
> phy[DATA]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 123
RTU ⤇ 123
[org.arl.unet.bb.BasebandDetectorParam]
capture ⤇ 24000
coherence ⤇ 0
inhibit ⤇ 0
metricStatistics ⤇ 0.0
preamble = [2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0 ... , 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
stride ⤇ 1
threshold ⤇ 0.0
[org.arl.unet.phy.PhysicalChannelParam]
dataRate = 1003.9216
fec ⤇ default
fecList ⤇ null
frameDuration ⤇ 1.02
frameLength = 128
framing ⤇ default
framingList ⤇ null
maxFrameLength = 4096
modulation ⤇ default
modulationList ⤇ null
powerLevel = -10.0
schemeCode ⤇ null
> phy[CONTROL].MTU
13
> phy[CONTROL].frameLength = 32
32
> phy[CONTROL].MTU
27
> phy[CONTROL].frameLength = 18
18
> phy[CONTROL].MTU
13The actual parameters you see may differ if you are working with a modem, depending on the specific capabilities of the modem. Use help to find out more about any listed parameter on your modem, or refer to the modem’s documentation for further information.
3.1.2 Shell commands
Most agents also support some commands. For example, the phy agent supports the plvl command:
> help plvl
plvl - get/set TX power level for all PHY channel types
Examples:
plvl // get all power levels
plvl -10 // set all power to -10 dB
plvl(-10) // alternative syntax
plvl = -10 // alternative syntax
> plvl
phy[1].powerLevel = -10.0
phy[2].powerLevel = -10.0
phy[3].powerLevel = -10.0
phy[4].powerLevel = -10.0
phy.powerLevel = [-10.0]
> plvl -20
OK
> plvl
phy[1].powerLevel = -20.0
phy[2].powerLevel = -20.0
phy[3].powerLevel = -20.0
phy[4].powerLevel = -20.0
phy.powerLevel = [-20.0]The plvl command simply displays or sets the powerLevel parameter of all channels (and waveform transmissions). The same can be manually accomplished by setting or getting individual parameters, if desired:
> phy[1].powerLevel
-20.0
> phy[1].powerLevel = -10
-10
> phy[1].powerLevel
-10.0
> plvl
phy[1].powerLevel = -10.0
phy[2].powerLevel = -20.0
phy[3].powerLevel = -20.0
phy[4].powerLevel = -20.0
phy.powerLevel = [-20.0]While plvl seems like a command to just set/get a powerLevel parameter, it does that for several channels (and waveform transmissions) in one go. This can save you a lot of time and typing — to achieve the same thing manually, you’d be typing 5 commands!
You have already come across the dtx and dshow commands in Chapter 2. You can check exactly what they do by asking for help on them:
> help dtx
dtx - transmit a datagram
Examples:
dtx // send broadcast datagram with no payload
dtx 5, [1,2,3] // send datagram to node 5 with payload [1,2,3]
dtx 0, 'hello' // send broadcast datagram with payload 'hello'
dtx 2, [1,2,3], reliability: true // send datagram with reliability
> help dshow
dshow - enable/disable display of incoming datagrams
Examples:
dshow on // show all incoming datagrams
dshow off // stop showing all incoming datagrams
dshow 0 // show incoming user data (default)
dshow Protocol.RANGING // show incoming ranging datagrams
dshow -Protocol.RANGING // stop showing incoming ranging datagrams
dshow 1, 2 // show incoming datagrams with protocols 1 and 2
dshow 5..7 // show incoming datagrams with protocols 5 to 7
dshow -1 // stop showing incoming datagrams with protocol 1
dshow // list currently enabled protocols3.2 Messages
Agents interact with each other by exchanging messages. In fact, all parameters and shell commands are implemented by exchanging messages between the shell agent and other agents in the stack. For many applications, the abstraction of agent parameters, shell commands, and datagram transfers is sufficient. In such cases, we do not have to concern ourselves with the details of the underlying messages. However, some functionality is accessible only using messages. It is therefore useful for us to know how to interact with agents directly using messages.
UnetStack primarily uses three classes of messages – requests, responses and notifications. The naming convention adopted dictates that the classnames for requests, responses and notifications end in Req, Rsp and Ntf respectively. Every message has a performative, sender, recipient and a unique messageID. In addition, messages can be specialized to have additional context-specific fields. The sender of a message is always an agent, but the recipient may be an agent or a topic. Topics permit a publish-subscribe paradigm where an agent can publish informational messages on topics, and other agents interested in those messages can subscribe to those topics.
Every request (performative REQUEST) demands a response (in some rare cases, more than one). Responses have an inReplyTo field that contains the messageID of the original request to help associate response messages with the corresponding request. Common performatives for responses include AGREE, REFUSE, INFO and FAILURE. The distinction between the last two is worth internalizing: REFUSE means the agent cannot or will not honor the request (an invalid parameter, an unsupported option, a bad time to ask), while FAILURE means the agent agreed to try, but something that should have worked went wrong along the way. An agent that does not understand a request at all responds with NOT_UNDERSTOOD. Notifications are usually unsolicited, but sometimes may be related to a request made by another agent previously. For such notifications, the inReplyTo field is also populated. Common notification performatives include INFO and FAILURE.
Since this might all seem very abstract so far, let us look at a few examples next…
Recall from Chapter 2 that on node A, we could get a range to node B by using the range command:
> range 31
1003.0697This command is implemented under the hood using RangeReq and RangeNtf messages. Instead of using the range command, we could send the RangeReq message directly to the ranging agent:
> ranging << new RangeReq(to: 31)
AGREE
ranging >> RangeNtf:INFORM[from:232 to:31 range:1003.0697 offset:265710978 rxStartTime:2923885603]The new RangeReq(to: 31) code snippet created a RangeReq message with a field to set to node address 31. We sent this message to the ranging agent using <<. The left shift operator (<<) is a short form for a method call request() that sends a message and waits for a response. The response was simply a Message with performative AGREE indicating that the ranging agent agreed to make the measurement for us. A few seconds later, we receive the RangeNtf notification message from the ranging agent with the details of the measured range. We can see that this message has a field range with the measured range of 1003.0697 meters, but it also has additional information such as time offset that may be used for synchronization.
Let’s take another example. We have been using the dtx command to transmit datagrams. The command sends a DatagramReq to the remote agent. For example, dtx 0, [1,2,3] is equivalent to executing:
> remote << new DatagramReq(to: 0, data: [1,2,3])
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019f4237-70f0-77c8-d48e-00982140fb22 to:0]The tell command that we used in Chapter 2 sent a text message from one node to another. That command is implemented by sending a RemoteTextReq to the remote agent. On node A:
> remote << new RemoteTextReq(to: 31, text: 'hello!')
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019f4237-75dd-7a5a-dabd-005c50e138d0 to:31]The two transmissions appear as receptions (RxFrameNtf and a text message) on node B:
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 rxStartTime:3192185581 rssi:-72.6 (3 bytes)]
010203
[232]: hello!The text message display on node B is in fact a visual representation of the RemoteTextNtf unsolicited notification message that was received by the shell agent on node B. The notification message was sent to a topic DATAGRAM that the shell subscribes to, and therefore the shell received the message for display.
Even parameter access is just messages. When we typed phy.MTU earlier, the shell sent a ParameterReq to the phy agent, and formatted the ParameterRsp that came back. We can do the same directly:
> phy << new org.arl.fjage.param.ParameterReq().get(DatagramParam.MTU)
ParameterRsp[MTU*:123]These two messages are honored by every agent that exposes parameters, and are described in more detail in Chapter 13. Here, we used the fully qualified class name as the package org.arl.fjage.param is not imported by default. The * after the MTU parameter denotes that it is read-only.
The DatagramTransmissionNtf you saw in response to a DatagramReq (a couple of listings above) was a notification from the remote agent telling us that the text message was transmitted. You might wonder why the sender is caddy rather than remote. In fact, if you go back to the output from the ps command earlier, you’ll see that there is no agent called remote at all. So, how were we able to send the RemoteTextReq to remote then? To answer that question, we need to understand the concept of UnetStack services.
3.3 Services
In order to have a properly functioning network stack, we require each agent in the stack to provide certain well-defined functionality. The functionality is defined as a service definition by clearly specifying the parameters that the agent should expose, the messages that it should exchange with other agents, and the behaviors that it should exhibit. If an agent complies with the service definition, we consider it to be providing that service.
UnetStack defines a rich set of services that enable us to build complex networks easily. The service definitions are detailed in Part III. For now, let us look at a few examples to understand how services are used.
To look at the services offered by UnetStack, on node A, type:
> services
org.arl.unet.Services.NODE_INFO: node
org.arl.unet.Services.PHYSICAL: phy
org.arl.unet.Services.REMOTE: caddy
org.arl.unet.Services.TRANSPORT: caddy
org.arl.unet.Services.ADDRESS_RESOLUTION: arp
org.arl.unet.Services.DEVICE_INFO: device
org.arl.unet.Services.RANGING: ranging
org.arl.unet.Services.MAC: mac
org.arl.unet.Services.DOA: phy
org.arl.fjage.shell.Services.DOCUMENTATION: scheduler caddy uwlink speedtest bbmon mac
org.arl.fjage.shell.Services.SHELL: shell
org.arl.unet.Services.DATAGRAM: router caddy uwlink phy
org.arl.unet.Services.BASEBAND: phy
org.arl.unet.Services.LINK: uwlink
org.arl.unet.Services.ROUTING: router
org.arl.unet.Services.SCHEDULER: scheduler
org.arl.unet.Services.ROUTE_MAINTENANCE: rdpWe see a list of all services offered, and against each service, a list of agents offering that service. In the previous section, we sent the RangeReq to the ranging agent that offered the RANGING service. We sent a RemoteTextReq to remote. However, there is no agent called remote and so the shell looked up the REMOTE service, found the caddy agent that provides it, and sent our request to that agent.
We can programmatically look up agents providing a service using the agentForService() method:
> a = agentForService(org.arl.unet.Services.ROUTING);
> a.name
routerWhy the a.name (and the trailing ;)? Printing a bare agent ID in the shell queries the agent and prints all its parameters — that is exactly what happened when we typed phy earlier. When all you want is the agent’s name, ask for a.name, and end the lookup statement with a ; so its return value isn’t printed.
A service may have more than one provider on a node. The DATAGRAM service is the classic example — the physical layer, links, routers and transports all deliver datagrams:
> agentsForService(org.arl.unet.Services.DATAGRAM)
[router, caddy, uwlink, phy]agentForService() returns any one of the providers, so when several exist, use agentsForService() to get them all and pick the one you need (for example, by checking capabilities, as described in Chapter 13). You can also ask an agent which services it provides, with phy.services.
In practice, we rarely look up agents by their names directly. We always prefer to look up agents based on the services they offer. This enables us to write future-proof applications that do not depend on the exact agents deployed in a network, but avail functionality provided by those agents. More advanced agents can be added to the stack later, and the application automatically uses the advanced functionality without having to be modified.
3.4 Shell scripting
Since the default shell in UnetStack uses the Groovy language, we can write arbitrary Groovy code and get the shell to execute it. This is a very powerful tool that we will leverage time and again.
Some simple examples:
> 1+2
3
> 5.times { println it }
0
1
2
3
4We can also define closures (think of them as functions for now, if you don’t already know what closures are) and call them:
> tx2 = {
- 2.times {
- dtx 0, [1,2,3]
- }
- };
> tx2
caddy >> DatagramTransmissionNtf:INFORM[id:019f4237-9dc6-71fe-cf8c-7723fa00195f to:0]
caddy >> DatagramTransmissionNtf:INFORM[id:019f4237-9dc7-77d2-f338-5d37473cc90f to:0]To type multiline commands in the shell, use ESC ENTER instead of just ENTER to start a new line without executing the code.
We defined a closure tx2 that calls dtx 0, [1,2,3] twice. Calling tx2 therefore caused two datagrams to be transmitted.
While writing short commands like this on the shell can be useful, the real power of Groovy can be harnessed in longer scripts. You can write Groovy scripts and store them in the scripts folder with an extension .groovy. You can invoke them by simply typing the name of the script (without the extension) in the shell.
If you want some commands to be executed every time you boot up the modem (or the simulated modems), you can create a startup.groovy script in the scripts folder and put those commands in it. The startup.groovy script is executed by the modem at bootup.
If you want to create convenience closures that you use in the shell, you can put them in a fshrc.groovy script in the scripts folder. This script is loaded by every Groovy shell during initialization.