2. Getting started

The material in this chapter is also available as a two short videos: part 1 , part 2 .

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:

bucket
Figure 3. Two-node acoustic underwater network

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 .
<<< [Introduction] [UnetStack basics] >>>