Preface
What is this book about?
About 71% of Earth’s surface is covered with water, and about 97% of the water is in our oceans. Although the ocean plays a critical role in everything from the air we breathe to daily weather and climate patterns, we know very little about it. To really understand our oceans, we need a way to sense and observe the numerous complex processes that drive the ocean environment, and to report the data collected back to our data centers. While cabled ocean observatories have been established in a few locations, they are too expensive to setup and maintain for large scale data collection across the vast oceans.
Over the past few decades, wireless communication technology has percolated into every aspect of our lives, and we have come to take it for granted. This technology forms the bedrock of wireless sensor networks, allowing us to gather data with ease. Most of the wireless communication technology we use relies on electromagnetic waves (e.g. radio waves, visible light) that get rapidly absorbed by water. Hence the technology is ineffective for underwater communication, except at very short distances or extremely low data rates. Most underwater communication systems today use acoustic waves, which can travel long distances in the right conditions. At short distances in clear waters, optical communication systems are sometimes used for high speed communications. Although these communication technologies can be leveraged to establish point-to-point communication links, these links do not integrate well with networking technology available today.
The Unet project strives to develop technologies that allow us to build communication networks that extend underwater, be it via acoustic, optical, or even wired links. Some nodes in such networks may be above water, while others are underwater. In this handbook, we explore how to build such networks using UnetStack3 , an agent-based network technology that was developed in the Unet project.
Who should read this book?
This book is intended for readers interested in deploying networks that extend underwater, or developing technology or protocols for use in underwater networks. Part I of the book provides an overview, and is recommended for all readers. Part II is aimed at readers who wish to deploy and maintain networks that extend underwater. Part III is aimed at application developers and software engineers who wish to integrate with UnetStack-based networks. Parts IV and V dive deeper into UnetStack, and are intended for researchers and engineers who wish to develop, simulate and test novel underwater networking protocols.
The book assumes that readers have a basic understanding of traditional networking technology. While expert software development skills are not required to benefit from this book, familiarity with scripting or programming is essential. Readers with knowledge of Java, Groovy and/or Python will find it easy to follow the examples in the text, but even readers without prior knowledge of these languages should be able to pick up necessary skills along the way.
Part I: Introduction to UnetStack
1. Introduction
1.1. What is a Unet?
The Internet has changed our lives beyond anyone’s wildest expectations, fundamentally changing the way we interact, the way we learn, and the way we work. More recently, devices have started connecting to the Internet, and communicating with other devices. This Internet of Things (IoT) has the potential to have a huge impact on the way we understand our environment, and interact with it. Given that most of our planet’s surface is covered with water, would it then not make sense that at least some of these devices might be in water? Some devices might measure ocean temperature and acidification to give us a handle on climate change, while other devices might monitor fresh water quality to ensure safe drinking water for us. Autonomous underwater vehicles (AUVs) may patrol our coastal waters looking for intruders, or tracking down sources of pollution or nutients that encourage harmful algal blooms. Be it static sensors or mobile AUVs, we need a way to connect them into a network that we can communicate and interact with. The Unet project strives to develop technologies that allow us to do precisely this. In this handbook, we explore how to use UnetStack3 , a technology developed as part of the Unet project, to build communication networks that extend underwater.
Most wireless technologies today rely on electromagnetic waves that don’t propagate well underwater. Therefore, to extend IoT underwater, we typically need a mix of technologies — cabled links where possible, otherwise radio frequency (RF) wireless links above water, and mid-to-long range wireless acoustic or short-range wireless optical links underwater. A "Unet" network (which we will simply call Unet henceforth) consists of several nodes (underwater, on the surface of water, or above water) that communicate over various types of links, as shown in Figure 1 .
A Unet consists of many Unet nodes (e.g. underwater sensor nodes, Autonomous Underwater Vehicles (AUVs), gateway buoys, ground stations, boats/ships) that generate, consume or relay data over a variety of links:
-
Acoustic links are typically used for mid-to-long range communication underwater. These links usually offer low data rates and long propagation delay due to the slow speed of sound in water (as compared to EM waves).
-
Optical links are used for short range high data rate communications in water.
-
RF links are used for mid-range communication in air.
-
GSM links are used for near-shore connectivity through air.
-
Satellite links are used for nodes that are far out at sea, and cannot be reached through GSM or RF links. These links usually are expensive and offer relatively low data rates.
-
Wired links (Ethernet, serial, fiber optic) are used for long-term static deployments underwater, or over short distances where cabling is feasible.
-
In some cases, nodes are retreived and data is transferred from them to other nodes in the network on a regular basis, using physical media (e.g. USB drives, SD cards, etc). These links usually offer very high data rates, but are only available intermittently. We dub such links as Sneakernet links .
A link is simply a logical connection between two nodes, often provided by equipping both the nodes with modems. We summarize various types of links in Table 1 .
| Link type | Communication range # | Data rate # | Latency |
|---|---|---|---|
|
High-frequency acoustic (underwater) |
Short |
Medium |
milliseconds |
|
Mid-frequency acoustic (underwater) |
Medium |
Low |
seconds |
|
Low-frequency acoustic (underwater) |
Long |
Very low |
seconds |
|
Optical (underwater) |
Very short |
High |
Negligible |
|
RF (in air) |
Medium |
Medium |
Negligible |
|
GSM (in air, near shore) |
Medium |
Medium |
milliseconds |
|
Satellite (in air) |
Long |
Low |
milliseconds |
|
Wired/cabled |
Long (expensive) |
High |
Negligible |
|
Sneakernet |
Long (intermittent) |
Very high |
hours or days |
|
# Communication range and data rate vary substantially across devices and environments. Short range usually is in tens of meters, medium range is several km, and long range is typically tens of km. Low data rates are in hundreds of bps, medium data rates are in kbps, and high data rates are in Mbps. |
1.2. UnetStack
Unet nodes are equipped with one or more network interfaces that allow communication over some of these links. For example, to communicate over an underwater acoustic link, we need an underwater acoustic modem . For an underwater optical link, we use an underwater optical modem . Most RF, GSM, satellite or wired links would be accessed over a standard TCP/IP network interface. In all cases, each Unet node would run the UnetStack software that allows us to effectively communicate over all of these types of links using a common Application Programming Interface (API). UnetStack API bindings are available for several languages including Java, Groovy, Python, Julia, C, Javascript, etc.
UnetStack has a several components, as depicted in Figure 2 :
-
The Unet framework provides core services, messages, agents and APIs needed by UnetStack.
-
The Unet basic stack is a collection of agents providing services and functionality required by typical Unets. These agents, together with the Unet framework, are sufficient to build fully functional Unets.
-
The Unet premium stack is a collection of agents providing advanced functionality and/or higher performance. Many of the premium agents provide similar services as the basic ones, but used advanced techniques for better performance and bandwidth efficiency.
-
The Unet simulator is able to simulate Unets with many nodes on a single computer. It can run in realtime simulation mode for interactive testing of agents and protocols, working to provide the user with the same user experience as in a real Unet. It can also be run in discrete event simulation mode to perform a large number of simulations in a short time, allowing Monte Carlo testing and performance evaluation of network protocols.
-
The Unet IDE is an integrated development environment (IDE) for developers to develop, simulate and test Unet agents and protocols. It also enables the developer to visualize and interact with simulated networks.
-
Unet audio is a soundcard-based realtime software defined open architecture acoustic modem (SDOAM) that runs on desktop, laptop or single-board computers, and can be used to build and test simple Unets. It is a great tool for not only developing and testing network protocols, but also developing acoustic communication techniques.
The components are packaged into various editions . The community edition is downloadable free of charge for educational and research purposes. It has all the components required to develop, simulate, test and deploy Unets. The commercial and OEM editions package offer advanced functionality, better performance and tighter integration with vendor-specific hardware.
In the next few chapters, we will learn how to use UnetStack and how to customize it to meet our networking needs. In some cases, it may be necessary to prototype and simulate a Unet before it is actually implemented. We will also learn how to do that using the Unet simulator.
2. Getting started
In this chapter, you will learn how to set up a simple 2-node underwater network with an acoustic link. If you already own a couple of UnetStack-compatible acoustic modems, you can certainly use them! And we’ll show you how to do that in Section 2.6 . But let us first start with a simulated 2-node underwater network, since all you need for this is a computer and the Unet simulator.
2.1. Setting up a simple simulated network
Download UnetStack community edition for your OS and untar/unzip it. Open a terminal window in the simulator’s root folder and start the simulator:
$ bin/unet samples/2-node-network.groovy
2-node network
--------------
Node A: tcp://localhost:1101, http://localhost:8081/
Node B: tcp://localhost:1102, http://localhost:8082/
If you’re using Windows, you may need to use:
bin\unet samples\handbook\2-node-network.groovy
|
Open two web browser windows and key in the two http URLs shown above in each browser. This should give you a command shell for node A and node B in the two browser windows.
2.2. Making your first transmission
On the command shell for node A, type:
> tell 0, 'hello!'
AGREE
Address 0 is a broadcast address, so you did not need to explicitly know the address of node B to transmit a message to it. After a short delay, you should see the message on the command shell for node B:
[232]: hello!
Congratulations!!! You have successfully transmitted your first message over the Unet.
The
[232]
that you see on node B is the
from
address (of node A). The simulator automatically allocates addresses to each node. You can easily find out the addresses of both nodes (on either node):
> host('A')
232
> host('B')
31
You can try sending a message back from node B:
> tell 232, 'hi!'
and you should see the message
[31]: hi!
on node A after just a short delay.
You could have specified the hostname instead of the address when sending the message:
tell host('A'), 'hi!'
.
|
2.3. Propagation delay & ranging
In the simulation, nodes A and B are placed 1 km apart. Since the speed of sound in water is about 1500 m/s (exact sound speed depends on temperature, salinity and depth), the signals take about 0.7 s to travel between the simulated nodes. This explains the short delays you see between sending the message from one node and receiving it on the other. You can also make use of this time delay to measure the distance between the nodes!
On node A, type:
> range host('B')
999.99976
You got an estimate of 1000 m for the range between the two nodes.
2.4. Sending & receiving application data
In real applications, you may want to send complex datagrams (messages) programmatically between nodes. The simplest way to do this is via the UnetSocket API ( Chapter 9 ). Let’s try it!
On node B, type:
> s = new UnetSocket(this); (1)
> rx = s.receive() (2)
| 1 |
Open a socket on node B (
this
refers to node B, since you are typing this on node B’s command shell). The semicolon (";") at the end of the statement simply prevents the shell from printing the return value automatically.
|
| 2 | Receive a datagram. This call blocks until a datagram is available. |
On node A, type:
> s = new UnetSocket(this);
> s.send('hello!' as byte[], 0) (1)
true
> s.close()
| 1 |
Send 6 ASCII bytes ('hello!') to address 0 (broadcast address). The
as byte[]
is necessary in Groovy to convert the string you specified into a byte array that the
send()
method expects.
|
Node B will receive the bytes as a
RxFrameNtf
message. You can check the data in the received datagram on the command shell for node B, and close the socket:
RxFrameNtf:INFORM[type:DATA from:232 rxTime:4134355059 (6 bytes)]
> rx.data
[104, 101, 108, 108, 111, 33] (1)
> new String(rx.data) (2)
hello!
> s.close()
| 1 | These are the bytes representing the ASCII characters ['h', 'e', 'l', 'l', 'o', '!']. |
| 2 | This puts together the ASCII characters in the byte array into a String. |
While we demonstrated the use of the
UnetSocket
API in Groovy on the command shell, the same commands work in a Groovy script or application, with one minor modification. When the socket is opened, you will have to specify the connection details (such as host name or IP address, and the API port number) of the modem (or simulated modem) to connect to. For example, if UnetStack is running on
localhost
at port number 1101, you can connect to it using:
s = new UnetSocket('localhost', 1101);
|
2.5. Sending & receiving from a Python application
UnetStack provides API bindings for many languages (Java, Groovy, Python, Julia, C, Javascript, etc). We demonstrate the use of the Python API here, but the usage is quite similar in other languages too.
We’ll assume you have Python 3.x already installed. Let us start by installing the UnetStack Python API bindings:
$ pip install unetpy
Collecting unetpy
Using cached unetpy-3.1.0-py3-none-any.whl (6.9 kB)
Collecting fjagepy>=1.7.0
Using cached fjagepy-1.7.0-py3-none-any.whl (12 kB)
Collecting numpy>=1.11
Using cached numpy-1.18.2-cp37-cp37m-macosx_10_9_x86_64.whl (15.1 MB)
Installing collected packages: numpy, fjagepy, unetpy
Successfully installed fjagepy-1.7.0 numpy-1.18.2 unetpy-3.1.0
We will now write
tx.py
and
rx.py
scripts to transmit and receive a datagram respectively. We assume that you have the two-node network setup from the previous section with node A and B available on
localhost
API port 1101 and 1102 respectively.
tx.py
from unetpy import UnetSocket
s = UnetSocket('localhost', 1101) (1)
s.send('hello!', 0) (2)
s.close()
| 1 |
Connect to node A (
localhost
API port 1101).
|
| 2 | Broadcast a 6-byte datagram. Address 0 is the broadcast address. |
rx.py
from unetpy import UnetSocket
s = UnetSocket('localhost', 1102) (1)
rx = s.receive() (2)
print('from node', rx.from_, ':', bytearray(rx.data).decode()) (3)
s.close()
| 1 |
Connect to node B (
localhost
API port 1102). Change the
localhost
to modem B’s IP address and port 1102 to port 1100, if you are working with a modem.
|
| 2 |
Blocking
receive()
will only return when a datagram is received or the socket is closed. If a datagram is received,
rx
will contain the notification message with the details of the datagram.
|
| 3 |
In Python
from
is a keyword and cannot be used as an field name. We therefore use
from_
for the source node address.
|
First run
python rx.py
to start reception. Then, on a separate terminal window, run
python tx.py
to initiate transmission. You should see the received datagram printed by the
rx.py
script:
$ python rx.py
from node 232 : hello!
Once you are done with your testing, it is time to shutdown the simulation. You can do that by pressing
Ctrl-C
on the terminal where you started the simulator. Alternatively, you can go to the shell of one of the nodes, and type:
shutdown
.
|
2.6. Using acoustic modems
So far, we have worked with a simulator. While the experience is similar, it is not exactly the same. There is no real substitute for working with real modems. If you happen to have two UnetStack-compatible acoustic modems, you can use them to set up a simple 2-node network. Put them in a water body (tank, bucket, lake, sea, …), power them on, and connect each to a computer over Ethernet. The setup would look something like this:
On each computer, open a web browser and key in the IP address of the respective modem. This should give us a command shell for node A and node B on the two computers.
| If you only have one computer available, you can connect both modems to the same Ethernet switch and connect to each modem’s IP address in separate browser windows. |
When working with modems, you may need to adjust the transmit power level to a suitable level for use in the water body that you have the modems in. Too high or too low a power level will not allow the modems to communicate well. The modem transmit power can be adjusted using the
plvl
command. Type
help plvl
on the command shell for node A to see examples of how the command is used:
> 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
The
help
command is your friend! Just type
help
to see a list of help topics. Type
help
followed by a command name, topic or parameter (you’ll learn more about these later) to get help information.
|
Assuming you have the modems in a bucket, you’ll need a fairly low transmit power. On node A, let us set the transmit power to -50 dB and try a transmission:
> plvl -50
OK
> tell 0, 'hello!'
AGREE
| A poor acoustic connection between modems can lead to multiple retransmits that can take many tens of seconds before successful delivery of message, or eventual delivery failure. |
If all goes well, you should see the message on node B:
[232]: hello!
Of course you’ll see a different "from" address than the one shown in the example here. It will be the actual address of your modem A. In case you don’t see the message on node B after a few seconds, you may want to adjust the power level up or down and try again.
All the other examples shown earlier in this chapter will also work with the modems. You’ll just need to replace the
localhost
with the appropriate modem IP address, and the API port for the modem will usually be 1100.
|
2.7. Transmitting and recording arbitrary acoustic waveforms
If you have UnetStack-compatible acoustic modems that support the BASEBAND service, you can use them to transmit and record arbitrary acoustic signals. Even without access to modems, you can try this out using the Unet audio SDOAM — a fully functional modem that uses your computer’s soundcard for transmission and reception. To start Unet audio, open a terminal window in the simulator’s root folder and type:
$ bin/unet audio
Modem web: http://localhost:8080/
This should start up the SDOAM and open a browser with a command shell accessing the modem. If the browser does not automatically open, just enter the modem web URL shown above in your browser. At the command shell, you can try transmitting a message:
> tell 0, 'hello!'
AGREE
You should hear the transmission from your computer speaker! If you don’t, check your speaker volume and try again.
If you have 2 computers running the Unet audio SDOAM, you can receive the transmitted signal on the second computer and see the received message:
[1]: hello!
.
|
Next, try sending a simple 10 kHz tonal signal:
> bbtx cw(10000, 0.5) (1)
AGREE
phy >> TxFrameNtf:INFORM[txTime:4104441] (2)
| 1 | Request transmission of a continuous wave (cw) signal of 10 kHz and 0.5 seconds duration. |
| 2 | Notification that the signal was successfully transmitted. |
You should hear a 0.5 second 10 kHz tone from your computer speaker. The
bbtx
command requests transmission of a baseband signal. The function
cw()
generates such a signal based on the specified frequency and duration.
To generate the baseband representation of the signal you wish to transmit, you will need to know the carrier frequency and the baseband sampling rate of the modem:
> phy.basebandRate
12000.0
> phy.carrierFrequency
12000.0
For the Unet audio SDOAM, the carrier frequency is 12 kHz and the baseband sampling rate is 12 kSa/s.
| The baseband signal is represented as a floating point array with alternate real and imaginary components in Java/Groovy. For languages that support complex numbers (e.g. Python, Julia), the signal is simply an array of complex numbers. |
You can equally easily ask the SDOAM to make an acoustic recording for you:
> bbrec 12000 (1)
AGREE
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:1911353 rssi:-61.2 fc:12000.0 fs:12000.0 (12000 baseband samples)]
| 1 | Request recording of 12000 baseband samples (1 second duration). |
The recording is sent to you as a
RxBasebandSignalNtf
message with 12000 baseband samples in the
signal
field. You can check the first 32 samples:
> ntf.signal[0..31]
[-3.735939E-4, 6.7323225E-4, 7.94507E-4, 5.0331384E-4, 0.0012656008, -0.0010853912, -2.0923217E-4, -8.322359E-4, 1.5215082E-4, 2.417963E-4, -3.0220395E-5, -5.190366E-4, -6.904016E-4, -7.3395047E-4, 3.9846844E-5, 5.161132E-4, 0.0013477469, 6.2060537E-4, 1.00925405E-4, -3.974573E-4, -8.8431453E-4, -5.807383E-4, -5.730035E-4, -8.5867435E-4, -9.026667E-4, 2.2320295E-5, -1.7575005E-5, 0.0010946163, 7.7881676E-4, -3.7582265E-4, -9.449492E-4, -1.7722705E-4]
The values you’d see would natually be different, since the SDOAM would have recorded whatever sounds it heard using your computer’s microphone.
While we illustrated the use of the BASEBAND service using the
bbtx
and
bbrec
commands, the same functionality can be accessed using the
TxBasebandSignalReq
and the
RecordBasebandSignalReq
messages. This is useful if you want to access the functionality from an agent or through the external gateway API (e.g. from a Jupyter Python notebook). You will learn how to do this in
Chapter 16
.
|
3. 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 section, we assume that you have shells open on nodes A and B.
On node A, we can ask for a list of agents running:
> ps
statemanager: org.arl.unet.state.StateManager - IDLE
remote: org.arl.unet.remote.RemoteControl - IDLE
rdp: org.arl.unet.net.RouteDiscoveryProtocol - IDLE
ranging: org.arl.unet.localization.Ranging - IDLE
uwlink: org.arl.unet.link.ReliableLink - IDLE
node: org.arl.unet.nodeinfo.NodeInfo - IDLE
websh: org.arl.fjage.shell.ShellAgent - RUNNING
simulator: org.arl.unet.sim.SimulationAgent - IDLE
phy: org.arl.unet.sim.HalfDuplexModem - IDLE
bbmon: org.arl.unet.bb.BasebandSignalMonitor - IDLE
arp: org.arl.unet.addr.AddressResolution - IDLE
transport: org.arl.unet.transport.SWTransport - IDLE
router: org.arl.unet.net.Router - IDLE
mac: org.arl.unet.mac.CSMA - IDLE
We can further ask for more details of a specific agent:
> phy
« Half-duplex modem »
Generic half duplex modem simulator.
[org.arl.unet.DatagramParam]
MTU ⤇ 56
RTU ⤇ 56
[org.arl.unet.bb.BasebandParam]
basebandRate = 12000.0
carrierFrequency = 12000.0
maxPreambleID ⤇ 4
maxSignalLength = 65536
signalPowerLevel = -42.0
[org.arl.unet.phy.PhysicalParam]
busy ⤇ false
maxPowerLevel = 0.0
minPowerLevel = -96.0
propagationSpeed ⤇ 1534.4574
refPowerLevel = 185.0
rxEnable = true
rxSensitivity = -200.0
time ⤇<PHONE_NUMBER>
timestampedTxDelay = 1.0
[org.arl.unet.sim.HalfDuplexModemParam]
basebandRxDuration = 1.0
clockOffset = 2932.3245
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
56
> phy.rxEnable
true
> phy.rxEnable = false
false
> phy.rxEnable
false
> phy.rxEnable = true
true
To 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 enabled
We can also ask for help on an agent:
> help phy
phy - access to physical service
Examples:
phy // access physical parameters
phy[CONTROL] // access control channel parameters
phy[DATA] // access data 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
Parameters:
The following parameters are available on all modems. Additional modem
dependent parameters are also available. For information on these
parameters type "help modem".
- phy.MTU - maximum transmission unit (MTU) in bytes
- phy.RTU - recommended data transfer size in bytes
- phy.rxEnable - true if reception enabled
- phy.propagationSpeed - propagation speed in m/s
- phy.timestampedTxDelay - delay before TX of timestamped frames
- phy.time - physical layer time (us)
- phy.busy - true if modem is TX/RX a frame, false if idle
- phy.refPowerLevel - reference power level in dB re uPa @ 1m
- phy.maxPowerLevel - maximum supported power level (relative to reference)
- phy.minPowerLevel - minimum supported power level (relative to reference)
Channel Parameters:
The following parameters are available on all modems. Additional modem
dependent parameters are also available. For information on these
parameters type "help modem".
- 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 - powel level used for transmission (relative to reference)
- phy[].errorDetection - number of bytes for error detection
- 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
From this help, we see that
phy
agent also supports channel parameters (also known as
indexed
parameters). It supports two logical channels, CONTROL (1) and DATA (2). The CONTROL channel is meant for low-rate robust data transmission, whereas the DATA channel is typically configured for higher rate data transmission. Channel parameters work in the same way as normal parameters, but with an index:
> phy[CONTROL]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 16
RTU ⤇ 16
[org.arl.unet.phy.PhysicalChannelParam]
dataRate = 202.10527
errorDetection ⤇ 1
fec ⤇ 0
fecList ⤇ null
frameDuration ⤇ 0.95
frameLength = 24
janus = false
llr ⤇ false
maxFrameLength = 128
powerLevel = -42.0
> phy[DATA]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 56
RTU ⤇ 56
[org.arl.unet.phy.PhysicalChannelParam]
dataRate = 731.4286
errorDetection ⤇ 1
fec ⤇ 0
fecList ⤇ null
frameDuration ⤇ 0.7
frameLength = 64
janus = false
llr ⤇ false
maxFrameLength = 512
powerLevel = -42.0
> phy[CONTROL].MTU
16
> phy[CONTROL].frameLength = 32
32
> phy[CONTROL].frameLength
32
> phy[CONTROL].MTU
24
> phy[CONTROL].frameLength = 24
24
The 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.
|
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 = -42.0
phy[2].powerLevel = -42.0
phy[3].powerLevel = -42.0
phy.signalPowerLevel = -42.0
> plvl -20
OK
> plvl
phy[1].powerLevel = -20.0
phy[2].powerLevel = -20.0
phy[3].powerLevel = -20.0
phy.signalPowerLevel = -20.0
The
plvl
command simply displays or sets the
powerLevel
parameter of all channels. The same can be manually accomplished by setting or getting individual parameters, if desired:
> phy[1].powerLevel
-20
> phy[1].powerLevel = -10
-10
> phy[1].powerLevel
-10
> plvl
phy[1].powerLevel = -10.0
phy[2].powerLevel = -20.0
phy[3].powerLevel = -20.0
phy.signalPowerLevel = -20.0
While
plvl
seems like a command to just set/get a
powerLevel
parameter, it does that for several channels in one go. This can save you a lot of time and typing — to achieve the same thing manually, you’d be typing 4 commands!
|
3.2. Interacting with agents using messages
While you can access a lot of functionality via parameters and commands, to fully harness the power of UnetStack, we require an understanding of the underlying messaging system between the agents. All agents support messages that expose their functionality. In fact, all parameters and commands are implemented by exchanging messages between the shell agent and other agents. In this section, we’ll take a brief look at how messaging between agents works.
All parameters and commands are implemented by exchanging messages between the shell agent and other agents. When you get/set a parameter, all the shell is doing is sending a
ParameterReq
message to the appropriate agent, and showing you the
ParameterRsp
message that the agent responds with.
|
Typically, we would want to send a
request
to an agent and get a
response
message back. This can be accomplished with the
request
call (or the equivalent alias
<<
) on the agent:
> phy << new TxFrameReq(data: [1,2,3])
AGREE
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:2913909740]
Here we made a request to the
phy
agent to transmit some data. The agent responded with an
AGREE
response, shortly followed by a
TxFrameNtf
notification from
phy
telling us that the transmission was successful.
| A frame is simply a datagram at the physical layer, also sometimes called a "packet". We prefer the term "frame" when working at the physical layer, but the distinction between frames and datagrams is unimportant at this point in time. We will come back to this later, in Chapter 15 . |
We can also use the return value in a condition, but we need to remember that the return value from the
request
is a message:
> x = phy << new TxFrameReq();
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:3381446740]
> x
AGREE
> x.class
class org.arl.fjage.Message
> x.performative
AGREE
> if (x.performative == Performative.AGREE) print 'OK'
OK
| The semicolon ";" at the end of the first statement prevents the return value from being printed on the shell. |
Unsolicited notification messages can be received by subscribing to the topic of interest. For example, on node B, we can subscribe to physical layer events on node B:
> subscribe phy
Now, if we broadcast a frame from node A using
phy << new TxFrameReq()
, we will see the relevant reception events on node B:
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:1765508396]
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 rxTime:1765508396]
The first event
RxFrameStartNtf
is triggered as soon as the frame is detected at node B. The second event
RxFrameNtf
is triggered when the frame is fully received, demodulated and successfully decoded at the receiver.
If all of this seems somewhat confusing to you, don’t worry about it. Most of the basic functionality of the stack can be accessed without having to deal with messages directly. As we need functionality that requires an understanding of messaging, we’ll gradually introduce them in later chapters.
3.3. Shell scripting
The default UnetStack shell accepts any Groovy code, and so is very flexible:
> 1+2
3
> 5.times { print it }
0
1
2
3
4
You can also define closures (if you’re not familiar with closures, you can think of them as functions for now):
> tx2 = {
- 2.times {
- phy << new TxFrameReq()
- }
- };
and call them later:
> tx2
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:3911898740]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:3912307740]
You can write Groovy scripts and store them in the
scripts
folder with an extension
.groovy
. You can then invoke them from the shell by simply typing the name of the script (without the extension).
|
This only scratches the surface of what the command shell is capable of. However, it should provide you a basic understanding of how the shell works, and illustrate its power. To understand more, we suggest that you explore the online
help
. As you further understand the UnetStack and fjåge API, you’ll develop expertise on using the shell.
Part II: Setting up underwater networks
4. Unet basics
Now that we have a basic understanding of how UnetStack works, it is time to take the next step into setting up and configuring underwater networks, or simply Unets .
4.1. Node names and addresses
Unet nodes are identified by unique addresses within the Unet. Small Unets might use 8-bit addresses, supporting up to 255 different nodes. Larger Unets might use 16-bit addresses, supporting up to 65535 different nodes in the network. The address space is controlled by the parameter
node.addressSize
, and must be set to the same value (either 8 or 16) on all nodes in a Unet.
| The code examples in this chapter assume that you have a simulated Unet (the 2-node-network simulation from Chapter 2 ) running, and you’re connected to the shell of one of the nodes. However, if you have access to modems, you may choose to use the real Unet and connect to the shell of one of the modem nodes. |
To check the current address size on your node:
> node
« Node information »
Manages and maintains node information and attributes.
[org.arl.unet.nodeinfo.NodeInfoParam]
nodeName = A
address = 232
addressSize = 8
canForward = true
mobility = false
location = [0.0, 0.0, -15.0]
origin = [NaN, NaN]
Some node parameters have not been shown in the above listing for brevity.
Address 0 is a broadcast address. All other addresses may be assigned to nodes in a Unet. Each Unet node is also associated with a node name (
node.nodeName
). If a node name is not explicity set, it defaults to the string representation of the node address. Descriptive node names may be used, if desired:
> node.nodeName = 'buoy_A'
buoy_A
> node
« Node information »
Manages and maintains node information and attributes.
[org.arl.unet.nodeinfo.NodeInfoParam]
nodeName = buoy_A
address = 232
addressSize = 8
canForward = true
mobility = false
location = [0.0, 0.0, -15.0]
origin = [NaN, NaN]
It is recommended that, if descriptive node names are used, the corresponding node addresses be set using the ADDRESS_RESOLUTION service. This ensures that name-to-address resolution leads to the correct address for the node. The ADDRESS_RESOLUTION service can be accessed via the
host()
shell command:
> host('buoy_A')
68
> node.address = host(node.nodeName)
68
The default ADDRESS_RESOLUTION agent in the UnetStack maps node names to node addresses using a hash function. The method reduces network traffic for host name resolution, but can lead to address conflicts between nodes if two names happen to map to the same address. It is the responsibility of the network engineer to resolve address conflicts manually during the setup of the network, if the default ADDRESS_RESOLUTION agent is used. For small networks, this is simply a matter of checking that all chosen node names in the network lead to unique node addresses:
> ['buoy_A', 'auv_1', 'auv_2', 'sensor_adcp1', 'sensor_ctd1'].each { name ->
- print "${name}: ${host(name)}"
- };
buoy_A: 68
auv_1: 150
auv_2: 109
sensor_adcp1: 43
sensor_ctd1: 14
4.2. Protocol numbers
Datagrams represent packets of data sent between nodes. Each node may have multiple agents and applications running on it, and so we need a way to specify which application the datagram is meant for. To aid with this, each datagram is associated with a protocol number that identifies the consumer on the destination node that the datagram is intended for. The consumer may be an agent or an end-user application. Protocol numbers can be thought of as port numbers in TCP/IP or UDP/IP.
The consumer may be an agent or an end-user application. Protocol number 0 (
Protocol.DATA
) is used for generic application data. Protocol numbers from 1 to
Protocol.USER-1
(31) are reserved for use by default stack agents. Protocol numbers from
Protocol.USER
(32) to
Protocol.MAX
(63) are available for end-user applications to use.
On node B, type:
> s = new UnetSocket(this);
> s.bind(Protocol.USER); // listen for datagrams with Protocol.USER
> rx = s.receive()
to wait for a reception with
Protocol.USER
.
On node A, type:
> s = new UnetSocket(this);
> s.connect(host('B'), Protocol.DATA); // send datagrams with Protocol.DATA
> s.send('hi!' as byte[]);
> s.connect(host('B'), Protocol.USER); // send datagrams with Protocol.USER
> s.send('hello!' as byte[]);
> s.close()
Node B will receive only the second message, since it is listening for datagrams with
Protocol.USER
only. We can confirm this by checking the data in the received datagram on the command shell for node B, and close the socket:
DatagramNtf:INFORM[from:68 to:31 protocol:32 (6 bytes)]
> new String(rx.data)
hello!
> s.close()
5. Setting up small networks
5.1. Netiquette testbed
The Netiquette testbed in Singapore is a 3-node network that is deployed at sea (see Figure 4 ), and accessible over the Internet. Nodes A and B are cabled seabed mounted nodes, while node C is a solar-powered buoy. We use a simulated version of the Netiquette testbed to learn how to set up and operate small networks.
To start the simulated network, we simply run the
netq-network.groovy
simulation script:
$ bin/unet samples/netq-network.groovy
Netiquette 3-node network
-------------------------
Node A: tcp://localhost:1101, http://localhost:8081/
Node B: tcp://localhost:1102, http://localhost:8082/
Node C: tcp://localhost:1103, http://localhost:8083/
The port numbers you see in the examples above aren’t particularly special. They are simply whatever were chosen by the developer of the simulation, and can be found in the
netq-network.groovy
script. The only restriction on the choice is that placed by the OS — usually port numbers below 1024 are
reserved
and unavailable to users. Of course, they must also be unique and unused by other applications running on your computer.
|
5.2. Node names & addresses
We start off by checking the configuration of each node:
Node A
> node
« Node information »
Manages and maintains node information and attributes.
[org.arl.unet.nodeinfo.NodeInfoParam]
address = 232
addressSize = 8
location = [121.0, 137.0, -10.0]
mobility = false
nodeName = A
origin = [1.216, 103.851]
Node B
> node
« Node information »
Manages and maintains node information and attributes.
[org.arl.unet.nodeinfo.NodeInfoParam]
address = 31
addressSize = 8
location = [160.0, -232.0, -15.0]
mobility = false
nodeName = B
origin = [1.216, 103.851]
Node C
> node
« Node information »
Manages and maintains node information and attributes.
[org.arl.unet.nodeinfo.NodeInfoParam]
address = 74
addressSize = 8
location = [651.0, 140.0, -5.0]
mobility = false
nodeName = C
origin = [1.216, 103.851]
All nodes are configured to use 8-bit addresses. Node A has is address 232, node B is 31, and node C is 74. The origin is set to GPS location 1.216° N, 103.851° E. Locations are measured in meters relative to this origin, with
x
axis pointing east, and
y
axis pointing north. The mobility of the nodes is set to
false
to indicate that the nodes are static (for mobile nodes, mobility should be set to
true
).
In the simulated network, all of the node parameters are correctly setup by the simulator. In a real network, you may need to setup each node by manually setting the appropriate parameters. To ensure that the nodes retain the parameters between reboots, once a node is setup, simply run
savestate
on the node. This creates a
saved-state.groovy
file in the
scripts
folder with the saved settings. The settings are then automatically loaded when the node is rebooted.
|
5.3. Connectivity & ranging
Let us first check the connectivity between the nodes:
Node A
> ping host('B')
PING 31
Response from 31: seq=0 rthops=2 time=2507 ms
Response from 31: seq=1 rthops=2 time=2852 ms
Response from 31: seq=2 rthops=2 time=2852 ms
3 packets transmitted, 3 packets received, 0% packet loss
> ping host('C')
PING 74
Response from 74: seq=0 rthops=2 time=2600 ms
Response from 74: seq=1 rthops=2 time=2634 ms
Response from 74: seq=2 rthops=2 time=2737 ms
3 packets transmitted, 3 packets received, 0% packet loss
The connectivity from node A to nodes B and C looks good. What about the connectivity from node B to node C?
Node B
> ping host('C')
PING 74
Response from 74: seq=0 rthops=2 time=2810 ms
Response from 74: seq=1 rthops=2 time=2666 ms
Response from 74: seq=2 rthops=2 time=2742 ms
3 packets transmitted, 3 packets received, 0% packet loss
Looks good too!
| In this simulation, everything checks out nicely. But, in the real world, there may be packet loss to contend with. We will see how to handle those in later chapters. |
We can also check cross-check that the routes from node A to nodes B and C are direct:
Node A
> trace host('B')
[232, 31, 232]
> trace host('C')
[232, 74, 232]
The first trace shows that the datagram originated at node A (address 232), reached node B (address 31), and was sent back to node A. The second trace similarly went from node A to node C (address 74) and back. No hops in between, since our network is fully connected.
We can also make range measurements (in meters) between the nodes:
Node A
> range host('A')
0.0
> range host('B')
371.08856
> range host('C')
530.0323
Node B
> range host('A')
371.08856
> range host('B')
0.0
> range host('C')
616.0877
5.4. Sending text messages
Once we have connectivity, we can of course send text messages from the shell:
Node A
> tell host('B'), 'hello!'
AGREE
and we see the text message on node B:
Node B
[232]: hello!
We have already seen in Chapter 2 and Section 4.2 on how to send text messages using the UnetSocket API from the shell, as well as from external applications. Hence we won’t dwell on it here.
5.5. File transfer and remote access
Data is often stored in files. Transferring files between nodes is a common requirement. File transfers and remote access is disabled by default. Let us enable this on node B:
Node B
> remote
« Remote control »
Text messaging and remote command execution service.
[org.arl.unet.remote.RemoteControlParam]
cwd = unet-3.2.0/scripts
dsp = transport
enable = false
groovy = true
reliability = true
shell = websh
> remote.enable = true
true
Now we can send & receive files, and run remote commands on node B. Let’s try it from node A:
Node A
> B = host('B')
31
> rsh B, 'tell me,"hi!"' (1)
AGREE
[31]: hi! (2)
> file('abc.txt').text = 'demo'; (3)
> ls (4)
abc.txt [4 bytes]
README.md [96 bytes]
> fput B, 'abc.txt' (5)
AGREE
| 1 |
Ask node B to send a "hi!" back to me. The variable
me
is automatically defined to be the source node address during the execution of the shell command when Groovy extensions are enabled (
remote.groovy = true
).
|
| 2 | On node A, we receive a "hi!" after a short delay. |
| 3 |
Create a file
abc.txt
with
demo
as content.
|
| 4 |
List local files to check that we have a 4-byte file called
abc.txt
.
|
| 5 |
Send file
abc.txt
to node B.
|
On the shell for node B, we see the notification that the file
abc.txt
was successfully received:
Node B
remote >> RemoteFileNtf:INFORM[from:232 filename:abc.txt (4 bytes)]
Although we demonstrated file transfers between nodes with the simulator, all simulated nodes are running on your machine and so sharing the filesystem. When the file
abc.txt
was transferred from node A to B, the same file was simply overwritten, since it was created in the same folder. You could easily verify this by checking the modification time of the file on the filesystem before and after the transfer.
|
You can also use
fget
to receive a file from a remote node, but you have to remember to set
remote.enable = true
on the receiving node:
Node A
> remote.enable = true
true
> fget B, 'abc.txt'
AGREE
remote >> RemoteFileNtf:INFORM[from:31 filename:abc.txt (4 bytes)]
> fget B, 'def.txt'
AGREE
remote >> RemoteFailureNtf:INFORM[RemoteFileGetReq:REQUEST[to:31 filename:def.txt] reason:no-file]
The last command failed to get file
def.txt
, as it does not exist on node B.
When we send commands to execute on a remote node, they are usually silently executed and the output is not sent back. If we want the output to be shown to us, we need to explicity ask for it using
tell
. Since this is often required, we have a simple Groovy extensions shortcut
?
to do this for us:
Node A
> rsh B, 'tell me,node.nodeName'
AGREE
[31]: B
> rsh B, '?node.nodeName'
AGREE
[31]: B
> rsh B, '?ls'
AGREE
[31]: abc.txt [4 bytes]
README.md [759 bytes]
> rsh B, '?1+2'
AGREE
[31]: 3
> rsh B, '?"You are ${me}, I am ${node.address}"'
AGREE
[31]: You are 232, I am 31
> rsh B, '?range '+host('C')
AGREE
[31]: 616.0877
Sometimes we are not interested in the output, but simply want an acknowledgement that the command was successfully executed. For example, if we set the transmission power on a remote node, we want to know that it was set. That can be requested using the
ack
function.
Node A
> ack on
> rsh B, 'plvl -6'
AGREE
remote >> RemoteSuccessNtf:INFORM[RemoteExecReq:REQUEST[to:31 command:plvl -6 ack:true]]
> ack off
5.6. Node locations & coordinate systems
As seen in Section 5.2 , some network nodes may know their own locations. This is useful for location-based routing and other applications. Depending on the application needs, we may wish to use different coordinate systems when setting up a network. There are 4 basic options to choose from:
- No coordinates
-
We do not know or care about each node’s location.
- Local coordinates
-
We wish to work in a local coordinate system, with only relative locations of the nodes being important.
- Georeferenced local coordinates
-
We wish to work in a local coordinate system, with relative node locations specified in local coordinates. The GPS coordinate of the origin of the local coordinate system is specified.
- GPS coordinates
-
We wish to specify the GPS location of each node, without defining a local coordinate system.
When node locations are not accurately known, we can opt not to define any coordinate system. Local coordinate systems are preferred in applications where such a coordinate system can be agreed upon for the entire network. Range computation and localization is easier to do in local coordinates. GPS coordinates are used when node location is important, but a local coordinate system cannot be easily defined (e.g. ad hoc network with no prior knowledge of area of operation).
UnetStack supports all 4 options through a set of simple conventions:
- No coordinates
-
node.origin = []
,node.location = []
for all nodes. - Local coordinates
-
node.origin = [Float.NaN, Float.NaN]
for all nodes.node.location = [x, y, z]
is specified as a 3-tuple in meters. The z axis points upwards (with sealevel being considered 0 m, and the half-space underwater having negative z coordinates), but the x and y axes are arbitrarily chosen. - Georeferenced local coordinates
-
node.origin = [latitude, longitude]
for all nodes, with latitude and longitude being the commonly agreed origin location.node.location = [x, y, z]
is specified as a 3-tuple in meters. The x axis points east, y axis points north, and the z axis points upwards (with sealevel being considered 0 m, and the half-space underwater having negative z coordinates). - GPS coordinates
-
node.origin = []
for all nodes, andnode.location = [latitude, longitude, z]
where the z axis points upwards (with sealevel being considered 0 m, and the half-space underwater having negative z coordinates).
| The Unet simulator requires a local coordinate system to be defined, and so only local coordinates or georeferenced local coordinates must be used in the simulator. |
In Section 5.3 , we measured the acoustic range between nodes A and B to about about 371 m. We can check this against distance computed from the location of nodes A and B. We first get the location of node A:
Node A
> node.location
[121.0, 137.0, -10.0]
and then compute the distance to it on node B:
Node B
> distance(node.location, [121.0, 137.0, -10.0])
371.0889
We see that it agrees well with the acoustic range!
It is often necessary to convert between the GPS coordinate system and the local coordinate system. To aid in this, UnetStack provides a set of utility functions:
Node A
> gps = new org.arl.unet.utils.GpsLocalFrame(node.origin); // set origin GPS
> gps.toGps(node.location[0..1]) // local to GPS
[1.217239, 103.852087] // GPS coordinates of node A
> gps.toLocal(1.21723898,<PHONE_NUMBER>) // GPS to local
[120.9994, 136.9999]
> node.location
[121.0, 137.0, -10.0]
The
GpsLocalFrame
class has additional constructors and utility methods to work with GPS coordinates in degrees, minutes and seconds, if desired.
6. Routing in larger networks
6.1. MISSION 2013 network
The MISSION 2013 experiment in Singapore featured a 7-node network that was deployed at sea (see
Figure 5
) for several weeks. The network operated in a challenging area with complex 3D bathymetry, several reefs and heavy shipping. During the experiment, we transmitted more than 40000 frames of data and collected statistics on communication performance across various links in the network. These performance statistics are embedded in the
Mission2013a
channel model in UnetStack. We use a simulated version of the MISSION 2013 network to learn how to set up and operate larger networks that require routing.
To start the simulated network, we simply run the
mission2013-network.groovy
simulation script:
$ bin/unet samples/mission2013-network.groovy
MISSION 2013 network
--------------------
Node 21: tcp://localhost:1121, http://localhost:8021/
Node 22: tcp://localhost:1122, http://localhost:8022/
Node 27: tcp://localhost:1127, http://localhost:8027/
Node 28: tcp://localhost:1128, http://localhost:8028/
Node 29: tcp://localhost:1129, http://localhost:8029/
Node 31: tcp://localhost:1131, http://localhost:8031/
Node 34: tcp://localhost:1134, http://localhost:8034/
While the MISSION 2013 network is not physically very large (only about 1.5 km across), the challenging environment kept the network from being fully connected, i.e., not all nodes could directly communicate with all others. The average frame delivery ratio (number of successfully delivered frames / number of transmitted frames) on each link is shown in Table 2 . The link quality is also shown on the map in Figure 5 , with dark blue links being the good ones, dark green ones being the weak ones, and brownish one being the very poor link.
| To: From: | 21 | 22 | 27 | 28 | 29 | 31 | 34 |
|---|---|---|---|---|---|---|---|
|
21 |
- |
0.926 |
0.266 |
0.917 |
0.912 |
0.000 |
0.552 |
|
22 |
0.867 |
- |
0.471 |
0.751 |
0.850 |
0.000 |
0.288 |
|
27 |
0.359 |
0.381 |
- |
0.313 |
0.322 |
0.000 |
0.000 |
|
28 |
0.847 |
0.869 |
0.390 |
- |
0.845 |
0.925 |
0.863 |
|
29 |
0.539 |
0.693 |
0.333 |
0.688 |
- |
0.374 |
0.000 |
|
31 |
0.000 |
0.000 |
0.000 |
0.902 |
0.805 |
- |
0.795 |
|
34 |
0.236 |
0.436 |
0.000 |
0.684 |
0.000 |
0.544 |
- |
6.2. Connectivity without routing
During the MISSION 2013 experiment, node 21 was a gateway node with surface expression and connectivity to the Internet (via a 3G cellular network). All other nodes were on the seabed and not directly accessible. So let us start by exploring the connectivity from node 21 to other nodes:
Node 21
> ping 22
PING 22
Response from 22: seq=0 rthops=2 time=2892 ms
Response from 22: seq=1 rthops=2 time=2912 ms
Response from 22: seq=2 rthops=2 time=3143 ms
3 packets transmitted, 3 packets received, 0% packet loss
> ping 27
PING 27
Request timeout for seq 0
Request timeout for seq 1
Response from 27: seq=2 rthops=2 time=11075 ms
3 packets transmitted, 1 packets received, 67% packet loss
> ping 28
PING 28
Response from 28: seq=0 rthops=2 time=2952 ms
Response from 28: seq=1 rthops=2 time=3110 ms
Response from 28: seq=2 rthops=2 time=3031 ms
3 packets transmitted, 3 packets received, 0% packet loss
> ping 29
PING 29
Response from 29: seq=0 rthops=2 time=3355 ms
Response from 29: seq=1 rthops=2 time=18720 ms
Request timeout for seq 2
3 packets transmitted, 2 packets received, 33% packet loss
> ping 31
PING 31
Request timeout for seq 0
Request timeout for seq 1
Request timeout for seq 2
3 packets transmitted, 0 packets received, 100% packet loss
> ping 34
PING 34
Request timeout for seq 0
Response from 34: seq=1 rthops=2 time=3294 ms
Response from 34: seq=2 rthops=2 time=3434 ms
3 packets transmitted, 2 packets received, 33% packet loss
We see that the connectivity to nodes 22 and 28 is good, that to nodes 27, 29 and 34 is poorer, and to node 31 is non-existent. Since the simulation is probabilistic, your exact results may differ.
6.3. Static routing
From Figure 5 and Table 2 , we see that node 28 has good connectivity to nodes 31 and 34, so perhaps we could relay datagrams via node 28. Although the link between 22 and 27 seems to be better than the rest, the connectivity to that node is generally poor. Let us set up the following routes:
-
Relay data between nodes 21 and 31 via node 28
-
Relay data between nodes 21 and 34 via node 28
On node 21, we add routes to nodes 31 and 34:
Node 21
> addroute 31, 28
OK
> addroute 34, 28
OK
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
riddcc 31 28 uwlink true 0 0.0 true
fkxbqm 34 28 uwlink true 0 0.0 true
On nodes 31 and 34, we add routes to node 21 via node 28, and enable remote access:
Node 31
> addroute 21, 28
OK
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
b7m7w9 21 28 uwlink true 0 0.0 true
> remote.enable = true
true
Node 34
> addroute 21, 28
OK
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
s6pjtb 21 28 uwlink true 0 0.0 true
> remote.enable = true
true
Now, we can check out connectivity from node 21 to nodes 31 and 34 again:
Node 21
> ping 31
PING 31
Response from 31: seq=0 rthops=4 time=18930 ms
Response from 31: seq=1 rthops=4 time=10680 ms
Response from 31: seq=2 rthops=4 time=46139 ms
3 packets transmitted, 3 packets received, 0% packet loss
> ping 34
PING 34
Response from 34: seq=0 rthops=4 time=26760 ms
Response from 34: seq=1 rthops=4 time=34408 ms
Response from 34: seq=2 rthops=4 time=21660 ms
3 packets transmitted, 3 packets received, 0% packet loss
Much better!
The pings to nodes 31 and 34 show
rthops
(round trip hops) to be 4, which makes sense, since we have 2-hop routes in each direction. We can ask the routing agent for a trace to check what route the datagram took:
Node 21
> trace 31
[21, 28, 31, 28, 21]
This shows that the datagram originated at node 21, passed through node 28 before reaching node 31. Then on the way back, it passed through node 28 again, and reached us back at node 21.
Let us next try to do something using the routes we created. We can get node 21 to ask node 31 to measure the range to node 34 and report it to us. This request will be relayed via node 28, since our routing tables are set up to do so. Remember to set
remote.enable = true
on node 31 before making the request from node 21:
Node 21
> rsh 31, '?range 34'
AGREE
[31]: 873.67
As you can see from Table 2 , the connectivity between nodes 31 and 34 is poor in this simulated network. You may need to try this command several times before you get a range estimate. When the ranging fails, you should see the message "ERROR: No response from remote node" back from node 31, which by itself demonstrates successful routing.
If you don’t have the patience to try a few times for range from node 31 to node 34, try getting a range from node 31 to 28, which will be much quicker:
rsh 31, '?range 28'
.
|
6.4. Route discovery
In the previous section, we learned how to set up static routes manually.  Or if we don’t have access to the nodes on the seabed to set up routes? We can use the route discovery agent to populate the routing tables.
To see how to do this, let us restart our MISSION 2013 simulation so that the routing tables are empty (alternatively we can remove the routes we created earlier by typing
delroutes
on nodes 21, 31 and 34). We can verify that the routing table is indeed empty:
Node 21
> routes
No routes available
Now, start a route discovery to node 31:
Node 21
> rreq 31
OK
Patiently wait for a minute or two before checking the routing table on node 21:
Node 21
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
69gnxp 22 22 uwlink true 1 0.0 true
ib3goj 28 28 uwlink true 1 0.0 true
68ozs5 29 29 uwlink true 1 0.0 true
gkfin2 31 29 uwlink true 2 -1.0 true
Your routing table may differ, as the route discovery process is probabilistic. We see that we now have a route to node 31 via node 29. Let us check the routing table on node 31 as well, to see if it has a corresponding entry for a route to node 21:
Node 31
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
1hveg6 28 28 uwlink true 1 0.0 true
pjfhin 21 28 uwlink true 3 -1.0 true
n5eqll 34 34 uwlink true 1 0.0 true
8jlzj3 21 34 uwlink true 3 -2.0 true
f6dit 29 29 uwlink true 1 0.0 true
qqtwvd 21 29 uwlink true 3 -2.0 true
Indeed it does! In fact, it has 3 routes back to node 21, one via node 29, and two more via nodes 28 and 34. Of these routes, the route via node 28 has the largest metric, and so will be the route that is used. We can verify that by issuing a trace from node 21:
Node 21
> trace 31
[21, 29, 31, 28, 21]
Since the route discovery process is probabilistic, it may be useful to repeat the route discovery if good routes are not established after a single try. The
rreq
command can also be called with parameters to control the repetition. For example
rreq 31, 3, 6, 30
will initiate 6 route discoveries to node 31 looking for up to 3-hop routes spaced by 30 seconds between discoveries.
|
7. Wired and over-the-air links
The networks we explored in the last few chapters were completely underwater. All links were underwater acoustic links. If we wanted to replace some of the acoustic links with underwater optical or RF links, or even through-the-air cellular, WiFi or RF links, that could easily be done, as long as you had a modem driver (a specific type of agent) that supported the device that provided the link. Cellular, WiFi and other devices often already have TCP/IP network stacks running on them, to provide seamless connectivity to the Internet. UnetStack can leverage the existing network stack in these devices without having to develop new modem drivers, by translating Unet datagrams to UDP/IP datagrams, tunneling them through the IP network, and translating them back to Unet datagrams at the other end.
7.1. The UdpLink agent
The
UdpLink
agent offers the LINK service (
Chapter 21
) over an IP network.
To see how this works, let us revisit the MISSION 2013 network from Figure 5 . Recall that node 21 was a gateway node with surface expression, and was connected to the Internet via a 3G cellular IP connection. During the experiment, we had no direct acoustic connectivity between nodes 21 and 31, and hence we routed all communication to node 31 via node 28.
Let us consider a scenario where node 31 also has a surface expression and 3G cellular IP connectivity. In this case, it would be nice to have a direct link from node 21 to node 31 via UDP/IP. Let’s see how to set that up.
Fire up the
mission2013-network.groovy
network simulation (or if you already have it running from the last chapter, terminate and restart it, so that we have no routes in our routing tables). Connect to node 21’s shell and add the
UdpLink
agent, and setup a route to node 31 via the UDP link:
> container.add 'udplink', new UdpLink();
> udplink
« UDP/IP Link »
Link protocol over UDP/IP for use over wired/wireless IP networks.
[org.arl.unet.DatagramParam]
MTU ⤇ 65535
RTU ⤇ 1450
[org.arl.unet.link.LinkParam]
dataRate = 0.0
[org.arl.unet.link.UdpLinkParam]
advertise = 30
broadcastAddress = <IP_ADDRESS>
monitorTimeout = 200
port = 5100
retries = 2
timeout = 0.5
> addroute 31, 31, udplink
OK
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
dhlx8t 31 31 udplink true 1 0.0 true
Similarly, connect to node 31’s shell and add the
UdpLink
agent as well as a route to node 21 via the UDP link:
> container.add 'udplink', new UdpLink();
> addroute 21, 21, udplink
OK
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
lbouxd 21 21 udplink true 1 0.0 true
Go back to node 21’s shell and see if you can communicate to node 31 via the UDP link:
> ping 31
PING 31
Response from 31: seq=0 rthops=2 time=27 ms
Response from 31: seq=1 rthops=2 time=5 ms
Response from 31: seq=2 rthops=2 time=4 ms
3 packets transmitted, 3 packets received, 0% packet loss
> ack on
> tell 31, 'hello'
AGREE
remote >> RemoteSuccessNtf:INFORM[RemoteTextReq:REQUEST[to:31 text:hello ack:true]]
and on node 31, you’ll see:
[21]: hello
You’ll also notice that the communication is much faster, since the UDP/IP latency is low and data rate is much higher.
7.2. Multilink routing
When we added the
UdpLink
agent in the last section, we set up static routes manually on both nodes. Let’s delete these routes on both nodes:
> delroutes
Now, let’s see what the route discovery agent does when we ask it to discover routes for us:
> rreq 31
OK
> routes (1)
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
w7iayp 31 31 udplink true 1 0.0 true
> routes (2)
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
w7iayp 31 31 udplink true 1 0.0 true
6gji1z 22 22 uwlink true 1 0.0 true
8zhn5v 28 28 uwlink true 1 0.0 true
mjhfaw 31 28 uwlink true 2 -1.0 true
fj4g2j 34 34 uwlink true 1 0.0 true
2r1ymj 28 22 uwlink true 2 -1.0 true
ii73zj 31 22 uwlink true 3 -2.0 true
> trace 31 (3)
[21, 31, 21]
| 1 |
Checking routes within a few seconds after the
rreq
, we see that the route via the
udplink
is discovered very quickly.
|
| 2 | After a few minutes, we see that additional acoustic routes are also discovered (your routes may vary, as the route discovery is a probabilistic process). |
| 3 |
The route used for data transfer is the single-hop
udplink
route to node 31 and back.
|
Note that the route discovery resulted in 3 routes to node 31 in this case. The first one is a single-hop UDP (
udplink
) route. The second one is an acoustic route (using
uwlink
) via node 28, and the third one is a 3-hop acoustic route via node 22. We can see that the metric for the 2-hop and 3-hop acoustic routes is lower than that of the UDP route, and so the UDP route is used for data transfer. The metric is computed based on a combination of number of hops and the packet loss on a route.
You can check the routing table on node 31:
> routes
uuid to nextHop link reliability hops metric enabled
---------------------------------------------------------------------
v5lej 21 21 udplink true 1 0.0 true
uxl8yr 28 28 uwlink true 1 0.0 true
bvsu21 21 28 uwlink true 2 -1.0 true
9q9x91 34 34 uwlink true 1 0.0 true
vvzke2 21 34 uwlink true 3 -2.0 true
We see 3 routes (direct/
udplink
, via node 28/
uwlink
and via node 34/
uwlink
), and the route with the largest metric is still the
udplink
direct route.
Part III: Building Unet applications
8. Interfacing with UnetStack
You now know how to set up a Unet. Let us next explore how you can go about interfacing your application with UnetStack to take advantage of the Unet. There are several options available:
-
The UnetSocket API ( Chapter 9 ) is the most convenient way of interface most modern applications with UnetStack. API bindings are available for many languages, including Java, Groovy, Python, Julia, Javascript and C. The API allows you to send and receive user data over the Unet, get and set agent parameters, and access advanced functionality by interacting with agents using messages.
-
UDP portals ( Section 10.1 ) provide a way to establish tunnels through the Unet for UDP datagrams. This facility can be used to transparently run applications that use UDP, over the Unet.
-
TCP portals ( Section 10.3 ) and serial portals ( Section 10.4 ) provide a way to establish connection-oriented tunnels through the Unet. This is a simple way to run applications that communicate over a TCP/IP or serial port links, over the Unet.
-
Many traditional modems provide an AT command set for applications to interact with them. While we do not encourage the use of AT commands (as they are error-prone and limited in functionality), it would be amiss not to mention that UnetStack also supports an AT script engine ( Chapter 12 ) that may be used by legacy applications to interact with it using AT commands.
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://<IP_ADDRESS>:1101 [API]
ws://<IP_ADDRESS>:8081/ws [API]
websh: ws://<IP_ADDRESS>: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
<IP_ADDRESS>
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('<IP_ADDRESS>', 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('<IP_ADDRESS>', 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('<IP_ADDRESS>', 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.
|
10. Portals
Although the UnetSocket API provides great flexibility, it requires an application to explicity use the API to integrate with UnetStack. Sometimes you might have devices or applications that talk to each other over a serial cable or a UDP/IP or TCP/IP connection, and you simply want to replace the cable or connection with an underwater wireless connection. Is there an easier way for such simple Internet or serial port applications to communicate over a Unet?
The answer lies in portals. A portal is a transparent connection across the Unet. Data going in through one end of the portal travels through the Unet and emerges from the other end. The interaction with the end points of the portal is via traditional technologies such as UDP/IP sockets, TCP/IP sockets, or serial ports. This enables applications developed to use these technologies to transparently work over a Unet.
| While portals are easy to use, you should bear in mind that applications developed for the Internet or for use over serial ports do not understand the constraints and characteristics of a Unet. Some applications may use bandwidth inefficiently, or expect responses with latencies that are unreasonable for a Unet, and therefore perform poorly. |
10.1. UDP portal
Since UDP is a datagram-oriented protocol, it is easy to map UDP datagrams to Unet datagrams. This is exactly what a UDP portal does, as shown in Figure 6 .
In this example, application X sends a UDP datagram to node A, where a UdpPortal agent listens on UDP port 7000 (arbitrarily chosen port number). The UdpPortal agent converts the UDP datagram into a Unet datagram and sends it to node B. The UdpPortal agent on node B receives this datagram, converts is back to a UDP datagram and sends it to application Y listening on UDP port 7778 (also an arbitrarily chosen port number).
Let us emulate this example with our favorite 2-node network. Fire up the 2-node network simulation, as before:
$ bin/unet samples/2-node-network.groovy
2-node network
--------------
Node A: tcp://localhost:1101, http://localhost:8081/
Node B: tcp://localhost:1102, http://localhost:8082/
Open browser windows for shell access to each of the nodes. On node A, create a UdpPortal listening on port 7000 (since application X will send UDP datagrams to this port) and sending Unet datagrams (protocol 0 in this example, but that can be configured using the
protocol
parameter below) to node B:
> container.add 'portal', new org.arl.unet.portal.UdpPortal(port:7000, peer:host('B'));
> portal
« UDP Portal »
Transparent transport for UDP frames through a network.
[org.arl.unet.portal.UdpPortalParam]
clientIP = <IP_ADDRESS>
clientPort = 7778
dsp = uwlink
peer = 31
port = 7000
priority = NORMAL
protocol = 0
reliability = false
The default
port
and
clientPort
for the UdpPortal are arbitrarily chosen to be 7777 and 7778 respectively. You can easily change them during creation of the UdpPortal, as we did above, or later, by setting the relevant parameter.
On node B, create the other end-point of the UDP portal to send the UDP datagrams to
localhost
UDP port 7778, where you will run application Y:
> container.add 'portal', new org.arl.unet.portal.UdpPortal(clientIP: 'localhost', clientPort: 7778);
> portal
« UDP Portal »
Transparent transport for UDP frames through a network.
[org.arl.unet.portal.UdpPortalParam]
clientIP = localhost
clientPort = 7778
dsp = uwlink
peer = 0
port = 7777
priority = NORMAL
protocol = 0
reliability = false
That’s it, your UDP portal is set up! Time to test it out!!
To test the UDP portal (and later, the TCP portal), we will use
netcat
or
nc
. If you don’t have this installed on your machine, now would be a good time to go download and install it.
|
Open a terminal window on your machine and set up a simple UDP server listening on port 7778 (application Y):
$ nc -u -l 7778
Open another terminal window and set up a simple UDP client to send text datagrams to port 7000 (application X). Assuming your IP address is
<IP_ADDRESS>
, you can do this using the command shown below. Type a text message and press ENTER.
$ nc -u <IP_ADDRESS> 7000
hello (1)
| 1 | Type your text message "hello" followed by ENTER. |
In a few seconds, you should see that text message appearing on application Y terminal:
$ nc -u -l 7778 (1)
hello (2)
| 1 | You had already typed this in earlier. |
| 2 | Your text message "hello" appears here. |
The text message went through the Unet to get there!
You may need to use the IP address of your machine (e.g.
<IP_ADDRESS>
) for the UDP connection to send the text message, rather than
localhost
. This is because the UdpPortal binds to the default network interface, and not to the loopback network interface.
|
10.2. Video streaming using UDP portal
You can do some cool things once you have set up the UDP portal. Here’s one real-life example:
Say, you wanted to stream video through the Unet. If you have
ffmpeg
installed, you can set up a UDP video client listening on port 7778:
$ ffplay udp://<IP_ADDRESS>:7778
and you can stream a video (
movie.m4v
) over UDP to port 7000:
$ ffmpeg -re -i movie.m4v -an -s cif -r 6 -c:v libx264 -b:v 15k -f mpegts udp://<IP_ADDRESS>:7000?pkt_size=512
The various flags control the quality, frame rate, and encoding of the video, and the
pkt_size
option controls the size of the datagrams sent.
The
ffmpeg
flags need to be adjusted to suit your Unet (read the
ffmpeg
documentation!). You need to ensure that the links in the Unet can support the data rates needed for this video, based on the flags you select. We have demonstrated real-time video with a high-speed acoustic underwater link with data rates of about 40 kbps.
|
10.3. TCP portal
A TCP portal is set up using the Portal agent. The Portal agent is quite similar to the UdpPortal agent, but provides more flexibility through the fjåge connectors framework. We can use a TCP connector for our TCP portal.
Restart your 2-node network, and on node A set up a TCP portal listening on port 7000:
> container.add 'portal', new org.arl.unet.portal.Portal(7000);
> portal
« Portal »
Transparent transport for TCP/IP or serial connections through a network.
[org.arl.unet.portal.PortalParam]
delimiters = [10, 13]
dsp = uwlink
peer = 0
priority = NORMAL
protocol = 0
reliability = false
timeout = 1000
On node B, create the other end-point of the TCP portal listening on port 7001:
> container.add 'portal', new org.arl.unet.portal.Portal(7001);
That’s it, your TCP portal is set up! Time to test it out!!
Open a terminal window on your machine and connect over TCP/IP to node A:
$ nc localhost 7000
Open another terminal window and connect over TCP/IP to node B. Type a text message and press ENTER.
$ nc localhost 7001
hello (1)
| 1 | Type in your text message "hello", and press ENTER. |
In a few seconds, you should see that text message appearing on the TCP/IP connection to node A:
$ nc localhost 7000 (1)
hello (2)
| 1 | You had already typed this in above. |
| 2 | Your text message "hello" appears here. |
The text message went through the Unet to get there!
The TCP portal is bidirectional, so you can type something on node A, and you should see it appear on node B. The UDP portal in
Section 10.1
can also be set up as bidirectional by carefully configuring the
peer
,
port
, and
clientPort
parameters at both end-points.
|
10.4. Serial portal
Since the Portal agent uses the fjåge connectors framework, it can easily work with any type of connector. Since fjåge provides a serial port connecor, we can easily set up a serial portal on each of your nodes:
> container.add 'portal', new org.arl.unet.portal.Portal('/dev/ttyS0', 9600, 'N81');
Since many modern computers do not have serial ports, you may not be able to test the above code on your computer. If you have underwater modems with serial ports, you’ll need to replace the device name (
/dev/ttyS0
) with the appropriate serial port device name to run this code. You can also customize the serial port baud rate (
9600
) and settings (
N81
).
|
Once you have the serial portal set up on all nodes, you can connect to the node’s serial port using a serial terminal application (e.g.
minicom
) and type text messages just like you did with
nc
during the TCP portal test.
11. Wormholes
Portals ( Chapter 10 ) provide a way to transparently tunnel data through a Unet. Wormholes, on the other hand, provide a way to easily communicate between agents in different Unet nodes.
The fjåge agent framework forms the inter-agent communication backbone of a Unet node. All agents in one Unet node live in one fjåge universe , and can seamlessly communicate with each other. However, agents in different nodes live in different fjåge universes, and typically only communicate with peer agents on other nodes using protocols implemented over Unet links. Wormholes connect multiple fjåge universes over Unet links, allowing all agents in multiple nodes to transparently talk to each other!
Why would we want to do this? The usefulness of this is best understood through an application that we explore next.
11.1. Diver tracking application
Imagine a network with gateway node A (a standalone buoy), surface node B (deployed from a boat), and an underwater node C (diver). Nodes A and B have underwater acoustic modems and in-air WiFi connectivity. Node C is fully submerged, and only has acoustic connectivity to nodes A and B. An diver tracking script on node B wishes to track the location of diver node C. This requires the script on node B to make range measurements btween node B and node C (easy to do!), but also between node A and node C (not so easy!). The traditional approach to this problem would be to deploy a helper agent on node A to make the range measurement, and have the script communicate with the agent using a custom protocol over a UDP link over WiFi. Another approach would be to enable remote access (
Chapter 24
) on node A, and use
rsh
to execute the ranging commands on node A, and send back the results using
tell
. While this would work, it would be quite fragile. Wormholes provide a much simpler and robust way to do this.
We’ll use the Netiquette 3-node network to demonstrate how to do this:
$ bin/unet samples/netq-network.groovy
Netiquette 3-node network
-------------------------
Node A: tcp://localhost:1101, http://localhost:8081/
Node B: tcp://localhost:1102, http://localhost:8082/
Node C: tcp://localhost:1103, http://localhost:8083/
Nodes A and B are assumed to be connected over an in-air WiFi network, and so we can enable
UdpLink
on both nodes and establish connectivity over it:
> container.add 'udplink', new UdpLink();
> addroute host('B'), host('B'), udplink
OK
> container.add 'udplink', new UdpLink();
> addroute host('A'), host('A'), udplink
OK
> ping host('A')
PING 232
Response from 232: seq=0 rthops=2 time=3 ms
Response from 232: seq=1 rthops=2 time=4 ms
Response from 232: seq=2 rthops=2 time=3 ms
3 packets transmitted, 3 packets received, 0% packet loss
Next, we enable the
Wormhole
agent on both nodes, and set it up to use the
udplink
for connectivity:
> container.add 'wh', new org.arl.unet.wormhole.Wormhole();
> wh.dsp = udplink;
> wh
« Wormhole »
[org.arl.unet.wormhole.WormholeParam]
compression = true
dsp = udplink
publish = []
publishTo = 0
That’s it for the setup! We’re now ready to prototype out our application over the wormhole.
The functionality we needed was to make range measurements between nodes B and C, and nodes A and C, from a script on node B. Let’s start off measuring range to node C from node B and call it
r1
:
> ranging << new RangeReq(to: host('C'))
AGREE
ranging >> RangeNtf:INFORM[from:31 to:74 range:616.0877 offset:-1751533450 rxTime:4096892937]
> r1 = ntf.range
616.0877
In a script, you’d probably want to use
ntf = receive(RangeNtf, 5000)
to access the
RangeNtf
that comes back, but we’ll stick to doing this just in the shell for the demonstration. Now comes the
magic
. From the shell on node B, we want to ask the ranging agent on node A (address 232) to make a range measurement to node C for us:
> agent('ranging@232') << new RangeReq(to: host('C'))
AGREE
ranging@232 >> RangeNtf:INFORM[from:232 to:74 range:530.0323 offset:636287273 rxTime:1806486132]
> r2 = ntf.range
530.0323
We are able to communicate with agents on other nodes connected through the wormhole by simply adding a suffix
@999
where
999
is the node address. So
ranging@232
refers to the agent
ranging
on node with address
232
.
|
Now that we have
r1
and
r2
, we can use it to compute the location of the diver in our script.
11.2. Publishing and subscribing over wormholes
In the diver tracking application above, we performed two-way travel-time (TWTT) ranging from nodes A and B to node C. While this works well, each diver location measurement required transmission of 4 frames. With one-way travel-time (OWTT) ranging (see Chapter 17 ), we could make each measurement in just one transmission from diver node C, as long as we had accurate clocks at all nodes.
First, let us enable OWTT ranging by synchronizing clocks between nodes A and C, and nodes B and C (see Section 17.3.2 for details):
> ranging[host('C')].lifetime = 3600
3600
> range host('C')
530.0323
> ranging[host('C')].sync // verify that the sync was achieved
true
> subscribe ranging
> ranging[host('C')].lifetime = 3600
3600
> range host('C')
616.0877
> ranging[host('C')].sync // verify that the sync was achieved
true
> subscribe ranging
OWTT is now enabled. To send out a beacon on node C, we use the
beacon
command (or equivalently send a
BeaconReq
message):
> beacon
AGREE
On nodes A and B, you’ll see the
RangeNtf
notifications:
ranging >> RangeNtf:INFORM[from:232 to:74 range:530.0323 rxTime:3059430132]
ranging >> RangeNtf:INFORM[from:31 to:74 range:616.0877 rxTime:5447306937]
Diver node C can now send out a beacon transmission regularly, and ranging agents on node A and node B will publish
RangeNtf
notifications on their respective agent topics. On node B, we would then want to subscribe to the ranging agent topics on both nodes. We are already subscribed to node B’s ranging agent, and connected over the wormhole (established in the previous section) to node A. Since we can refer to the ranging agent on node A as
agent('ranging@232')
, we should be able to subscribe to it:
> subscribe agent('ranging@232')
Now, try sending a
beacon
again from node C, and see what we get at node B:
ranging >> RangeNtf:INFORM[from:31 to:74 range:616.0877 rxTime:6152654937]
We got the notification from node B’s agent, but nothing from node A! 😕
By default, wormholes do not publish messages on topics. However, it is easy to enable certain topics to be published. To enable this, we set the
wh.publish
parameter on node A:
> wh.publish = [topic(ranging)]
[ranging__ntf]
We can restrict the publication of the topic to only certain peer nodes, if we wish, by setting the
wh.publishTo
parameter to the address of the target node. By default, it is set to 0 (broadcast).
|
Now, try sending a
beacon
again from node C, and see what we get at node B:
ranging@232 >> RangeNtf:INFORM[from:232 to:74 range:530.0323 rxTime:4129955132]
ranging >> RangeNtf:INFORM[from:31 to:74 range:616.0877 rxTime:6517831937]
Cool! We got both ranges on node B now. 🙂
We can now write our application script to call
receive(RangeNtf)
to get the ranges, and computes the position of the diver everytime the diver node transmits a beacon.
12. AT script engine
| The UnetSocket API ( Chapter 9 ) is the recommended way to integrate applications with UnetStack. The AT script engine is only provided for legacy system support, and its use should be avoided in modern systems, as they suffer from several drawbacks (serialized interaction, error prone parsing, limited flexibility, lack of scripting support, etc.) |
The AT command interpreter provides support for legacy applications that prefer to interact with UnetStack using AT text commands. All AT commands begin on a fresh line with an
AT
prefix, and end with a new line (CR or LF). Spaces and other whitespace characters are considered significant. Lines without the
AT
command prefix are silently ignored and may be used as a comment in AT command files. All AT commands are case-insensitive, with the exception of Java class names and parameter names in the
AT~EXT
and
AT~MSG
commands.
A successful response to an AT command may span several lines of text followed by an
OK
to mark the end of the response. If the AT command is unsuccessful, an
ERROR
response is returned. The
OK
or
ERROR
response is also terminated by a new line (CR or LF).
Unsolicited notifications may be sent by the AT command interpreter to the user. Responses and notifications are atomic, and lines from each may not be interleaved in the other.
We describe the AT command set through examples below. The convention used in describing commands is that the command is in uppercase. Lowercase words denote parameters in the command, to be replaced by the user with appropriate values. Optional parts of the command are denoted by […].
12.1. Starting the AT command interpreter
The AT command interpreter is available as a script engine that can be loaded using a shell agent:
> iface ATScriptEngine, 5001
Once started, you can connect to port 5001 (assuming you started the interpreter on TCP port 5001 as shown above) using a terminal window:
$ nc localhost 5001
Now you can interact with the AT command interpreter.
You can also bind the AT command interpreter to a serial port, if desired. See
help iface
for details.
|
12.2. Basic AT commands
A small set of basic AT commands are honored by the interpreter:
-
AT
— check if a command link is active:
AT
OK
-
ATE0
/ATE1
— turn off/on echo:
ATE1
OK
AT
AT
OK
ATE0
ATE0
OK
AT
OK
-
ATZ
— shutdown/reboot -
AT/
— repeat last AT command
12.3. Shell extensions
The interpreter can be customized using shell extensions:
-
AT~EXT=classname
— load shell extension
Fields and methods exposed by a shell extension are made available in a shell using this command. Example:
AT~PLVL
ERROR
AT~EXT=org.arl.unet.phy.PhysicalShellExt
OK
AT~PLVL
PHY/1.POWERLEVEL=-42.0
PHY/2.POWERLEVEL=-42.0
PHY/3.POWERLEVEL=-42.0
PHY/4.POWERLEVEL=-42.0
PHY.SIGNALPOWERLEVEL=-42.0
OK
AT~PLVL=-3
OK
AT~PLVL
PHY/1.POWERLEVEL=-3.0
PHY/2.POWERLEVEL=-3.0
PHY/3.POWERLEVEL=-3.0
PHY/4.POWERLEVEL=-3.0
PHY.SIGNALPOWERLEVEL=-3.0
OK
The parameters to methods are specified as a comma-separated list after the
=
symbol in the command (e.g.
-3
in
AT~PLVL=-3
). The parameters may be numeric (
int
,
long
,
float
or
double
),
boolean
represented by
0
or
1
as
false
and
true
respectively, or double-quoted strings (e.g.
"this is a string"
).
12.4. Agent parameter access commands
Agent parameters may be listed, read and written to:
-
AT~agent[/index]?
— list parameters:
AT~PHY?
PHY.SIGNALPOWERLEVEL=-10.0
PHY.RXENABLE=1
PHY.MAXPOWERLEVEL=0.0
PHY.MINPOWERLEVEL=-138.0
PHY.NOISE=-71.9
PHY.MTU=13
PHY.FULLDUPLEX=1
PHY.BUSY=0
PHY.RTC="Tue Jul 23 02:19:39 SGT 2019"
OK
AT~PHY/1?
PHY/1.FRAMELENGTH=18
PHY/1.FEC=3
PHY/1.MTU=13
PHY/1.DATARATE=52.554745
PHY/1.FRAMEDURATION=2.74
PHY/1.MODULATION="fhbfsk"
PHY/1.POWERLEVEL=-10.0
PHY/1.VALID=1
PHY/1.THRESHOLD=0.25
OK
-
AT~agent[/index].parameter?
— get parameter:
AT~PHY/1.FRAMELENGTH?
PHY/1.FRAMELENGTH=18
OK
-
AT~agent[/index].parameter=value
— set parameter:
AT~PHY/1.FRAMELENGTH=21
OK
AT~PHY/1.FRAMELENGTH?
PHY/1.FRAMELENGTH=21
OK
12.5. Sending and receiving messages
The command interpreter may make requests and receive message notification by defining the messages of interest and subscribing to appropriate topics:
-
AT~MSG:<msg>=<classname>:parameter[,parameter]…
— define message format
Message formats defined using this command are available for requests and also used for notifications. If a message is not defined, notifications of that message type are silently ignored. The following command defines a message
DRQ
of class
org.arl.unet.DatagramReq
with 3 parameters:
to
,
protocol
and
data
in that order:
AT~MSG:DRQ=org.arl.unet.DatagramReq:to,protocol,data
OK
We also define other messages similarly:
AT~MSG:TXNTF=org.arl.unet.phy.TxFrameNtf:type,txTime
OK
AT~MSG:RXNTF=org.arl.unet.phy.RxFrameNtf:from,to,protocol,rxTime,data
OK
-
AT~agent<msg=parameter[,parameter]…
— make a request
Once we have defined the messages above, we can make a request to
PHY
to send a datagram to node 2 with protocol 0 and 3 bytes of data:
[1,2,3]
:
AT~PHY<DRQ=2,0,"010203"
OK
The notification for the datagram transmission completion will be displayed as an unsolicited notification:
~PHY>TXNTF=2,1994962099
The general notifications format as:
~agent>msg=parameter[,parameter]…
. If any of the parameters are
byte[]
or
float[]
, they are not included in the parameter list. Instead a colon (
:
) is added at the end of the line, and the data in hex follows on subsequent lines. Once the data ends, a period (
.
) is sent on a single line. If multiple parameters are arrays, the number of array parameters is given by the number of colons at the end of the line, and each array is terminated by a period, followed by the next array. An example is shown below:
~PHY>RXNTF=1,0,0,2095058353:
0102030405060708090A0B0C0D0E0F
1112131415161718191A1B1C1D1E1F
.
-
AT~SUB=topic[,subtopic]
— subscribe to a topic
Without subscribing to a topic, we see that the user is not notified about the reception of a frame, although the message type is already defined:
AT~PHY.FULLDUPLEX=1
OK
AT~PHY<DRQ=0,0,"010203"
OK
~PHY>TXNTF=2,2095026099
After subscribing to
PHY
, the received message is reported:
AT~SUB=PHY
OK
AT~PHY<DRQ=0,0,"010203"
OK
~PHY>TXNTF=2,2095026099
~PHY>RXNTF=1,0,0,2095058353:
010203
.
Here we see that the data from the
RXNTF
is included after the notification message as a
data block
. This is the case for all
byte[]
or
float[]
parameters. Each data block may span several lines, and is terminated by a period (
.
) on a line by itself. The number of data blocks to follow a notification is denoted by the number of colons (
:
) at the end of a notification.
-
AT~UNSUB=topic[,subtopic]
— unsubscribe from a topic:
AT~UNSUB=PHY
OK
AT~PHY<DRQ=0,0,"010203"
OK
~PHY>TXNTF=2,2095026099
12.6. Managing the data buffer
While data may be directly included in a request message, sometimes it is useful to load data into a data buffer first, and then use it multiple times for requests. This is managed using the following commands:
-
AT~DATA:
— load data buffer
Data is represented as a series of hexadecimal bytes, and may span many lines. Data entry is terminated by a period (
.
) on a line by itself:
AT~DATA:
010203
040506
.
OK
The above representation is convenient for
byte[]
parameters. However, the same representation is used for other data arrays, including
float[]
, where the IEEE floating point representation is used for the floating point number to be converted to a series of bytes.
An alternative data representation is useful for
float[]
, where the floating point numbers are directly specified:
AT~DATA:
1.54
0.78
5.92
2.00
.
OK
For this representation, it is necessary to have a decimal place (
.
) in each number, and each line to contain only one floating point number.
-
AT~DATA?
— check size of data buffer:
AT~DATA:
010203
040506
.
OK
AT~DATA?
6 bytes
OK
-
AT~CLRDATA
— clear data buffer:
AT~CLRDATA
OK
AT~DATA?
EMPTY
OK
To use the data buffer, we simply use
"DATA"
instead of the hexadecimal data in a message. For example:
AT~SUB=PHY
OK
AT~DATA:
010203
040506
.
OK
AT~PHY<DRQ=0,0,"DATA"
OK
~PHY>TXNTF=2,3738882099
~PHY>RXNTF=1,0,0,3738925936:
010203040506
.
Part IV: Understanding UnetStack services
13. Services and capabilities
So far you’ve interacted with agents, checking and changing agent parameters, and sending and receiving messages. But how do you know which agents to send what messages to, and what agents support which parameters? The answer to this question lies in the concept of services .
13.1. Terminology
To fully understand services, we need to formally define a few terms, many of which you are already somewhat familiar with:
- Agent
-
An agent is logical entity that implements a specific functionality of the network. Loosely, an agent maps to a layer in a traditional network stack, but is more flexible. Each agent has its own thread of execution, and all agents can be thought of as running concurrently. An agent is normally referenced using its AgentID . You may think of an AgentID as the name of an agent, or a reference to the agent.
- Message
-
Agents interact with each other via messages. Agents can send and receive messages, and typically expose all their functionality as a set of messages that they will respond to. Messages are transmitted within the network stack on a node, and not between nodes in the network. Each message is tagged with a performative that summarizes the purpose of the message. Common performatives are
REQUEST
,AGREE
,REFUSE
,FAILURE
,NOT_UNDERSTOOD
andINFORM
.
| It is easy to confuse messages, datagrams and frames. Messages are used by agents on a node to interact with other agents on the same node. They are never transmitted! Datagrams are logical packets of data that are exchanged between nodes. Datagrams may be fragmented and reassembled, and thus one datagram does not necessarily map to one transmission. Physical layer datagrams are called frames; they form the basic unit of data exchanged between nodes. |
- Request
-
Request messages ask an agent to perform some task. Such messages are marked with the performative
REQUEST
, and it is a convention to name the message class with a suffixReq
(e.g.DatagramReq
,ParameterReq
). - Response
-
When an agent receives a request, it must respond back to the requesting agent. Common responses are simply messages with the performative set to
AGREE
,REFUSE
,FAILURE
orNOT_UNDERSTOOD
. AnAGREE
message confirms to the requester that the agent will perform the requested task. AREFUSE
message tells the requester that the request cannot be performed. AFAILURE
message, on the other hand, means that the agent should have been able to do the request under normal circumstances, but something went wrong. ANOT_UNDERSTOOD
response is generated if the agent does not know how to deal with the request. Other than these simple messages, responses may sometimes contain more information. Such messages are respresented by message classes with a suffixRsp
(e.g.ParameterRsp
), and may have a performative ofINFORM
to indicate that they contain information in response to the request. - Notification
-
Agents sometimes generate unsolicited information. This information is encapsulated in a notification message, typically with a performative
INFORM
. Notification messages may be sent to a specific agent, or on a topic. - Topic
-
A topic defines a publish-subscribe mechanism where an agent may publish some notifications, and other agents interested in those notifications may subscribe to the topic. Most agents have an unnamed topic associated with themselves, that other agents can subscribe to. For example, a link agent may subscribe to the topic of a physical layer agent to listen for incoming data frames from the physical layer.
- Parameters
-
Most UnetStack agents publish a number of parameters associated with them. Parameters are key-value pairs that provide information about the agent (read-only parameters), or allow controlling the behavior of the agent (read-write parameters). Parameters are technically accessed via
ParameterReq
andParameterRsp
messages, but a simpler notation (agent.parameter
) is also available to get/set parameters. - Service
-
A service is a collection of messages (requests, responses and notifications) and parameters that an agent honors. Agents publish the list of services they offer, and you can find agents through the services they advertise.
- Capability
-
Services often define optional capabilities. These capabilities may be offered by some agents advertising a service, but may be omitted by others. An agent can be queried to check if it supports a specific capability using the
CapabilityReq
message.
| If all of these terms pique your curiosity, you may wish to take a look at the fjåge documentation . fjåge is the underlying agent framework that UnetStack is built on. While we don’t assume familiarity with fjåge in this handbook, your understanding of UnetStack would certainly be quicker if you were to invest in building that familiarity. |
13.2. Finding service providers
In
Section 9.5
, we have already come across the
agentForService()
function that helps us find an agent that provides a specific service. In this section, we’ll explore this a bit more.
Fire up your trusty 2-node network and connect to node A’s shell:
> a = agentForService(org.arl.unet.Services.PHYSICAL);
> a.name
phy
We asked for an agent that provides the
org.arl.unet.Services.PHYSICAL
service, and UnetStack responded back with an agent ID of an agent that can provide you that service. The name of that agent on node A is
phy
.
But what if there were more than one agents capable of providing the same service? We can ask for the list of all agents that provide a service:
> agentsForService(org.arl.unet.Services.PHYSICAL)
[phy]
Well, there was only one agent providing the PHYSICAL service. Are there other services that multiple agents provide? Indeed, there are:
> agentsForService(org.arl.unet.Services.DATAGRAM)
[transport, router, uwlink, phy]
The DATAGRAM service is provided by several agents. If you were interested in all incoming datagrams, you’d need to subscribe to the topics of all these agents!
What would have happened if you only asked for
agentForService()
instead of
agentsForService()
for the DATAGRAM service? Try it:
> a = agentForService(org.arl.unet.Services.DATAGRAM);
> a.name
transport
Any one of the agents in the list is returned!
Ever wondered what we assigned the
a = agentForService(…)
, and then asked for
a.name
to see the name of the agent?
When you try to print an AgentID object on the shell, the shell tries to be helpful and queries the agent for all its parameters and displays them. In this case, we were only interested in the name and not all the parameters, so we asked the shell not to get the parameters by explicitly asking for just
a.name
.
|
You can also ask an agent for the list of services it provides:
> phy.services
[org.arl.unet.Services.PHYSICAL, org.arl.unet.Services.DATAGRAM, org.arl.unet.Services.BASEBAND]
or get a list of all services provided by all agents in the stack:
> services
org.arl.unet.Services.NODE_INFO: node
org.arl.unet.Services.PHYSICAL: phy
org.arl.unet.Services.REMOTE: remote
org.arl.unet.Services.TRANSPORT: transport
org.arl.unet.Services.ADDRESS_RESOLUTION: arp
org.arl.unet.Services.MAC: mac
org.arl.unet.Services.RANGING: ranging
org.arl.fjage.shell.Services.SHELL: websh
org.arl.unet.Services.DATAGRAM: transport router uwlink phy
org.arl.unet.Services.BASEBAND: phy
org.arl.unet.Services.LINK: uwlink
org.arl.unet.Services.ROUTING: router
org.arl.unet.Services.STATE_MANAGER: statemanager
org.arl.unet.Services.ROUTE_MAINTENANCE: rdp
13.3. Checking capabilities
So let’s say you looked up the list of agents that provide the DATAGRAM service:
> agentsForService(org.arl.unet.Services.DATAGRAM)
[transport, router, uwlink, phy]
If you wanted to send a datagram, how do you pick which one you’d rather use? Different agents may provide different optional capabilities. If you were specifically interested in a particular capability (e.g. reliability), you could ask the agent if it supported that:
> phy << new CapabilityReq(org.arl.unet.DatagramCapability.RELIABILITY)
DISCONFIRM
> uwlink << new CapabilityReq(org.arl.unet.DatagramCapability.RELIABILITY)
CONFIRM
Here, we asked
phy
if it can do reliable datagram delivery, and it said "no". Then we asked
uwlink
, and it confirmed that it can. If you needed reliable delivery of our datagram, you should choose the latter.
You can also ask an agent to list all its optional capabilities:
> transport << new CapabilityReq()
CapabilityListRsp:INFORM[PROGRESS,RELIABILITY,FRAGMENTATION,CANCELLATION]
The
transport
agent says it can do reliable datagram delivery, fragment & reassemble large datagrams (if necessary), report on the progress of large datagram transfers, and cancel datagram delivery half way through the process (if the user wishes to).
Another way you may choose a service provider is by checking its parameters. For example, the
MTU
parameter (defined in the DATAGRAM service) tells you what is the largest datagram the agent can deliver:
> phy.MTU
56
> uwlink.MTU
3145632
If you had a small datagram (56 bytes or less) to deliver, and you did not care about reliability, you could ask
phy
to deliver it for you. But, if your datagram was larger, even if you did not need reliability, you’d have to ask
uwlink
to deliver it for you.
The
MTU
parameter is the DATAGRAM service is actually
org.arl.unet.DatagramParam.MTU
. Since we only have one
MTU
parameter that
phy
advertises, there is no ambiguity in using
phy.MTU
. But if you wanted to explicitly ask for the parameter by its fully qualified name, you could send a
ParameterReq
for it:
phy << new ParameterReq().get(org.arl.unet.DatagramParam.MTU)
|
13.4. Service list
The following services are currently defined in UnetStack:
| Short name | Fully qualified name | Description | Read… |
|---|---|---|---|
|
DATAGRAM |
|
Send and receive datagrams |
|
|
PHYSICAL |
|
Physical layer |
|
|
BASEBAND |
|
Arbitrary waveform transmission & recording |
|
|
RANGING |
|
Ranging & synchronization |
|
|
NODE_INFO |
|
Node & network information |
|
|
ADDRESS_RESOLUTION |
|
Address allocation & resolution |
|
|
LINK |
|
Datagram transmission over a single hop |
|
|
MAC |
|
Medium access control |
|
|
ROUTING |
|
Routing of datagrams over a multihop network |
|
|
ROUTE_MAINTENANCE |
|
Discovery & maintenance of routes in a multihop network |
|
|
TRANSPORT |
|
Datagram transmission over a multihop network |
|
|
REMOTE |
|
Remote command execution, text messaging & file transfer |
|
|
STATE_MANAGER |
|
State persistence across node reboots |
|
|
SCHEDULER |
|
Sleep-wake scheduling for energy management |
|
|
SHELL |
|
Commmand execution & file management services |
You can enjoy reading more about these services in the next few chapters.
14. Datagram service
org.arl.unet.Services.DATAGRAM
Unets are all about sending datagrams between nodes!
The DATAGRAM service is, therefore, one of the fundamental services that many agents provide. However, different agents have very different capabilities in terms of what they can do with datagrams. Let’s take a look at what the service offers, and how to use it effectively.
14.1. Overview
14.1.1. Messages
Agents offering the DATAGRAM service support messages to transmit and receive datagrams:
-
DatagramReq
⇒AGREE
/REFUSE
/FAILURE
-
DatagramNtf
— sent to the agent’s topic, when a datagram is received
14.1.2. Parameters
Agents offering the DATAGRAM service support the following parameter:
14.1.3. Capabilities
Agents may support several optional capabilities:
Agents capable of fragmentation may break a datagram into smaller datagrams, transmit each of them across the network, and reassemble them on the peer node.
If an agent advertises relaibility, it is able to acknowledge the successful delivery of the datagram. Reliability is enabled on a per-datagram basis by setting the
reliability
flag in the
DatagramReq
. Depending on whether the datagram could be successfully delivered or not, one of the following notifications is generated:
-
DatagramDeliveryNtf
— sent to requestor -
DatagramFailureNtf
— sent to requestor
Agents capable of reporting progress do so by periodically sending the following notification for long data transfers:
-
DatagramProgressNtf
— sent to requestor on the transmitting node, and agent’s topic on the receiving node
If an agent supports cancellation, it honors the following request for stopping an ongoing data transfer:
-
DatagramCancelReq
⇒AGREE
/REFUSE
/FAILURE
/NOT_UNDERSTOOD
(if unsupported)
Agents advertising this capability prioritize datagrams with higher
priority
indicated in the
DatagramReq
.
Agents that spool
DatagramReq
over extended periods of time usually advertise this capability. They discard
DatagramReq
that are undelivered after the time-to-live (
ttl
attribute of the
DatagramReq
) has expired.
Agents advertising this capability may apply data compression to incoming
DatagramReq
messages. If it does so, the peer agent will decompress the data before sending the
DatagramNtf
.
14.2. Examples
Fire up your 2-node network again, and open two browser windows — one connecting to node A’s shell, and the other to node B’s shell.
On node B:
> agentsForService(org.arl.unet.Services.DATAGRAM)
[transport, router, uwlink, phy]
> phy.MTU
56
> uwlink.MTU
848
> uwlink << new CapabilityReq()
CapabilityListRsp:INFORM[RELIABILITY,FRAGMENTATION]
> subscribe uwlink
We obtained a list of agents which provide the DATAGRAM service. We checked the
MTU
of the
phy
and
uwlink
agents, and found them to be 56 bytes and 848 bytes respectively. We decided to use the
uwlink
agent to communicate over a single-hop link between node A and B, and checked its capabilities. We found that it supports reliability and fragmentation. We then decided to listen to all datagrams received by node B’s
uwlink
agent by subscribing to its topic.
On node A:
> uwlink << new DatagramReq(to: 31, data: new byte[64]) (1)
AGREE
> uwlink << new DatagramReq(to: 31, data: new byte[64], reliability: true) (2)
AGREE
uwlink >> DatagramDeliveryNtf:INFORM[id:4dda055e-a533-401f-89c8-a01065ca5d70 to:31] (3)
> uwlink << new DatagramReq(to: 37, data: new byte[64], reliability: true) (4)
AGREE
uwlink >> DatagramFailureNtf:INFORM[id:bc65e643-6161-4b06-913c-3ff4f6985d36 to:37] (5)
> uwlink << new DatagramReq(to: 0, data: new byte[64]) (6)
AGREE
> uwlink << new DatagramReq(to: 0, data: new byte[64], reliability: true) (7)
REFUSE: Reliability not supported for broadcast
> uwlink << new DatagramReq(to: 31, data: new byte[1024]) (8)
REFUSE: Data length exceeds MTU
| 1 | Send an unreliable datagram to node 31. |
| 2 | Send a reliable datagram to node 31. |
| 3 | Successful delivery of reliable datagram reported. |
| 4 | Send a reliable datagram to node 37. Since node 37 does not exist in this network, this should eventually fail. |
| 5 | Delivery failure reported (after trying for a few minutes). |
| 6 | Broadcast an unreliable datagram. |
| 7 | Broadcast request for reliable datagram is refused, as reliability requires a response from peer node and therefore cannot be supported on broadcast. |
| 8 | Datagram transmission request for data larger than MTU is also refused. |
If we look at the shell for node B, we should see the 3 successfully delivered datagrams:
uwlink >> DatagramNtf:INFORM[from:232 to:31 (64 bytes)]
uwlink >> DatagramNtf:INFORM[from:232 to:31 (64 bytes)]
uwlink >> DatagramNtf:INFORM[from:232 (64 bytes)]
Agent
uwlink
uses the PHYSICAL service (agent
phy
) to deliver the data. Since the
phy.MTU
is only 56 bytes, but our datagrams were 64 bytes, unbeknownst to us, the
uwlink
agent must have been fragmenting these datagrams and reassembling them on the other side!
|
14.3. Short-circuit delivery
We were able to successfully deliver datagrams from node A to node B in the examples in the previous section. We not only saw the
DatagramNtf
messages on node B, but also got
DatagramDeliveryNtf
on node A if
reliability
was enabled.
Let’s try it again, but with a small difference. On node A:
> uwlink << new DatagramReq(to: 31, data: new byte[32])
AGREE
We transmitted a smaller datagram, and node A happily accepted it for delivery. However, if we look at the shell for node B, we don’t see a
DatagramNtf
message corresponding to the datagram, even though you had already subscribed to
uwlink
! What’s going on? Let’s try it again, but this time enable reliability:
> uwlink << new DatagramReq(to: 31, data: new byte[32], reliability: true)
AGREE
uwlink >> DatagramDeliveryNtf:INFORM[id:4aaa86e5-9a56-46f8-bc1a-f6be33af03a4 to:31]
We see that the datagram was indeed delivered! And now, if we look at node B’s shell, we’ll see the delivery notification:
uwlink >> DatagramNtf:INFORM[from:232 to:31 (32 bytes)]
It seems that enabling reliability successfully delivered the datagram, but otherwise the
DatagramNtf
message did not appear on node B’s shell! You can try this many times, and the result will be the same. So it can’t be random packet loss in the network either. What’s going on?
To try and troubleshoot this, let’s subscribe to notifications from the
phy
agent to see if the data is arriving at the physical layer. On node B:
> subscribe phy
On node A, transmit the unreliable small datagram again:
> uwlink << new DatagramReq(to: 31, data: new byte[32])
AGREE
On node B, we now see a couple of notifications:
phy >> RxFrameStartNtf:INFORM[type:DATA rxTime:3956973678]
phy >> RxFrameNtf:INFORM[type:DATA from:232 to:31 rxTime:3956973678 (32 bytes)]
The first notification says that the physical layer detected the start of a data frame. The second notification is for a received frame with 32 bytes from node 232 to node 31. That’s our datagram!!! But why is it delivered by
phy
and not
uwlink
, when it was sent by
uwlink
on node A? And why is it a
RxFrameNtf
instead of a
DatagramNtf
?
Let’s solve the second mystery first. An
RxFrameNtf
is a subclass of
DatagramNtf
, so it is indeed a
DatagramNtf
message. We can easily verify this on node B:
> ntf
RxFrameNtf:INFORM[type:DATA from:232 to:31 rxTime:3956973678 (32 bytes)]
> ntf instanceof DatagramNtf
true
Variable
ntf
contains the last notification received. It is the
RxFrameNtf
, and it is indeed an instance of
DatagramNtf
. So, we indeed got the datagram on node B, and it was delivered as a
DatagramNtf
with the correct metadata.
But why was it sent on
phy
agent’s topic and not
uwlink
agent’s topic, like all other datagrams we transmitted?
This is due to an optimization known as
short-circuit delivery
(introduced in UnetStack 3), depicted in
Figure 8
. The
uwlink
agent on node A looked at the unreliable
DatagramReq
for 32 bytes and realized that it is within the
phy
agent’s capability (no reliability needed, and the datagram size is less than
phy.MTU
) to deliver this without the help of the
uwlink
agent. It delegated the task to the
phy
agent, which in turn send the datagram to its peer on node B, and therefore it was delivered to us by the
phy
agent on node B. This delegation not only reduces computation, but more importantly reduces the overhead of link headers in the frame, and therefore save valuable bandwidth in a resource-constrained underwater network.
Short-circuit delivery is not only done by
uwlink
, but by all agents supporting the DATAGRAM service. If a downstream agent is capable of delivering the datagram, the delivery is delegated automatically.
As a result of short-circuit delivery optimization, you need to subscribe to
all
DATAGRAM service providers to receive
DatagramNtf
messages, and not just the one you send the datagram via.
|
uwlink
on node A recognizes the
DatagramReq
to be within the
phy
agent’s capability, and delegates it without adding any headers. On node B, the received frame is directly delivered as a
DatagramNtf
by the
phy
agent, since
uwlink
functionality is not required.
On node B, we should have done this in the first place:
> agentsForService(org.arl.unet.Services.DATAGRAM).each { subscribe it }
This single-liner in Groovy iterates over the list of agents providing the DATAGRAM service, and subscribes to the topic of each agent in that list.
Agents should use the call
subscribeForService(org.arl.unet.Services.DATAGRAM)
instead. This call subscribes to all agents providing teh DATAGRAM service, but has an added advantage: it also asks the agent to keep track of new agents that are added to the stack later, and subscribes to them if they provide the DATAGRAM service.
|
14.4. Datagrams and the UnetSocket API
The UnetSocket API also supports delivery of datagrams. Let’s try it. On node A:
> s = new UnetSocket(this);
> s.send new DatagramReq(to: 31, data: new byte[32])
true
On node B, we will see the datagram delivery:
uwlink >> DatagramNtf:INFORM[from:232 to:31 (32 bytes)]
Note that we did not have to specify an agent or service when making the datagram request via the UnetSocket API. An appropriate agent was automatically selected by the API for us. In this case, the
uwlink
agent was used by the API to deliver the datagram.
15. Physical service
org.arl.unet.Services.PHYSICAL
Agents offering the PHYSICAL service are most commonly modem drivers and modem simulators. They support messages and parameters that are explained below. PHYSICAL service providers may also provide optional capabilities to send frames triggered at a specified time, or send timestamped frames where the timestamp is embedded in the transmitted frame.
| Agents implementing the PHYSICAL service typically directly access the channel, bypassing any MAC protocol that may be in use in the network. It is highly recommended that clients wishing to use the PHYSICAL service consult with the MAC service for advice on when it is safe to access the channel, so as not to adversely affect the network performance. |
15.1. Overview
All agents supporting the PHYSICAL service must also support the DATAGRAM service ( Chapter 14 ).
15.1.1. Messages
Agents supporting the PHYSICAL service provide a set of messages to manage frame transmission and reception:
-
TxFrameReq
⇒AGREE
/REFUSE
/FAILURE
— transmit a frame -
TxRawFrameReq
⇒AGREE
/REFUSE
/FAILURE
— transmit a frame without headers -
ClearReq
⇒AGREE
/REFUSE
/FAILURE
— cancel all ongoing and pending transmissions -
RxFrameNtf
— sent to agent’s topic when a frame addressed to the node is received, and the agent’sSNOOP
sub-topic when a frame addressed to other nodes is overheard -
TxFrameNtf
— sent to requestor when a frame is transmitted -
RxFrameStartNtf
— sent to agent’s topic when a frame is detected -
TxFrameStartNtf
— sent to agent’s topic when a frame transmission is started -
BadFrameNtf
— sent to agent’s topic when a bad frame is received -
CollisionNtf
— sent to agent’s topic when a frame is detected (and dropped) while another frame is being received
The
TxFrameReq
class extends a
DatagramReq
to add physical layer options, and the
RxFrameNtf
class extends a
DatagramNtf
to add physical layer metadata.
15.1.2. Parameters
Agents offering the PHYSICAL service support the following parameters:
-
rxEnable
— true if reception is enabled, false otherwise -
propagationSpeed
— signal propagation speed in m/s -
time
— current physical layer clock time in microseconds -
busy
— true if modem is busy transmitting/receiving (carrier sense), false if modem is idle -
refPowerLevel
— reference source level in dB (re micro-Pascals @ 1m for underwater modems) -
maxPowerLevel
— maximum allowable transmission power in dB rerefPowerLevel
-
minPowerLevel
— minimum allowable transmission power in dB rerefPowerLevel
-
rxSensitivity
— reference receive sensitivity (dB re micro Pascals for underwater modems)
All physical layer timestamps are in microseconds as per the clock provided by the
time
parameter. This clock is generally not synchronized with the platform clock (system time).
|
In addition to the above parameters, agents also support indexed (frame type) parameters:
-
frameDuration
— frame duration in seconds (maximum duration in case of variable frame length) -
powerLevel
— transmission power in dB rerefPowerLevel
-
errorDetection
— number of bytes used for error detection -
frameLength
— frame length in bytes (maximum length in case of variable frame length) -
maxFrameLength
— maximum possible frame length in bytes -
fec
— forward error correction (FEC) code (0 = none/default, otherwise base 1 index from fecList) -
fecList
— list of available FEC code names (in the order of increasing robustness), may be null if FEC change not supported -
dataRate
— effective frame data rate (bps) -
llr
— true to enable log-likelihood ratio reporting inBadFrameNtf
, false otherwise
Two frame types are defined:
15.1.3. Capabilities
Agents may support several optional capabilities:
Agents advertising this capability are able to transmit frames with a transmission timestamp (start of transmission) encapsulated in the frame. This is requested through the
timestamped
flag in the
TxFrameReq
message. In order to do the timestamping, the frame has to be scheduled for transmission after a short delay. This delay is configured via an additional parameter:
-
timestampedTxDelay
— delay in seconds to transmit timestamped frames
Agents advertising this capability are able to start transmitting a frame at a specified time (on a best effort basis). The time is given in the
txTime
attribute of the
TxFrameReq
message.
If an agent supports the ANEP-87 JANUS standard, it advertises this capability. An additional frame type (indexed parameter set) is defined:
-
JANUS
frame = 3
The JANUS capability also adds one parameter:
-
janus
— true for JANUS frame type, false for all other frame types
It also adds two JANUS-specific messages that are supported:
-
TxJanusFrameReq
⇒AGREE
/REFUSE
/FAILURE
— transmit a JANUS frame -
RxJanusFrameNtf
— sent to agent’s topic when a JANUS frame is received
If an agent advertises this capability, it supports an additional request to perform FEC decoding:
-
FecDecodeReq
⇒AGREE
/REFUSE
/FAILURE
— attempt FEC decoding a frame, and if successful, send out aRxFrameNtf
15.2. CONTROL and DATA channels
The physical layer in UnetStack typically supports 2 logical channels (3 if JANUS is supported). The CONTROL channel provides low-rate, robust communication that allows exchange of small amounts of control information in the network. The DATA channel is a usually a higher rate communication link, but may require tuning to operate well in various environmental conditions.
The configurable parameters of the CONTROL and DATA channels depend strongly on the device (modem) in use. The Unet simulator provides a simplified physical layer (
HalfDuplexModem
) that captures the essential aspects of the communication using the two channels, exposing only a limited set of parameters. When configuring a real network, you should refer to your modem’s manual on advise on how best to set up the physical layer parameters.
|
Fire up the 2-node network simulation and connect to node A’s shell. If you simply type
phy
, you can explore the physical layer parameters for the node:
> phy
« Half-duplex modem »
Generic half duplex modem simulator.
[org.arl.unet.DatagramParam]
MTU ⤇ 56
RTU ⤇ 56
[org.arl.unet.phy.PhysicalParam]
busy ⤇ false
maxPowerLevel = 0.0
minPowerLevel = -96.0
propagationSpeed ⤇ 1534.4574
refPowerLevel = 185.0
rxEnable = true
rxSensitivity = -200.0
time ⤇<PHONE_NUMBER>
timestampedTxDelay = 1.0
The
phy.MTU
and
phy.RTU
parameters tells us the maximum and recommended amount of user data that can be transmitted in a single frame (56 bytes in this case) respectively. This is based on the DATA channel, as we will see shortly, since
DatagramReq
are fulfilled using the DATA channel. The
PhysicalParam
parameters provide us information on whether the channel is busy, transmission power levels supported, receiver sensitivity, and propagation speed of the signal (e.g. speed of sound for underwater modems). The
phy.time
parameter is a microsecond resolution clock that is used to timestamp all physical layer events such as frame transmission, reception, etc.
We can dig deeper into the parameters for the CONTROL and DATA channel separately:
> phy[CONTROL]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 16
RTU ⤇ 16
[org.arl.unet.phy.PhysicalChannelParam]
dataRate = 202.10527
errorDetection ⤇ 1
fec ⤇ 0
fecList ⤇ null
frameDuration ⤇ 0.95
frameLength = 24
janus = false
llr ⤇ false
maxFrameLength = 128
powerLevel = -10.0
> phy[DATA]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 56
RTU ⤇ 56
[org.arl.unet.phy.PhysicalChannelParam]
dataRate = 731.4286
errorDetection ⤇ 1
fec ⤇ 0
fecList ⤇ null
frameDuration ⤇ 0.7
frameLength = 64
janus = false
llr ⤇ false
maxFrameLength = 512
powerLevel = -10.0
| The values you see above are specific to this simulated network, and will generally be different for different networks, depending on the devices that are being used and the environment that they are deployed in. |
Here are a few important parameters to take note of:
-
Note that
MTU
for the CONTROL channel is 16 bytes, whereas DATA channel’sMTU
is 56 bytes. CONTROL frames typically carry less data, but are more robust. -
The
frameLength
for the CONTROL and DATA channels are 8 bytes longer than the correspondingMTU
. The difference is due to header information that the frames carry. The number of bytes taken by the header is device dependent, and also a function of network configuration (e.g. changes innode.addressSize
may change header size). -
Typically physical layer agents allow setting of the
frameLength
parameter, and theMTU
parameter is automatically determined based on the necessary headers. ThemaxFrameLength