parameter indicates the maximum size of the frame supported. -
The
frameDuration
for the CONTROL channel is about 0.95 seconds, whereas that for the DATA channel is 0.7 seconds. While the CONTROL frames carry less data, they also have lower data rate and so may have comparable duration as the DATA frames. -
The
dataRate
reported by the channel is the effective data rate in bps including the header bits, i.e., it is the frame length in bits divided by the frame duration. -
The
powerLevel
parameter controls the transmission power used by the channel. This value is in dB, with reference to thephy.refPowerLevel
, and may range betweenphy.minPowerLevel
andphy.maxPowerLevel
. -
The
errorDetection
parameter reports the number of bytes used for error detection CRC (value of 1 indicates that we are using a 8-bit CRC). Some modems will allow you to set this to 2 to switch to 16-bit CRC, if you desire a lower probability of accepting a frame with some bit errors.
15.3. Modem physical layer
In the previous section, we explored several parameters from a simplified simulated physical layer. Next let’s look at a real modem. If you are lucky enough to own one with UnetStack on it, you can connect to it’s shell now. Otherwise, we can use Unet audio SDOAM as our test modem:
$ bin/unet audio
Modem web: http://localhost:8080/
On the web shell for the modem:
> phy
« Physical layer »
Provides software-defined physical layer communication services (including error detection & correction).
[org.arl.unet.DatagramParam]
MTU ⤇ 31
RTU ⤇ 31
[org.arl.unet.phy.PhysicalParam]
busy ⤇ false
maxPowerLevel ⤇ 0.0
minPowerLevel ⤇ -138.0
propagationSpeed = 1500.0
refPowerLevel ⤇ 0.0
rxEnable = true
rxSensitivity ⤇ 0.0
time = 4167772
timestampedTxDelay = 1.0
[org.arl.yoda.ModemParam]
adcrate ⤇ 48000.0
bbsblk ⤇ 6000
bbscnt = 0
bpfilter = true
clockCalib = 1.0
dacrate ⤇ 96000.0
downconvRatio = 4.0
fan = false
fanctl = 45.0
fullduplex = false
gain = 0.0
hpc = false
inhibit = 120
isc = true
loopback = false
model ⤇ Unet audio
mute = true
noise ⤇ -105.6
npulses = 1
pbsblk = 65536
pbscnt = 0
post = null
poweramp = false
preamp = true
pulsedelay = 0
serial ⤇ unetaudio
standby = 15
upconvRatio ⤇ 8.0
vendor ⤇ UnetStack
voltage ⤇ 0.0
wakeupdelay = 400
wakeuplen = 8000
For brevity, we have omitted the baseband service and scheduler service parameters in the listing above. Even then, there are many parameters that allow you to configure the SDOAM. We cannot cover each parameter in detail here, but we encourage you to explore the help pages for the parameters by simply typing
help phy.
followed by the parameter name.
Further, let’s look at the indexed parameters for the CONTROL channel:
> phy[CONTROL]
« PHY »
[org.arl.unet.DatagramParam]
MTU ⤇ 13
RTU ⤇ 13
[org.arl.unet.phy.PhysicalChannelParam]
dataRate ⤇ 70.588234
errorDetection ⤇ true
fec = 1
fecList ⤇ [ICONV2]
frameDuration ⤇ 2.04
frameLength = 18
janus = false
llr = false
maxFrameLength ⤇ 796
powerLevel = -42.0
[org.arl.yoda.FhbfskParam]
chiplen = 1
fmin = 9520.0
fstep = 160.0
hops = 13
scrambler = 0
sync = true
tukey = true
[org.arl.yoda.ModemChannelParam]
basebandExtra = 0
basebandRx = false
modulation = fhbfsk
preamble = (480 samples)
test = false
threshold = 0.25
valid ⤇ true
Again, we cannot cover all the parameters in detail here, but will draw your attention to a few important ones. You see that the
modulation
for the CONTROL channel is set to
'fhbfsk'
(frequency-hopping binary frequency shift keying). Depending on your modem, different modulations may be supported. Once a modulation scheme is chosen, you see additional modulation-dependent parameters. In this case, these are the
org.arl.yoda.FhbfskParam
parameters such as
fmin
,
fstep
,
hops
,
chiplen
,
tukey
, etc. These parameters allow you to control the modulation’s frequency band, number of hops, chip duration, windowing, etc.
If you change modulation parameters, you have to remember to do it on all your modems in the network. Otherwise they will be speaking different
languages
, and they won’t be able to understand each other. Not all combination of modulation parameters are valid. The
valid
parameter tells us if the current setting is valid or not. If the setting is invalid, all transmission requests will be refused.
|
The
preamble
parameter determines a detection preamble that is transmitted before each frame. This is used by the receiving modem to determine the start of a frame. The
threshold
parameter controls the detection probability and false alarm rate for frame detection. A lower threshold will improve detection probability, but increase false alarm rate.
If the
test
flag is set on the transmission and reception modems, each transmit frame is filled with known test data. This allows the receiving modem to compute the bit error rate (BER), even when the frame has too many errors for FEC to be able to correct.
15.4. Transmitting & receiving using Unet audio
If you have two computers with speakers and microphones, you could run Unet audio on both, and communicate between the two. If you happen to have only one computer handy, do not worry — we can get one Unet audio instance to transmit and receive at the same time. This is full-duplex communication!
| Real modems typically cannot do full-duplex communication because the weak incoming signals are masked by clutter from the strong outgoing signal. However, by adjusting the volume of your computer carefully, you can easily do full-duplex communication on your Unet audio SDOAM. |
On Unet audio shell, enable full-duplex operation and try a transmission (you should be able to hear it from your computer speaker!). Your output might not look exactly the same, but let’s go over all the notifications we got and see if we can understand all of them:
> phy.fullduplex = true
true
> subscribe phy
> phy << new TxFrameReq()
AGREE
phy >> TxFrameStartNtf:INFORM[type:CONTROL txTime:79322682] (1)
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:79309353 detector:0.87] (2)
phy >> RxFrameStartNtf:INFORM[type:DATA rxTime:80659519 detector:0.26] (3)
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:79310432] (4)
phy >> RxFrameNtf:INFORM[type:CONTROL from:1 rxTime:79309353 rssi:-29.3] (5)
phy >> BadFrameNtf:INFORM[type:DATA rxTime:80659519 rssi:-38.5 (18 bytes)] (6)
| 1 | Transmission of our requested CONTROL frame has started. |
| 2 | Our frame being transmitted was detected as a CONTROL frame, and reception has started. |
| 3 | Our frame being transmitted was wrongly detected (false alarm) as a DATA frame. |
| 4 | Transmission of our frame was completed. |
| 5 | Reception of the frame was completed, and successful. |
| 6 | The wrongly detected frame resulted in data that did not satisfy CRC, and hence reported as a bad frame. |
To get rid of the false alarm on the DATA channel, we could either increase the detection threshold or turn off the detector completely (
phy[DATA].threshold = 0
). For now, we’ll do the latter. Let’s also turn on the
phy[CONTROL].test
flag so that we can measure communication performance in terms of BER. To measure BER before error correction, we also need to turn off
phy[CONTROL].fec
:
> phy[DATA].threshold = 0
0
> phy[CONTROL].test = true
true
> phy[CONTROL].fec = 0
0
Now we can make 10 transmissions, 2 seconds apart, and watch the BER of the received frames:
> 10.times { phy << new TxFrameReq(); delay(2000); }
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:204359766]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:204385187 rssi:-28.9 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:205578432]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:205603853 rssi:-28.4 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:207567766]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:207589186 rssi:-28.5 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:209583766]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:209609187 rssi:-28.2 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:211573099]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:211594519 rssi:-28.3 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:213589099]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:213614520 rssi:-28.1 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:215578432]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:215599853 rssi:-28.5 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:217594432]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:217619853 rssi:-28.2 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:219583766]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:219605186 rssi:-28.0 cfo:0.0 ber:0/144 (18 bytes)]
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:221599766]
phy >> RxFrameNtf:INFORM[type:CONTROL rxTime:221625187 rssi:-27.7 cfo:0.0 ber:0/144 (18 bytes)]
For brevity, we have omitted the
TxFrameStartNtf
and
RxFrameStartNtf
messages. We see that no bits were in error, out of 144 transmitted bits. We had perfect communication, even without FEC! This is not surprising since the speaker and microphone are very close (and hence good signal-to-noise ratio), but real channels are rarely so forgiving. You can try this between 2 computers, and things may not be as rosy.
Feel free to play around with the parameters of the modulation scheme and try transmissions to get a feel for how the parameters affect communication performance. Since your transmission and reception modems are the same, you only need to set the parameters once! In real life, you’ll need to set the same parameters on all modems in your network.
Remember to turn off the
phy[CONTROL].test
flag before trying any data transfer. While the flag is on, no user data can be carried by the transmitted frames.
|
15.5. Timed and timestamped transmissions
To explore timed and timestamped transmissions, let’s go back to our 2-node network simulation. On the shell for node A:
> phy << new CapabilityReq()
CapabilityListRsp:INFORM[TIMESTAMPED_TX,TIMED_BBREC,TIMED_BBTX,TIMED_TX]
We see that the
phy
agent supports the
TIMESTAMPED_TX
and
TIMED_TX
optional capabilities. Let us try them out. On node B:
> subscribe phy
Going back to node A, send a timestamped frame:
> phy << new TxFrameReq(timestamped: true)
AGREE
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:2489196375]
We see that the frame was transmitted at time<PHONE_NUMBER> (when you try this, the time will of course be different). You should see the
RxFrameNtf
for this frame on node B:
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:687419054]
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 rxTime:687419054 txTime:2489196375]
Note that the
RxFrameNtf
now has an additional
txTime
field that’s populated, and the timestamp in there is the same as the
txTime
on node A’s
TxFrameNtf
. The frame was timestamped before transmission, and transmitted at exactly the intended time.
Timestamps take up bits in the transmitted frame. Your effective
MTU
for frames with timestamps is 6 bytes less than the advertised
MTU
.
|
Do bear in mind that the
phy.time
clocks on node A and B may not be synchronized. So timestamps from one node cannot be directly compared with timestamps on another node. In the above example, the
rxTime
was 687,419,054 microseconds, whereas the
txTime
was 2,489,196,375 microseconds. This does not mean that the frame was received before it was transmitted! It’s just that node A and B have an offset between their clocks.
|
Sometimes you may not need to transmit a timestamped frame, but you do want the frame to be transmitted at a specified time. On node A:
> t = phy.time + 5000000; println(t); phy << new TxFrameReq(txTime: t) (1)
3174864375 (2)
AGREE
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:3174864375] (3)
| 1 |
t
is the current time + 5 seconds. We ask for a frame to be transmitted at time
t
.
|
| 2 |
The value of time
t
is printed immediately (due to the
println(t)
).
|
| 3 |
The
TxFrameNtf
message will appear after a few seconds, once the transmission is made. Note that the actual
txTime
when the transmission occurred matches with our requested value
t
.
|
If you check node B’s shell, you’ll find the corresponding
RxFrameNtf
, but it will not have a
txTime
field, as the frame transmitted was not timestamped.
| The transmission time is honored on a best effort basis, which means that there could be a small difference between the requested time and the actual transmit time. |
15.6. Snooping frames meant for other nodes
If you’re familiar with Ethernet network interface cards, you may have come across promiscuous mode . In this mode, the network card receives all packets that it hears, not just the ones that are addressed to the node. Agents providing the PHYSICAL service essentially do this continuously, but they send the notifications for frames intended for other nodes on a special sub-topic called SNOOP.
With the 2-node network simulation, let’s first only subscribe to the
phy
agent’s topic on node B:
> subscribe phy
From node A, transmit a frame to node B and to node C (node C does not exist in this network):
> phy << new TxFrameReq(to: host('B'))
AGREE
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:4622534375]
> phy << new TxFrameReq(to: host('C'))
AGREE
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:4623823375]
On node B, you’ll find that it receives the
RxFrameStartNtf
for both transmissions, but only the
RxFrameNtf
for the transmission addressed to node B:
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:2820757054]
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxTime:2820757054]
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:2822046054]
The
RxFrameStartNtf
is sent when a frame is detected. At that point in time, the agent has no idea whom the frame is intended for, because the frame contents have not yet arrived. Only when the frame is received and decoded does the agent know the destination address. Seeing that the second frame was intended for node C, node B does not report a
RxFrameNtf
for it.
If you were interested in snooping conversations between other nodes, you could subscribe to the SNOOP topic on node B:
> subscribe topic(phy, org.arl.unet.phy.Physical.SNOOP)
Now try transmitting another frame from node A to node C. On node A:
> phy << new TxFrameReq(to: host('C'))
AGREE
phy >> TxFrameNtf:INFORM[type:CONTROL txTime:4899843375]
Now you’ll see on node B that the corresponding
RxFrameNtf
is received:
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:3098066054]
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:74 rxTime:3098066054]
The
to
address of 74 corresponds to
host('C')
, but the frame is available for agents on node B through the SNOOP topic.
16. Baseband service
org.arl.unet.Services.BASEBAND
Software-defined modems often allow developers to transmit and record arbitrary waveforms. This functionality is encapsulated in the baseband service of UnetStack.
| Agents implementing the baseband 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 baseband service for transmitting arbitrary waveforms 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. |
16.1. Overview
Agents offering the baseband service are most commonly modem drivers (and simulators). They support a set of messages and parameters that are explained below. Baseband service providers may also provide optional capabilities to send or record signals at a specified time, or based on premable detection.
16.1.1. Messages
Agents supporting the baseband service provide messages for arbitrary signal transmission and recording:
-
TxBasebandSignalReq
⇒AGREE
/REFUSE
/FAILURE
— transmit a signal -
RecordBasebandSignalReq
⇒AGREE
/REFUSE
/FAILURE
— record a signal -
GetPreambleSignalReq
⇒REFUSE
/BasebandSignal
— get baseband preamble signal -
RxBasebandSignalNtf
— signal recording sent to requestor, or to agent’s topic if the recording was not specifically requested
During signal transmission, an agent implementing the baseband service sends out
TxFrameStartNtf
and
TxFrameNtf
, as described in the PHYSICAL service (
Chapter 15
). Similarly, if recording is triggered on a preamble detection, the agent sends out a
RxFrameStartNtf
as described in the PHYSICAL service.
Detection preambles are often added to transmitted signals for the receiving modem to identify the incoming signals. The receiving modem can capture the signal when it detects the preamble, and then send a
RxBasebandSignalNtf
. If a modem supports multiple preambles, the
TxBasebandSignalReq
can specify the
preamble
to be used. Setting the
preamble
to 0 results in a signal transmission without a preamble. Such a transmission will ordinarily not be received by any modem, unless it is recorded by an explicit
RecordBasebandSignalReq
at the appropriate time.
16.1.2. Parameters
Agents offering the baseband service support the following parameter:
-
carrierFrequency
— default carrier frequency for baseband signals (Hz) -
basebandRate
— default baseband sampling rate for baseband signals (samples/s) -
maxPreambleID
— maximum preamble identifier supported -
maxSignalLength
— maximum baseband signal length (in samples) for transmission/reception -
signalPowerLevel
— signal transmission power level in dB rerefPowerLevel
(refPowerLevel
is specified in the PHYSICAL service)
16.1.3. Capabilities
Agents may support several optional capabilities:
Agents advertising this capability are able to transmit signals at specified time (on a best effort basis). The time is given in the
txTime
attribute of the
TxBasebandSignalReq
message.
Agents advertising this capability are able to record signals at specified time (on a best effort basis). The time is given in the
recTime
attribute of the
RecordBasebandSignalReq
message.
16.2. Baseband and passband signals
Communication systems usually represent signals in a complex baseband representation, as this allows them to be sampled at a lower sampling rate than real passband signals. Passband signals have to be sampled at more than twice the highest frequency (Nyquist criterion). Baseband signals need to be sampled at more than twice the bandwidth. Since the bandwidth is typically much lesser than the carrier frequency, the baseband representation is usually more economical than the passband representation.
While the baseband service is aimed at signals represented in the complex baseband representation, it also supports signals represented as real passband samples. Such signals are identified by setting the
fc
(carrier frequency) field of the
TxBasebandSignalReq
or
RxBasebandSignalNtf
to 0. Modems offering the baseband service may optionally support passband signal transmission and recording.
Since Java and Groovy do not support complex numbers natively, the complex baseband signals are represented by an array of floats with alternate samples from the in-phase (real part) and quadrature (imaginary part) channels. In languages that support complex numbers (e.g. Python and Julia), the signals are represented as arrays (or lists) of complex numbers.
If all this seems to be confusing, don’t worry, it’ll become clear as we go through examples shortly.
16.3. Transmitting and recording arbitrary signals
In
Section 2.7
, you already saw how to transmit and record arbitrary signals. We then used the convenience functions
bbtx
and
bbrec
for simplicity. In this chapter, we will do the same thing by sending the
TxBasebandSignalReq
and
RecordBasebandSignalReq
messages instead. As you’ll see, these messages provide you greater control, and can also be sent via the UnetSocket API or a fjåge gateway from external applications.
Fire up Unet audio to try out the examples here:
$ bin/unet audio
Modem web: http://localhost:8080/
On the shell for the Unet audio SDOAM, check which agents provide the baseband service:
> agentsForService(org.arl.unet.Services.BASEBAND)
[phy]
We can now direct all our requests to the
phy
agent. Let’s check the baseband parameters of the
phy
agent (we omit other parameters here for brevity):
> phy
« Physical layer »
Provides software-defined physical layer communication services (including error detection & correction).
[org.arl.unet.bb.BasebandParam]
basebandRate ⤇ 12000.0
carrierFrequency = 12000.0
maxPreambleID ⤇ 4
maxSignalLength ⤇<PHONE_NUMBER>
signalPowerLevel = -42.0
We see that the Unet audio SDOAM operates at a carrier frequency of 12 kHz and a bandband sampling rate of 12 kSa/s. Let’s create a DC signal of length 12000 complex baseband samples (24000 floats) and transmit it. A DC signal of length 12000 complex samples with a carrier frequency of 12 kHz is a sinusoidal 12 kHz signal for 1 second.
> s = [1,0]*12000 (1)
[1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1 <<snip>> 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
> s.size()
24000
> phy << new TxBasebandSignalReq(signal: s)
AGREE
phy >> TxFrameNtf:INFORM[txTime:11401231]
| 1 | In Groovy, we can repeat a list using the "*" operator. The list [1,0] represents a complex number 1+0j. Repeating it 12000 times gets us a 1 second long DC signal. |
You should hear the sound from your computer speaker.
You can generate other frequency signals using the
cw()
(continuous wave) function available in the shell. You can also save and load signals from text files using the
save
and
load
commands. For information on all these commands/functions, simply type
help cw
,
help save
or
help load
.
|
Unet audio supports transmission of passband signals as well. Let us create a half second 2000 kHz passband signal and transmit it:
> s = cw(2000, 0.5, 0) (1)
> s.size()
48000 (2)
> phy << new TxBasebandSignalReq(signal: s, fc: 0) (3)
AGREE
phy >> TxFrameNtf:INFORM[txTime:414013271]
| 1 |
The
cw()
function enables us to create baseband or passband continuous wave signals. The third parameter is the carrier frequency. Setting that to 0 creates a baseband signal.
|
| 2 | Notice that our signal is 48000 floats for 0.5 seconds. Compare that with the previous DC signal, which was 24000 floats for 1 second. |
| 3 |
The
fc
field is set to 0 to tell
phy
that the
signal
is given in passband.
|
Next, let’s request a recording of 12000 samples (1 second duration):
> phy << new RecordBasebandSignalReq(recLength: 12000)
AGREE
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:11780353 rssi:-79.1 fc:12000.0 fs:12000.0 (12000 baseband samples)]
> ntf.signal (1)
[8.611694E-5, 5.8899976E-5, 1.01 <<snip>> E-6, -9.148392E-6, 3.5340495E-6]
| 1 |
The
ntf
variable holds the last received notification, which in this case is the
RxBasebandSignalNtf
. The
signal
field contains the complex baseband recording.
|
You can also ask for recordings to begin at a specified time:
> t = phy.time + 5000000; println(t); phy << new RecordBasebandSignalReq(recLength: 12000, recTime: t)
855949105
AGREE
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:855949105 rssi:-90.3 fc:12000.0 fs:12000.0 (12000 baseband samples)]
You’d have noticed the 6 second delay (5 seconds to begin recording, 1 more second to finish the recording) before the recording notification.
Interestingly, you can also request recordings in the past! Many modems have a short buffer, allowing recording in the recent past. Go too far in the past and the modem will refuse your request!
> t = phy.time - 5000000; println(t); phy << new RecordBasebandSignalReq(recLength: 12000, recTime: t)
1186471772
AGREE
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:1186471772 rssi:-74.6 fc:12000.0 fs:12000.0 (12000 baseband samples)]
> t = phy.time - 60000000; println(t); phy << new RecordBasebandSignalReq(recLength: 12000, recTime: t)
1204946438
REFUSE: Bad start time
Specifying a negative
recTime
is understood by the baseband service provider as a relative time. So we can simpify our request to record 5 seconds in the past:
> phy << new RecordBasebandSignalReq(recLength: 12000, recTime: -5000000)
AGREE
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:1361359020 rssi:-72.9 fc:12000.0 fs:12000.0 (12000 baseband samples)]
16.4. Transmitting and detecting preambles
Each logical channel (CONTROL, DATA, etc.) is associated with a detection preamble. Detectors in a modem monitor incoming signals, and trigger when the preamble is detected.
We can transmit a preamble easily:
> phy << new TxBasebandSignalReq(preamble: 1)
AGREE
phy >> TxFrameNtf:INFORM[txTime:5470777099]
Here, we did not specify a signal to transmit, so only the preamble was transmitted. You should have heard the preamble as a short chirp from your computer speaker. If we had specified a signal, the preamble would have been followed by the signal.
If you had another Unet audio SDOAM running nearby, it would have heard the preamble and detected a CONTROL frame. It would have tried to decode the frame, but failed, as we didn’t actually transmit anything after the preamble. So you’d have seen a
RxFrameStartNtf
followed by a
BadFrameNtf
if you had subscribed to
phy
topic on that modem.
If you don’t have access to another computer to run Unet audio on, we can easily demonstrate the above behavior with a single Unet audio SDOAM by simply enabling the full-duplex mode (and hence using the same computer for transmission and reception simultaneously), as we did in Chapter 15 :
> subscribe phy
> phy.fullduplex = true
true
> phy << new TxBasebandSignalReq(preamble:1)
AGREE
phy >> TxFrameStartNtf:INFORM[txTime:5809832016 txDuration:40416]
phy >> TxFrameNtf:INFORM[txTime:5809859766]
phy >> RxFrameStartNtf:INFORM[type:CONTROL rxTime:5809825603 rxDuration:2740000 detector:0.9]
phy >> BadFrameNtf:INFORM[type:CONTROL rxTime:5809825603 rssi:-55.6 (18 bytes)]
Preambles 1 and 2 are used by the CONTROL and DATA channel respectively. It’s better not to mess with these, but instead use preamble 3, which is left for the user to configure in Unet audio. By default, detection of preamble 3 is disabled. You can enable it by setting the detection threshold
phy[3].threshold
parameter. Let’s try it:
> phy[3].threshold = 0.25
0.25
> phy[3].modulation = none
none
> phy << new TxBasebandSignalReq(preamble: 3)
AGREE
phy >> TxFrameStartNtf:INFORM[txTime:6011688016 txDuration:170916]
phy >> TxFrameNtf:INFORM[txTime:6011686599]
phy >> RxFrameStartNtf:INFORM[type:#3 rxTime:6011700270 rxDuration:170500 detector:0.73]
We see the
RxFrameStartNtf
of type #3 indicating that preamble 3 was detected. Since
phy[3]
is not associated with any modulation scheme (we set the
modulation
parameter to
none
), the modem did not generate a
BadFrameNtf
as it did with preamble 1.
Preambles 1, 2 and 3 are preconfigured on the Unet audio SDOAM to be short signals with good autocorrelation properties. You can change these, if you wish, by setting the
preamble
indexed parameter (type
help phy[].preamble
for details).
|
In applications such as sonar or ranging, we may only be interested in the detecting the timing of a known signal. In that case, the
RxFrameStartNtf
is sufficient for us. But in some applications, we may wish to capture the signal once detected. That can be easily achieved in Unet audio by setting the
basebandRx
parameter.
The
basebandRx
and
basebandExtra
parameters are provided by the Unet audio SDOAM, and work closely with the baseband service. These are not currently part of the baseband service specifications, but are under consideration for adoption as part of the service. Most modems that currently support the baseband service also support these parameters.
|
If you enable
basebandRx
, a recording will be triggered every time the preamble is detected:
> phy[3].basebandRx = true
true
> phy << new TxBasebandSignalReq(preamble: 3)
AGREE
phy >> TxFrameStartNtf:INFORM[txTime:6992613349 txDuration:170916]
phy >> TxFrameNtf:INFORM[txTime:6992598599]
phy >> RxFrameStartNtf:INFORM[type:#3 rxTime:6992616269 rxDuration:170500 detector:0.78]
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:6992616269 rssi:-22.0 preamble:3 fc:12000.0 fs:12000.0 (2400 baseband samples)]
The
RxBasebandSignalNtf
notified us of the recorded signal (containing just the detected preamble). If we wanted a longer recording after the preamble, we can ask for that using the
basebandExtra
parameter, specifying the length of the recording (in samples) beyond the preamble:
> phy[3].basebandExtra = 1200 (1)
1200
> phy << new TxBasebandSignalReq(preamble: 3)
AGREE
phy >> TxFrameStartNtf:INFORM[txTime:7143093349 txDuration:170916]
phy >> TxFrameNtf:INFORM[txTime:7143062599]
phy >> RxFrameStartNtf:INFORM[type:#3 rxTime:7143081603 rxDuration:1170500 detector:0.78]
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:7143081603 rssi:-36.7 preamble:3 fc:12000.0 fs:12000.0 (3600 baseband samples)]
| 1 | We are requesting 100 ms recording beyond the end of the preamble. |
You can see that the recording is much longer now.
If you cross-correlate this recording with the preamble you transmitted, you’d get an estimate of the impulse response of the channel between your computer speaker and microphone! You can easily obtain the complex baseband representation of preamble #3 that you have been transmitting (
phy[3].preamble.signal
), if you wanted to try doing this.
|
16.5. Baseband signal monitor
During the development of signal processing algorithms, one often wants to simply record received signals in the modem for postprocessing. For this, you could write a script to listen for
RxBasebandSignalNtf
messages from
phy
agent’s topic, and store them in a file. Since this requirement is common, UnetStack already provides an agent which does exactly this. The agent is called
BasebandSignalMonitor
or
bbmon
for short.
The
bbmon
agent is already loaded when you run Unet audio. However, by default, it is disabled. It’s easy to enable it:
> bbmon.enable = true
true
Now, every
RxBasebandSignalNtf
that is sent to
phy
agent’s topic will be recorded in a
signal-0.txt
file in the
logs
folder.
> logs (1)
signals-0.txt [0 bytes] (2)
results.txt [39 bytes]
phy-log-0.txt [687 bytes]
log-0.txt [4 kB]
> phy << new TxBasebandSignalReq(preamble: 3) (3)
AGREE
phy >> TxFrameStartNtf:INFORM[txTime:102528016 txDuration:170916]
phy >> TxFrameNtf:INFORM[txTime:102513266]
phy >> RxFrameStartNtf:INFORM[type:#3 rxTime:102531520 rxDuration:270500 detector:0.74]
phy >> RxBasebandSignalNtf:INFORM[adc:1 rxTime:102531520 rssi:-23.6 preamble:3 fc:12000.0 fs:12000.0 (3246 baseband samples)]
> logs
signals-0.txt [34 kB] (4)
results.txt [39 bytes]
phy-log-0.txt [687 bytes]
log-0.txt [5 kB]
| 1 | Check the logs folder. |
| 2 |
We have a
signals-0.txt
file with no data.
|
| 3 |
Transmit preamble #3. This will trigger a recording, based on the
phy[3]
configuration from the previous section.
|
| 4 |
Now the
signals-0.txt
file has grown to 34 kB. It contains the signal that was just recorded.
|
As you record more signals, they are appended to the same file (with delineating metadata for each signal). If you restart Unet audio, this file will be renumbered to
signals-1.txt
, as the logs are rotated.
The name of the signals file and number of files kept through log rotation is configured when the
bbmon
agent is loaded. This happens in the
etc/setup.groovy
file in your Unet audio installation, and you can change it, if you like.
|
The signals file stores the signals in a base64 encoded format. The Python package
arlpy.unet
allows you to read this file and work with the signals in it:
$ pip install arlpy (1)
$ ipython
Python 3.6.8 |Anaconda custom (64-bit)| (default, Dec 29 2018, 19:04:46)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.2.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from arlpy import unet
In [2]: s = unet.get_signals('logs/signals-0.txt')
In [3]: s (2)
Out[3]:
time rxtime adc channels fc ... len preamble rssi filename lno
0<PHONE_NUMBER>848 75224853 1 1 0 ... 3246 3 -23.6 logs/signals-0.txt 1
[1 rows x 12 columns]
In [4]: x = unet.get_signal(s, 0)
In [5]: x.shape (3)
Out[5]: (3246,)
In [6]: x
Out[6]:
array([ 9.50995535e-02-3.77136953e-02j, 1.30487725e-01-1.90211199e-02j,
1.27376720e-01+1.91459619e-02j, ...,
8.61445224e-05+2.21590763e-05j, -2.69901575e-05-3.34111392e-05j,
3.39479702e-05-1.82653162e-06j])
| 1 |
Install the
arlpy
pacakge. You need to do this only if you don’t already have it installed. The output of this command is omitted here.
|
| 2 |
s
is now a pandas table with an index of all signals available in
signals-0.txt
.
|
| 3 |
x
is now signal #0 (first signal) from the
signals-0.txt
file.
|
16.6. Transmitting and receiving waveforms directly from Python
In the previous section, we showed you how to record signals for postprocessing. This is great if you postprocessing is what you desire, but sometimes it is important to access the functionality in real time from Python. This is very useful while debugging new signal processing algorithms, since tools such as Jupyter notebooks and libraries such as
numpy
,
scipy
,
pandas
,
arlpy
, and many others have made Python the preferred platform for a lot of scientific computation.
Let’s try it!
You had already installed the Python package
unetpy
in
Section 2.5
. We’ll be using it now, so in case you don’t have it installed, now is a good time to install it. Start a Jupyter new notebook with Python 3 and connect to your Unet audio instance:
In[1]: from unetpy import *
import arlpy.plot as plt
In[2]: # connect to the Unet audio SDOAM
sock = UnetSocket('localhost', 1100)
gw = sock.getGateway()
In[3]: # lookup the agent providing baseband service
bb = gw.agentForService(Services.BASEBAND)
bb.name
Out[3]: 'phy'
In[4]: # transmit preamble 3 -- you should be able to hear it
bb << TxBasebandSignalReq(preamble=3)
Out[4]: AGREE
In[5]: # discard old notifications to get ready for a recording
gw.flush()
In[6]: # request a recording
bb << RecordBasebandSignalReq()
Out[6]: AGREE
In[7]: # obtain the recording notification and check that it's of the correct type
ntf = gw.receive(timeout=5000)
ntf
Out[7]: RxBasebandSignalNtf:INFORM[rxTime:203329687 rssi:-68.847565 adc:1 fc:12000.0 fs:12000.0 channels:1 preamble:0 (65536 samples)]
In[8]: # close the connection
sock.close()
In[9]: len(ntf.signal)
Out[9]: 65536
In[10]: # plot the first 10000 baseband samples (real/in-phase components only)
plt.plot(ntf.signal[:10000].real, fs=ntf.fs)
Out[10]:
Of course you could do the same thing with Julia or other languages, if you wish, with obvious minor changes to the syntax!
17. Ranging and synchronization
org.arl.unet.Services.RANGING
It is common to use underwater acoustic modems for range estimation, as the travel time of acoustic signals can easily be measured. In
Section 2.3
, we saw that we can use the
range
command to estimate range between nodes. This command uses the RANGING service described below.
Ranging is closely related to time synchronization, since travel time measurement between two nodes requires some sort of synchronization between the nodes. If the nodes are synchronized, one-way travel time (OWTT) can be directly measured and used to estimate range. If the nodes are not synchronized, two-way travel time (TWTT) can be used to measure range and synchronize the nodes simultaneously. The RANGING service supports both modes of ranging, and manages synchronization information between nodes.
The RANGING service also supports ranging to commercial-off-the-shelf (COTS) transponders.
17.1. Overview
The RANGING service provides messages and parameters to support OWTT and TWTT ranging to other Unet nodes or COTS transponders, and to manage synchronization information between the nodes.
17.1.1. Messages
The following messages are defined by the RANGING service:
-
RangeReq
⇒AGREE
/REFUSE
/FAILURE
— measure range to peer node via OWTT or TWTT -
BeaconReq
⇒AGREE
/REFUSE
/FAILURE
— transmit beacon message for OWTT -
RangeNtf
— sent when a range to another node is measured, typically in response toRangeReq
on current node (sent to requester), orBeaconReq
on peer node (sent to agent’s topic) -
InterrogationNtf
— sent on agent’s topic when a ranging interrogration frame is recevied from peer, before a response is sent back -
RespondReq
⇒AGREE
/REFUSE
— if automatic response is disabled, request to respond to interrogation frame (with optional payload)
The use of these messages will become clearer through examples below.
17.1.2. Parameters
The RANGING service does not define any parameters, but the default RANGING agent provides several useful parameters to control its behavior, and to provide status information.
17.2. Ranging agent
17.2.1. Parameters
A few parameters control the behavior of the default ranging agent:
In addition, the following parameters provide defaults for ranging:
-
channel
— channel (DATA/CONTROL) to use for ranging -
maxRange
— maximum expected range to peer (m) -
ipreamble
— interrogation preamble to transmit -
rpreamble
— response preamble to expect after interrogation -
rsignal
— response baseband signal to expect after interrogation -
rdelay
— response delay (seconds) -
threshold
— threshold (0-1) for signal detection, when response is specified as an arbitrary acoustic signal
17.2.2. Indexed parameters
Some parameters can be additionally specified (or overridden) on a per-link basis, using indexed parameters (indexed by the node address of the peer node on the link):
-
address
— peer node address -
channel
— channel (DATA/CONTROL) to use for ranging -
maxRange
— maximum expected range to peer (m) -
ipreamble
— interrogation preamble to transmit -
isignal
— interrogation baseband signal to transmit -
rpreamble
— response preamble to expect after interrogation -
rsignal
— response baseband signal to expect after interrogation -
rdelay
— response delay (seconds) -
data
— payload data to automatically include in response frame -
threshold
— threshold (0-1) for signal detection, when response is specified as an arbitrary acoustic signal -
lifetime
— synchronization validity lifetime (seconds)
The lifetime of syncrhonization information should be set based on the expected drift of modem clocks. Lifetime is defined as the time for the expected clock drift (scaled by speed of sound in water) to exceed the required range estimation accuracy. If a network uses modems with low-drift clocks (such as oven-controlled oscillators), the lifetime can be quite long (hours to days). Without low-drift clocks, reasonable lifetimes may only be in the order of several minutes to tens of minutes.
A few more read-only per-link parameters provide synchronization information about the link, when available:
17.3. Examples
In order to understand how the RANGING service provides OWTT and TWTT ranging, it is instructive to try a few examples using the Netiquette 3-node network simulation (
bin/unet samples/netq-network.groovy
). Start the simulation, and connect to node A:
> agentsForService(org.arl.unet.Services.RANGING) (1)
[ranging]
> ranging
« Range estimator »
Provides time synchronization and range estimation services.
[org.arl.unet.localization.RangingParam]
channel = 2 (2)
ipreamble = 0
mac = mac
maxRange = 3000.0
phy = phy
rdelay = 1.95
respond = true
rpreamble = 0
rsignal = []
threshold = 0.3
> range host('B') (3)
371.09
> ranging << new RangeReq(to: host('B')) (4)
AGREE
ranging >> RangeNtf:INFORM[from:232 to:31 range:371.08856 offset:-1523892589 rxTime:1860655032]
> ntf.range
371.09
| 1 |
We see that the
ranging
agent provides the RANGING service on the node.
|
| 2 | The DATA channel (channel 2) is being used for ranging. |
| 3 |
The
range
command provides us the range to node B of about 371 m.
|
| 4 |
The
range
command is implemented by sending a
RangeReq
to the
ranging
agent. We directly send that message. As expected, it leads to a
RangeNtf
message that gives us the same range estimate as the
range
command. The
RangeNtf
also provides us time synchronization information between the nodes (as time difference between the nodes, or
offset
, in microseconds).
|
17.3.1. Two-way travel time ranging
The range measurement above used TWTT ranging. While node B participated in the range measurement by responding to node A’s request for a two-way frame exchange, this is all done quietly and we see nothing on node B’s shell. To see what is happening on both nodes, subscribe to the
phy
and
ranging
agent’s topics on both nodes. Then repeat the
RangeReq
on node A:
> subscribe phy
> subscribe ranging
> ranging << new RangeReq(to: host('B'))
AGREE
phy >> TxFrameStartNtf:INFORM[type:DATA txTime:2048652195 txDuration:700]
phy >> RxFrameStartNtf:INFORM[type:DATA rxTime:2051086032]
phy >> RxFrameNtf:INFORM[type:DATA from:31 to:232 protocol:1 rxTime:2051086032 txTime:526951606 (7 bytes)]
ranging >> RangeNtf:INFORM[from:232 to:31 range:371.08856 offset:-1523892589 rxTime:2051086032]
We see that node A transmitted a DATA frame. It then received a timestamped DATA frame back from node B. The timing information in both frames was used to compute the range and time offset between the nodes. This was sent back to us as a
RangeNtf
. This is the frame exchange that implements TWTT ranging.
If we look at node B’s shell at the same time:
> subscribe phy
> subscribe ranging
phy >> RxFrameStartNtf:INFORM[type:DATA rxTime:525001443]
phy >> RxFrameNtf:INFORM[type:DATA from:232 to:31 protocol:1 rxTime:525001443 (1 byte)]
ranging >> InterrogationNtf:INFORM[type:DATA from:232 to:31 rxTime:525001443 responded:true]
phy >> TxFrameStartNtf:INFORM[type:DATA txTime:526951606 txDuration:700]
We see that node B received a DATA frame and responded back with a DATA frame. It generated a
InterrogationNtf
with
responded
set to
true
to indicate that it received an interrogation and responded to it. This is because
ranging.respond = true
, as seen above. If
ranging.respond
was set to
false
, the agent would have simply generated the
InterrogationNtf
without responding. This allows another agent to study the intorrogation, and optionally send back payload data as part of the response, using the
RespondReq
message.
17.3.2. Synchronization
We can ask node A for the synchronization information it has gathered from previous ranging exchanges:
> ranging[host('B')]
« RANGING »
[org.arl.unet.localization.RangingNodeParam]
address ⤇ 31
channel = 2
data = []
ipreamble = 0
isignal = []
lastSync ⤇<PHONE_NUMBER>974
lifetime = 0
maxRange = 3000.0
offset = -1523892589
rdelay = 1.95
rpreamble = 0
rsignal = []
sync ⤇ false
threshold = 0.3
We see that it has stored the time offset to node B, along with the information on when the synchronization information was last updated. However, you’ll find that the
sync
flag is
false
, since the
lifetime
parameter was set to 0, and hence the synchronization is considered expired. If you ask for synchronization information on node B, you’ll find that it does not have any:
> ranging[host('A')]
« RANGING »
[org.arl.unet.localization.RangingNodeParam]
address ⤇ 232
channel = 2
data = []
ipreamble = 0
isignal = []
lastSync ⤇ 0
lifetime = 0
maxRange = 3000.0
offset = 0
rdelay = 1.95
rpreamble = 0
rsignal = []
sync ⤇ false
threshold = 0.3
Without synchronization information, OWTT ranging cannot be performed.
If we have low-drift clocks on all our nodes, we can set the
lifetime
parameter for all the links to a larger value. Let’s do that on node A for link A-B. Also unsubscribe from
phy
to avoid too much clutter:
> ranging[host('B')].lifetime = 3600
3600
> unsubscribe phy
Now check the synchronization information for link A-B again:
> ranging[host('B')]
« RANGING »
[org.arl.unet.localization.RangingNodeParam]
address ⤇ 31
channel = 2
data = []
ipreamble = 0
isignal = []
lastSync ⤇<PHONE_NUMBER>974
lifetime = 0
maxRange = 3000.0
offset = -1523892589
rdelay = 1.95
rpreamble = 0
rsignal = []
sync ⤇ true (1)
threshold = 0.3
| 1 |
We see that
sync
is now true, indicating that we have valid synchronization information on this link.
|
17.3.3. One-way travel time ranging
Now, let’s transmit a ranging beacon from node B:
> beacon // equivalent to: ranging << new BeaconReq()
AGREE
On node A, we see
RangeNtf
from the OWTT ranging:
ranging >> RangeNtf:INFORM[from:232 to:31 range:371.08856 rxTime:1039174911]
Any timestamped frame transmission from node B will generate
RangeNtf
on node A now. This can be used to piggyback data (e.g. 42) along with the beacon:
phy << new TxFrameReq(timestamped: true, data: [42])
. This will generate a
RxFrameNtf
on node A, if you subscribe to
phy
, in addition to the
RangeNtf
messages. This works with both CONTROL and DATA frames.
|
17.3.4. Ranging to COTS transponders
The ranging agent provides a lot of flexibility for configuration. To see the power of this, let’s consider a scenario in which we want to use a UnetStack-based modem to measure range to a COTS transponder that is configured to respond to a 22 kHz 2 ms long pulse with a 30 kHz 2 ms pulse, after a delay of 30 ms.
| The example below is meant to run on a UnetStack-based modem. If you don’t have one, you can still try out the example on Unet audio and hear the interrogation pulse. However, because the default operating band of Unet audio SDOAM is 6-18 kHz, you’ll need to pick interrogation and response frequencies in this band. Also, you may want to make the interrogation pulse longer (say 200 ms), so you can hear it when you try ranging with this in air. |
First, we welcome the COTS transponder into our network as a Unet
guest
node by assigning it a name and address. Let’s call it node T with address
host('T') = 152
. On our modem node, we set up details of the link to the transponder:
> T = host('T')
152
> ranging[T].isignal = cw(22.kHz, 2.ms);
> ranging[T].rsignal = cw(30.kHz, 2.ms);
> ranging[T].rdelay = 30.ms;
> ranging
« RANGING »
[org.arl.unet.localization.RangingNodeParam]
address ⤇ 152
channel = 2
data = []
ipreamble = 0
isignal = [0.0, -0.0, 0.8660254, -0.5, 0.5 ... 0.5, 0.8660254, 0.8660254, 0.5]
lastSync ⤇ 0
lifetime = 0
maxRange = 3000.0
offset = 0
rdelay = 0.03
rpreamble = 0
rsignal = [0.0, 0.0, 6.123234E-17, 1.0, -1 ... 6022E-15, -3.1847007E-15, -1.0]
sync ⤇ false
threshold = 0.3
Now, we are all setup. To range to the transponder, all we need to do is:
> range T
235.7
Assuming you have the UnetStack-based modem in the water, along with the COTS transponder, you’ll get a range estimate back after just a short delay.
We have hardly scratched the surface of what the RANGING service and the ranging agent is capable of. There’s a lot you can do with it!
| For an example of how to build a simple long-baseline (LBL) navigation system with multiple nodes connected over wormholes amd ranging to a mobile node, see Chapter 11 . |
18. Node information
org.arl.unet.Services.NODE_INFO
The NODE_INFO service provides a single place to collate node-related information that is commonly needed by many agents. It is a special service, in the sense that each node must be configured to have one and only one agent providing this service.
18.1. Overview
An agent implementing the NODE_INFO service not only exposes a set of parameters, as described in this section, but also provides some special handling for specific parameters.
18.1.1. Parameters
An agent offering the NODE_INFO service supports several parameters:
-
nodeName
— node name -
address
— node address -
addressSize
— address size in bits (valid values are 8 or 16) -
time
— node time (read-only) -
canForward
— true if the node will forward datagrams to other nodes (routing) -
origin
— origin as (latitude, longitude) -
location
— location as (x, y, z) in meters if origin set, otherwise (latitude, longitude, z) -
mobility
— true if the node is mobile, false if it is fixed -
speed
— speed in m/s, if mobile node -
heading
— heading in degrees, 0 is North, measured clockwise -
pitch
— pitch angle in degrees, 0 is level, positive is nose down -
roll
— roll angle in degrees, 0 is level, positive is rolling starboard -
turnRate
— turn rate in degrees/s, measured clockwise, if mobile node -
diveRate
— dive rate in m/s, if mobile node
Any changes to parameters
nodeName
,
address
,
addressSize
,
origin
or
location
are published as
ParamChangeNtf
to the agent’s topic (in addition to the
PARAMCHANGE
topic that all parameter changes are automatically published to — see
Chapter 25
). This is to facilitate monitoring of changes to these important parameters by other agents, simply by subscribing to the NODE_INFO service provider’s topic.
Additionally, if time stability or location accuracy information is available, the following parameters are populated:
-
timeStability
— time stability in ppm -
locationAccuracy
— location accuracy as (x, y, z) in meters
18.1.2. Notes
-
See Section 4.1 for a discussion on
nodeName
,address
andaddressSize
. -
See Section 5.6 for a discussion on
origin
,location
and coordinate systems. -
If node
mobility
is enabled, the agent may automatically updatelocation
based on motion parameters such asspeed
,heading
, etc.
18.2. Example
If you start the mission2013 network simulation (
bin/unet samples/mission2013-network.groovy
), connect to node 21’s shell and type
node
, you’ll see the NODE_INFO parameters for the node in this network:
> node
« Node information »
Manages and maintains node information and attributes.
[org.arl.unet.nodeinfo.NodeInfoParam]
address = 232
addressSize = 8
canForward = true
diveRate = 0
heading = 0
location = [0.0, 0.0, -15.0]
mobility = false
nodeName = A
origin = [NaN, NaN]
pitch = 0
roll = 0
speed = 0
time ⤇ Thu Apr 08 20:43:40 SGT 2021
turnRate = 0
19. Address resolution
org.arl.unet.Services.ADDRESS_RESOLUTION
19.1. Overview
An ADDRESS_RESOLUTION service provider is responsible for address allocation and resolution. The size of the address space is detemined by the
addressSize
parameter of the NODE_INFO service (
Chapter 18
).
19.1.1. Messages
Agents supporting this service honor the following requests:
-
AddressAllocReq
⇒AddressAllocRsp
/REFUSE
/FAILURE
— request for allocation of address to node -
AddressResolutionReq
⇒AddressResolutionRsp
/REFUSE
/FAILURE
— resolve node name to address
19.2. Usage and notes
Shell usage of this service via the
host
command is described in
Section 4.1
. In this section, we show examples of how address allocation and address resolution can be implemented directly by sending messages.
Address allocation is typically required at startup, and usually initiated by the agent providing the NODE_INFO service to populate the
address
parameter of that service. To ask the ADDRESS_RESOLUTION service provider to allocate an address, an agent sends it a
AddressAllocReq
. The allocation may depend on the node name, and so the request must contain the node name.
We can start the 2-node network, and manually test this on the shell of node A:
> a = agentForService(org.arl.unet.Services.ADDRESS_RESOLUTION);
> a << new AddressAllocReq(name: 'A')
AddressAllocRsp:INFORM[address:232]
Address resolution is performed via the
AddressResolutionReq
message. The
host
command sends this message on your behalf, and shows you the response:
> a << new AddressResolutionReq(name: 'A')
AddressResolutionRsp:INFORM[address:232 name:A]
> ans.address
232
> host('A')
232
While the address allocation and resolution processes may seem very similar, there is a conceptual difference between the two. Address allocation is performed for a new node without an address. The address allocation process associates it with an address. Address resolution is performed to find the address that was assigned to a node.
| The default ADDRESS_RESOLUTION service provider uses a hashing function to map node names to addresses. This enables it to allocate and resolve addresses without generating network traffic, as the hashing function generates the same address for a given name on each node. Because the hashing maps a large name space to a small address space, there is always the chance that two names map to the same address; we require that the network architect check this manually, and assign node names such that there are no address conflicts. |
| Network designers and protocol developers should not rely on the ADDRESS_RESOLUTION service provider being based on a hashing function for correct operation of the network. |
20. Medium access control
org.arl.unet.Services.MAC
20.1. Overview
Agents offering the medium access control (MAC) service advise other agents on when they may be permitted to make transmissions, in an effort to reduce collisions and improve network throughput.
MAC protocols that use PDUs for channel reservation may support piggybacking of client data in the PDU. If such support is available, it is advertised using a non-zero
reservationPayloadSize
parameter. A
ReservationReq
should provide the payload data to be sent to a peer node (as part of RTS or equivalent PDU) to whom the reservation is made. If that node wishes to send payload data back (as part of CTS or equivalent PDU), it may send a
ReservationAcceptReq
in response to a
ReservationStatusNtf
to provide its payload data.
| Protocol data units (PDUs) are protocol-specific datagrams exchanged by nodes in a network. MAC protocls that use a handshake often use three basic type of PDUs — request to send (RTS), clear to send (CTS) and acknowledgement (ACK). |
20.1.1. Messages
Agents supporting the MAC service provide messages to request, grant and cancel reservations:
-
ReservationReq
⇒ReservationRsp
/REFUSE
— request a reservation -
ReservationCancelReq
⇒AGREE
/REFUSE
— cancel a reservation -
ReservationAcceptReq
⇒AGREE
/REFUSE
— request piggybacking of payload in a reservation PDU, typically sent by a client on receiving aReservationStatusNtf[status: REQUEST]
notification -
TxAckReq
⇒AGREE
/REFUSE
— request transmission of acknowledgement payload -
ReservationStatusNtf
— sent to requestor or agent’s topic when a reservation-related events occur
While we use the term reservation for all MAC agents, it is important to understand that MAC agents are typically unable to guarantee that no other nodes transmit during the reservation period. The reservation is on a best-effort basis, and the probability of collision is dependent on the underlying MAC protocol in use. For example, a handshake-based MAC (using RTS/CTS) might be able to inform other nodes' MAC agents about a reservation, while carrier-sensing MAC agents may rely on hearing the transmission from another node to backoff reservations at their node. A MAC agent must, however, guarantee that no two agents on the same node are granted reservation at the same time.
Agents providing the MAC service are able to queue reservation requests from multiple agents, and grant reservations in order of arrival of request (or priority, or some other measure of fairness). It is, however, recommended that one agent only make one request to the MAC at a time, and submit its next request after the previous reservation is completed. An agent providing the MAC service may choose to refuse reservation requests from an agent that has pending reservation requests already in the queue.
20.1.2. Parameters
Agents offering the MAC service support the following parameters:
-
channelBusy
— true if channel is busy, false otherwise -
reservationPayloadSize
— maximum size of payload (bytes) that can be piggybacked in a reservation PDU -
ackPayloadSize
— maximum size of acknowledgement (bytes) that can be included in an ACK PDU -
maxReservationDuration
— maximum duration of reservation in seconds -
recommendedReservationDuration
— recommended duration of reservation in seconds (null, if unspecified)
A MAC agent advertises the maximum supported reservation duration (
maxReservationDuration
), and must honor reservation requests for up to this duration. However, the network performance might suffer if all agents use very long reservations. To address this, the MAC agent also advertises a shorter recommendation for reservation duration (
recommendedReservationDuration
). Most agents should request reservations of this duration (or shorter) unless unavoidable.
20.1.3. Capabilities
Agents may support several optional capabilities:
An agent advertising this capability must be able to send acknowlegements as part of the MAC protocol. The agent must support the
TxAckReq
request to provide acknowledgement payload to be transmitted to the peer node at the end of the reservation. On reception, this payload should be delivered in the
ReservationStatusNtf[status: END]
on the peer node.
Agents advertising this capability must honor priority settings in the reservation request.
Agents advertising this capability must honor time-to-live settings in the reservation request.
Agents advertising this capability must support scheduling of reservations in the future, through the use of the
startTime
attribute of the
ReservationReq
.
20.2. Basic MAC functionality
MAC agents advise other agents that wish to transmit (we shall call them clients ) on when they may do so. MAC agents make channel reservations on behalf of their clients, as necessary. Some MAC protocols such as Aloha and TDMA may not require explicit handshake for reservation, while others such as MACA and FAMA may involve control packet exchanges between peer MAC agents on various nodes. In either case, a typical client with data to transmit starts by asking the prevailing MAC agent for a channel reservation:
// client wishes to transmit data to "destination" for specified "duration"
def mac = agentForService(Services.MAC)
if (mac) {
def req = new ReservationReq(recipient: mac, to: destination, duration: duration) (1)
def rsp = request(req)
if (rsp && rsp.performative == Performative.AGREE) {
def ntf = receive(ReservationStatusNtf, timeout) (2)
if (ntf && ntf.inReplyTo == req.messageID && ntf.status == ReservationStatus.START) {
// :
// transmit data for requested duration
// :
}
}
}
| 1 | Send a channel reservation request. |
| 2 | Wait for a channel reservation notification. |
In the above sample code, error handling has been omitted for simplicity. In reality, you would want to have else clauses to handle reservation failures. The MAC agent not only sends a
ReservationStatusNtf[status: START]
notification, but also a
ReservationStatusNtf[status: END]
notification at the end of the reservation duration. The sample code above ignores this notification, but a well-behaved client should ensure that the transmission does not exceed the requested duration.
20.3. Working with MAC payloads
Messages such as
ReservationReq
and
ReservationStatusNtf
may carry payloads, when the MAC protocol supports them. When payloads are supported, additional messages such as
ReservationAcceptReq
,
TxAckReq
and
TxAckNtf
are available for clients to provide payloads to the MAC service provider to piggyback on the MAC PDUs. A typical exchange is illustrated in
Figure 9
.
For a MAC reservation initiated by node A with node B, we elaborate on the steps for a full reservation lifecycle with payloads:
-
On node A, the client (agent) sends a
ReservationReq
to the MAC (agent), with an optional payload. The MAC accepts the request. -
MAC on node A sends an RTS PDU with the payload to the MAC on node B.
-
MAC on node B generates a
ReservationStatusNtf[status: REQUEST]
message and publishes it on its topic. A client subscribing to this topic receives the notification. -
If the client on node B wants to send back some payload with the CTS PDU, it immediately sends a
ReservationAcceptReq
to the MAC, with the payload. -
The MAC accepts the request and responds to node A’s MAC with a clients PDU containing the payload.
-
The payload is delivered to the client on node A as part of a
ReservationStatusNtf[status: START]
message marking the start of the reservation time. -
During the reservation, the two nodes exchange data as they wish.
-
If the client on node B wishes to provide an acknowledgment (with a payload), it sends a
TxAckReq
message before the reservation duration ends, and the MAC on node B accepts. -
The MAC on node B sends an ACK PDU with the payload to the MAC on node A. The ACK PDU marks the end of the channel reservation. The MAC delivers this acknowledgment payload to the client on node A as a part of the
ReservationStatusNtf[status: END]
message. -
If node B does not send an ACK PDU, when the channel reservation ends, the MAC on node A sends a
ReservationStatusNtf[status: END]
message to its client.
20.4. Examples
Sample MAC implementations are illustrated in Chapter 29 .
21. Single-hop links
org.arl.unet.Services.LINK
21.1. Overview
Agents offering the LINK service provide single-hop communication.
Single-hop here refers to a single hop in the Unet sense. For example, a link may be provided over wireless RF network that has multiple physical hops (e.g. using UDP/IP). However, as long as the link does not pass through multiple Unet nodes, it is considered a logically single-hop link.
All agents supporting the LINK service must also support the DATAGRAM service ( Chapter 14 ).
It is recommended that agents offering the LINK service provide reliability, when requested. Agents that are able to provide reliability, do so by advertising the DATAGRAM service capability RELIABILITY.
LINK service providers using the PHYSICAL service should also consult the MAC service to determine when they should transmit.
In a typical Unet, gateway nodes may have several LINK service providers, with the ROUTING service ( Section 22.2 ) provider forwarding datagrams across links.
21.1.1. Messages
The LINK service defines only one optional message, used only by agents advertising the LINK_STATUS capability.
-
LinkStatusNtf
— link up/down or quality notification
21.1.2. Parameters
Agents offering the LINK service may support the following parameter:
-
dataRate
— nominal data rate of link in bps (0 if unknown)
21.1.3. Capabilities
Agents may support several optional capability:
Agents advertising this capability will send
LinkStatusNtf
messages on its topic, when a link becomes active, is lost, or has significant change in quality.
21.2. ReliableLink
Start the 2-node network and connect to node A:
> agentsForService org.arl.unet.Services.LINK
[uwlink]
> uwlink
« Reliable acoustic link »
Link protocol with support for reliability and fragementation-reassembly.
[org.arl.unet.DatagramParam]
MTU ⤇ 848
RTU ⤇ 848
[org.arl.unet.link.LinkParam]
dataRate ⤇ 731.4286
[org.arl.unet.link.ReliableLinkParam]
acks = 2
controlChannel = 1
dataChannel = 2
mac = mac
maxPropagationDelay = 2.5
maxRetries = 2
phy = phy
reservationGuardTime = 0.5
> uwlink << new CapabilityReq()
CapabilityListRsp:INFORM[FRAGMENTATION,RELIABILITY,LINK_STATUS]
We see that in the 2-node network simulation, the only agent that provides the link service is the
uwlink
agent of type
ReliableLink
. This agent fragments large datagrams, and transmits a batch of frames at a time, before waiting for an acknowledgement from the peer node. Unacknowledged frames are retransmitted until all frames are delivered, or there are too many retries. Once all frames are received, the peer node’s
uwlink
agent reassembles the datagram and delivers it. The agent also provides link status notifications.
ReliableLink
provides the LINK service for all underwater links in Unet basic stack that ships as part of the community edition.
|
With the default PHYSICAL service settings, the nominal data rate provided by this link is 731 bps, and that only 848 or less bytes may be transferred per datagram. The actual data rate may differ, depending on the size of the datagram, reliability settings, and the channel conditions.
We also see that
ReliableLink
provides a set of configurable parameters:
-
acks
-
Number of acknowledgements to use for reliable data transfer. Since a lost acknowledgement frame results in retransmission of the entire batch of frames, multiple acknowledgements are used to improve the robustness of the acknowledgement frame.
-
controlChannel
-
Frame type to use for control information. This is usually the CONTROL frame type.
-
dataChannel
-
Frame type to use for data frames. This is usually the DATA frame type.
-
mac
-
MAC service provider to use for reserving the channel. This is automatically discovered during startup. Setting this to
null
disables MAC reservation. -
maxPropagationDelay
-
Maximum expected propagation delay for the link (in seconds). This should be set based on the expected single-hop communication range, and the sound speed (in case of acoustic links). This parameter is used to determine expected round-trip time for setting timeouts for acknowledgement frames.
-
maxRetries
-
Maximum number of retries per frame. Once the maximum number of retries is exceeded, a datagram transfer is deemed to have failed.
-
phy
-
PHYSICAL service provider for data transmission. This is automatically discovered during startup, but may be configured manually on gateway nodes with multiple PHYSICAL service providers.
-
reservationGuardTime
-
Guard time (in seconds) that is included in a MAC reservation of the channel. The guard time allows for small timing variability in transmission and small delays in response from peer node.
21.3. ECLink
In the Unet premium stack,
ECLink
replaces
ReliableLink
as the default LINK service provider for underwater links.
|
ECLink
uses an erasure correction code (type of error correction code that deals with lost frames) to reduce the protocol overhead required for retransmissions in a lossy channel. This usuaully results in significantly better performance than
ReliableLink
in poor channel conditions, and when transferring large datagrams.
If you have a modem with the commercial version of UnetStack3, it’ll have
ECLink
loaded as the default LINK service provider:
> uwlink
« Erasure coded link »
Link protocol based on erasure coding, for fast large data transfers over a single hop.
[org.arl.unet.DatagramParam]
MTU ⤇ 3145584
RTU ⤇ 1450
[org.arl.unet.link.ECLinkParam]
compress = true
controlChannel = 1
dataChannel = 2
mac = mac
maxBatchSize = 65533
maxPropagationDelay = 3.0
maxRetries = 2
minBatchSize = 3
phy = phy
reliability = false
reliableExtra = 0.2
unreliableExtra = 0.3
[org.arl.unet.link.LinkParam]
dataRate ⤇ 731.4286
> uwlink << new CapabilityReq()
CapabilityListRsp:INFORM[COMPRESSION,RELIABILITY,CANCELLATION,FRAGMENTATION,LINK_STATUS]
We see that the
MTU
for
ECLink
is quite large (as compared to
ReliableLink
), as
ECLink
can efficiently transfer large amounts of data. While the
dataRate
parameter advertises a similar nominal rate as with
ReliableLink
, you’ll find that
ECLink
yields better practical performance when transferring large files, and in poor channel conditions.
ECLink
also supports data compression, and link status notifications.
The
phy
,
controlChannel
,
dataChannel
,
mac
,
maxRetries
, and
maxPropagationDelay
parameters of
ECLink
are similar to the ones in
ReliableLink
. However,
ECLink
has several additional parameters that control performance:
-
minBatchSize
-
Minimum number of frames to send in each batch.
-
maxBatchSize
-
Maximum number of frames to send in each batch.
-
reliability
-
Default reliability for a datagram transfer, if a
DatagramReq
does not specify reliability (null
). -
reliableExtra
-
Fraction of extra frames to transmit for erasure correction, during reliable datagram transfer (using acknowledgements to determine retries). A value of 0.2 indicates 20% extra frames are transmitted. This allows for 20% frame loss without the need for retries.
-
unreliableExtra
-
Fraction of extra frames to transmit for erasure correction, during unreliable datagram transfer (no acknowledgement or retries). A value of 0.3 indicates 30% extra frames are transmitted. This allows for successful datagram transfer with as much as 30% frame loss.
-
compress
-
Enable/disable data compression.
22. Routing and route maintenance
22.1. Overview
The ROUTING and ROUTE_MAINTENANCE services work closely to provide multi-hop communications. The ROUTING serice provider is responsible for maintaining a routing table, and for routing datagrams according to the table. The ROUTE_MAINTENANCE service provider, on the other hand, is responsible for discovering new routes and providing updated routing information to the ROUTING service provider.
If a network only requires static routes, the ROUTING service provider is sufficient to provide the functionality. In case of networks with dynamic route discovery, a ROUTE_MAINTENANCE service provider discovers routes (or route changes) and publishes
RouteDiscoveryNtf
messages. The ROUTING service provider subscribes to these messages and updates its routing tables.
The ROUTING service provider also typically listens for
LinkStatusNtf
messages from the LINK service agents. These messages allow it to keep track of link availability and link quality.
Both ROUTING and ROUTE_MAINTENANCE services are described below.
22.2. Routing service
org.arl.unet.Services.ROUTING
Agents offering the ROUTING service provide multi-hop communication.
All agents supporting the ROUTING service must also support the DATAGRAM service ( Chapter 14 ).
It is recommended that agents offering the ROUTING service provide reliability, when requested. Agents that are able to provide reliability, do so by advertising the DATAGRAM service capability RELIABILITY.
22.2.1. Messages
Agents providing the ROUTING service support the following messages:
-
GetRouteReq
⇒RouteRsp
/AGREE
/REFUSE
— find route or a list of routes -
EditRouteReq
⇒AGREE
/REFUSE
— add, delete or change routing table entry -
RouteChangeNtf
— sent to agent’s topic when a routing table entry is added, removed or changed
The
GetRouteReq
message may be used to request a single route (
all = false
), or a list of routes (
all = true
). When a single route is requested, the response is either a
RouteRsp
(with route information) or a
REFUSE
(if no route available). When multiple routes are requested, the response is a series of
RouteRsp
messages, followed by an
AGREE
message to indicate end of the list. If no routes available, a
REFUSE
message is sent back instead of the
AGREE
message.
While the above messages may be used programmatically to manipulate the routing table, typically users interact with the router via the shell commands described in Chapter 6 and Section 22.4 below.
22.3. Route maintenance service
org.arl.unet.Services.ROUTE_MAINTENANCE
Agents offering the ROUTE_MAINTENANCE service generate
RouteDiscoveryNtf
messages to allow ROUTING service providers to maintain routing tables.
22.3.1. Messages
Agents providing the ROUTE_MAINTENANCE service support the following messages:
-
RouteDiscoveryReq
⇒AGREE
/REFUSE
/FAILURE
— start route discovery to a specified node -
RouteTraceReq
⇒AGREE
/REFUSE
/FAILURE
— trace current route to a specified node -
RouteDiscoveryNtf
— sent to the agent’s topic when a route is discovered -
RouteTraceNtf
— sent to requestor when a requested trace is successfully completed
22.4. Router and the route discovery protocol
The
Router
class (agent name
router
) provides the ROUTING service in the standard stack. Apart from supporting the
routes
,
addroute
,
delroute
and
delroutesto
commands, this agent exposes two parameters:
-
auto1hop
— automatically assume single-hop routes available, if no entry for destination node in routing table -
defaultLink
— default LINK service provider to use for datagram transmission, if unspecified while adding a route
Without
auto1hop
enabled, every route must be explicitly added to the routing table (even when the node is accessible over a single hop). By enabling
auto1hop
, we tell the router than any node that isn’t explicitly added to the routing table is assumed to be accessible over a single hop. This is the default setting:
> router
« Router »
Routes datagrams in the network, based on a routing table.
[org.arl.unet.DatagramParam]
MTU = 3145581
RTU = 1447
[org.arl.unet.net.RouterParam]
auto1hop = true
defaultLink = uwlink
The
RouteDiscoveryProtocol
class (agent name
rdp
) provides the ROUTE_MAINTENANCE service in the standard stack. This agent has no configurable parameters.
In
Chapter 6
, we explored several examples of how to set up networks with static and dynamic routes. To find out more about routing, type
help router
in the shell:
> help router
router - access to routing service
Examples:
routes // display routing table
routes 2 // display routes to node 2
addroute 27, 29 // add a route to node 27 via node 29
delroute 'as7623' // delete route with UUID as7623
editroute 'as7623', metric: 5.0 // edit route to change metric
delroutesto 27 // delete all routes to node 27
delroutes // delete all routes
trace 27 // trace route to node 27
ping 27 // check if node 27 is accessible
Parameters:
- router.MTU - maximum data transfer size
- router.RTU - recommended data transfer size
- router.auto1hop - automatically assume single hop routes
- router.defaultLink - default link to use
Commands:
- routes - print routing table
- addroute - add a route to the routing table
- editroute - edit a route in the routing table
- delroute - delete a route from the routing table
- delroutesto - delete all routes to specified node from the routing table
- delroutes - delete all routes from the routing table
You can also type
help
followed by any of the commands above to get more information on the usage of that command.
23. Transport service
org.arl.unet.Services.TRANSPORT
23.1. Overview
Agents offering the TRANSPORT service provide end-to-end reliability and fragmentation/reassembly for large datagrams. They may also support connection-oriented services for data streaming. Agents providing this service typically use the ROUTING service for multi-hop delivery of data.
All agents supporting the TRANSPORT service must also support the DATAGRAM service ( Chapter 14 ), along with the RELIABILITY and FRAGMENTATION capabilities. It is also recommended that they support the CANCELLATION and PROGRESS capabilities, since datagrams at this level are likely to be large.
There are no special messages or parameters defined by the TRANSPORT service, but agents providing this service may expose additional parameters to configure the transport protocol in use.
23.2. Stop-and-wait transport
The default implementation of the TRANSPORT service is the
SWTransport
class. If you start the 2-node network simulation and connect to node A, you can explore the configurable parameters that it advertises:
> transport
« Stop-and-wait transport »
Multi-hop transport service based on the stop-and-wait protocol.
[org.arl.unet.DatagramParam]
MTU ⤇ 16777215
RTU ⤇ 16777215
[org.arl.unet.transport.SWTransportParam]
dsp = router
maxHops = 3
maxRetries = 2
reportProgress = false
timeoutPerHop = 90.0
We briefly explain each parameter below:
-
dsp
-
Datagram service provider. This is the agent that is used to deliver datagrams. Using
router
for this parameter enables multi-hop networks. -
maxHops
-
Maximum number of hops expected to destination nodes.
-
maxRetries
-
Maximum number of end-to-end retries, if end-to-end acknowledgements are not received. We recommend that link level reliability be enabled to reduce end-to-end retries. This is easily done by setting the
reliability
parameter for a route, or by enablingreliability
at the LINK service provider (if it offers such a parameter). -
reportProgress
-
Setting this parameter to
true
asks thetransport
agent to send out periodicDatagramProgressNtf
messages to the requester (on the transmitting node) or on the agent’s topic (on the receiving node), when transferring large datagrams. -
timeoutPerHop
-
Timeout (in seconds) per hop, for datagram transfer.
Next, let’s try a 2048-byte datagram transfer from node A to node B with progress reports:
> transport.reportProgress = true;
> transport << new DatagramReq(to: 31, data: new byte[2048], reliability: true)
AGREE
transport >> DatagramProgressNtf:INFORM[id:2 to:31 to:(838/2048 bytes, 40%)]
transport >> DatagramProgressNtf:INFORM[id:2 to:31 to:(1676/2048 bytes, 81%)]
transport >> DatagramProgressNtf:INFORM[id:2 to:31 to:(2048/2048 bytes, 100%)]
transport >> DatagramDeliveryNtf:INFORM[id:d76a56bf-58f6-4a9c-8a19-7a0a6ce8bcd4 to:31]
The entire transfer will take a few minutes. During that time, we get periodic
DatagramProgressNtf
reports showing how much of the data transfer was completed.
24. Remote access
org.arl.unet.Services.REMOTE
24.1. Overview
Agents offering the REMOTE service provide text messaging, file transfer, and remote command execution services across a network.
| While the REMOTE service provides a field for credentials to be included in a request, it does not specify how authentication and security should be handled by an agent. It is important for developers and users of the REMOTE service to give due consideration to network security before enabling this service on their network. |
24.1.1. Messages
Agents providing the REMOTE service support the following messages:
-
RemoteTextReq
⇒AGREE
/REFUSE
/FAILURE
— send a text message to remote node -
RemoteFileGetReq
⇒AGREE
/REFUSE
/FAILURE
— download a file from remote node -
RemoteFilePutReq
⇒AGREE
/REFUSE
/FAILURE
— upload a file to remote node -
RemoteExecReq
⇒AGREE
/REFUSE
/FAILURE
— execute a shell command on the remote node -
RemoteTextNtf
— sent to the agent’s topic when a text message from another node arrives -
RemoteFileNtf
— sent to the agent’s topic when an incoming file transfer from another node is completed -
RemoteSuccessNtf
— sent to a requester when a remote operation is successfully completed, if an acknowledgement was requested -
RemoteFailureNtf
— sent to a requester when a remote operation fails, if an acknowledgement was requested
24.2. RemoteControl
Start the 2-node network and connect to node A:
> agentsForService org.arl.unet.Services.REMOTE
[remote]
> remote
« Remote control »
Text messaging and remote command execution service.
[org.arl.unet.remote.RemoteControlParam]
cwd = /Users/mandar/tmp/unet-3.2.0/scripts
dsp = transport
enable = false
groovy = true
reliability = true
shell = websh
We see that the REMOTE service is provided by the
remote
agent of type
RemoteControl
. The agent’s behavior is controlled by several parameters:
-
cwd
-
Current working directory. This directory is the reference location for all file transfers and command execution.
-
dsp
-
Datagram service provider. This is the agent that is used to deliver datagrams.
-
shell
-
SHELL service provider (see Chapter 27 ) used to execute commands. When a remote command is to be executed, a request is sent to this shell agent to execute the command.
-
groovy
-
Enable Groovy extensions for shell commands. This should only be enabled if the
shell
is a Groovy shell. The only Groovy extension defined at this point in time is the?
shortcut. Starting a command with a?
automatically sends the output of the command back to the requesting node (e.g.?phy.MTU
is equivalent totell me, phy.MTU as String
). We have encountered the use of this extension before in Section 5.5 . -
reliability
-
Setting this to
true
enablesreliability
for all datagrams used by the agent. -
enable
-
Setting this to
true
enables incoming remote file operations and remote commands. The parameter isfalse
by default, for security reasons. Outgoing remote operations are always enabled, irrespective of this parameter. Incoming and outgoing text messaging is also always enabled when this agent is loaded.
The default
RemoteControl
agent in the basic stack does not implement any authentication. Once enabled, it will accept all file transfer and remote command execution requests. Care should be taken not to
enable
it in networks where malicious hackers may be able to send harmful requests to your node.
|
The
phy[CONTROL].scrambler
and
phy[DATA].scrambler
parameters available in many UnetStack-based modems provide a basic level of protection against malicious hackers by scrambling each transmission in the modem. Setting the
scrambler
to a "secret" value (64-bit key) in all your nodes enables this basic protection. Do bear in mind that scrambling is not a crypotgraphically strong technique, and will not protect you from a serious hacker. The technique is also vulnerable to playback attack, even if the malicious hacker is unable to unscramble your frame.
|
All remote commands (
tell
,
fget
,
fput
,
rsh
, and
ack
) encountered in
Section 5.4
and
Section 5.5
are implemented by the shell using the above messages. For example, the same effect as the
tell
command can be achieved by directly sending the
RemoteTextReq
message to the
remote
agent on node A:
> remote << new RemoteTextReq(to: host('B'), text: 'hello!', ack: true)
AGREE
remote >> RemoteSuccessNtf:INFORM[RemoteTextReq:REQUEST[to:31 text:hello! ack:true]]
We should see the text message delivered on node B:
[232]: hello!
We encourage you to re-read
Section 5.5
and explore the command’s help documentation (
help remote
) to fully appreciate the use of this service.
25. State persistence
org.arl.unet.Services.STATE_MANAGER
25.1. Overview
An agent offering the STATE_MANAGER service provides a way to save the state (parameter values) of specified (or all) agents.
Such an agent typically subscribes to the
PARAMCHANGE
topic and monitor
ParamChangeNtf
for all parameter changes for all agents. It then helps persist selected agents' parameter values between reboots.
25.1.1. Messages
STATE_MANAGER service providers honor the following messages:
-
SaveStateReq
⇒AGREE
/REFUSE
/FAILURE
— save agent state to a file -
ClearStateReq
⇒AGREE
/REFUSE
/FAILURE
— clear agent state in memory, and track only parameter changes henceforth
The
SaveStateReq
message causes the agent state (changed parameters) to be persisted to a Groovy script file in the
scripts
folder. The default name of the file is
saved-state.groovy
, and this file is automatically loaded on startup. However, the
SaveStateReq
message can specify an alternate filename to persist the state in. In such cases, the file is run manually when the state is to be restored.
25.2. Examples
Fire up Unet audio (
bin/unet audio
) to test out how state persistence works:
> 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
> plvl -40
OK
> shutdown
Now start Unet audio again, and you’ll find that the
plvl
state was not retained through reboots:
> 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
We can ask it to retain the state:
> plvl -40
OK
> savestate (1)
AGREE
> ls
README.md [759 bytes]
saved-state.groovy [156 bytes] (2)
> shutdown
| 1 |
The
savestate
command just sends a
SaveStateReq
message to the STATE_MANAGER service provider.
|
| 2 |
The
saved-state.groovy
file is created with all the parameter changes to all agents.
|
Start Unet audio again, and you’ll find that the state is retained:
> plvl
phy[1].powerLevel = -40.0
phy[2].powerLevel = -40.0
phy[3].powerLevel = -40.0
phy[4].powerLevel = -40.0
phy.signalPowerLevel = -40.0
> shutdown
The
saved-state.groovy
is human-readable, and you’ll see that it simply contains the Groovy code to set the parameters required to restore the state:
def phy = agent('phy')
phy[1].powerLevel = -40.0
phy[2].powerLevel = -40.0
phy[3].powerLevel = -40.0
phy[4].powerLevel = -40.0
phy.signalPowerLevel = -40.0
Delete this file and start Unet audio again:
> help savestate
savestate - save state of all or specified agent in Groovy script format
Examples:
savestate 'pandan' // save current state of all agents
savestate 'pandan', 'phy' // save current state of specified agent
savestate 'pandan', phy // save current state of specified agent
savestate // save current state in "saved-state.groovy"
> help clrstate
clrstate - set current state as the baseline for savestate
Example:
clrstate // set baseline state
phy[1].powerLevel = -10 // change parameters
savestate // save changed parameters
The help shows you that the
savestate
command can be used to save the state of individual agents, if you wish, to a filename of your choice. If you save the state to a different filename, it is not automatically restored on startup. But you can restore it easily with a single command (name of the file):
> 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
> plvl -40
OK
> savestate 'p40', phy (1)
AGREE
> plvl -10 (2)
OK
> ls
README.md [759 bytes]
p40.groovy [156 bytes] (3)
> p40 (4)
> plvl
phy[1].powerLevel = -40.0
phy[2].powerLevel = -40.0
phy[3].powerLevel = -40.0
phy[4].powerLevel = -40.0
phy.signalPowerLevel = -40.0
| 1 |
Save the
plvl -40
state to a file called
p40.groovy
.
|
| 2 | Change the state. |
| 3 |
The state is saved in the
p40.groovy
file in the
scripts
folder.
|
| 4 |
Command
p40
runs the
p40.groovy
file to restore the state to
plvl -40
.
|
26. Scheduler
org.arl.unet.Services.SCHEDULER
26.1. Overview
Agents offering the SCHEDULER service provide a way to schedule sleep/wake tasks in the future.
26.1.1. Messages
The following messages are used by the SCHEDULER service:
-
AddScheduledSleepReq
⇒AGREE
/REFUSE
/FAILURE
— add a new scheduled sleep -
RemoveScheduledSleepReq
⇒AGREE
/REFUSE
/FAILURE
— remove a scheduled sleep -
GetSleepScheduleReq
⇒AGREE
/REFUSE
/FAILURE
— get all the scheduled sleep/wake times -
WakeFromSleepNtf
— sent to agent’s topic just after node wakes up from a sleep
26.2. Sleep/wake scheduling
The Unet simulator currently does not support sleep/wake scheduling. While Unet audio supports the SCHEDULER service, it does not actually put your computer to sleep. Therefore you can experiment with creating and managing schedules on Unet audio, without worrying about your computer going to sleep. Start up Unet audio (
bin/unet audio
) and connect to its shell:
> agentsForService org.arl.unet.Services.SCHEDULER
[phy]
> phy.rtc
Sun Sep 22 02:58:20 SGT 2019
> help scheduler
scheduler - access to scheduling service
Commands:
- addsleep - schedule sleep and wakeup of the modem
- showsleep - shows sleep/wakeup schedule
- rmsleep - removes sleep/wakeup schedule
> help addsleep
addsleep - schedule sleep and wakeup of the modem
Examples:
addsleep<PHONE_NUMBER>,<PHONE_NUMBER> // sleep from epoch<PHONE_NUMBER> to<PHONE_NUMBER>
addsleep<PHONE_NUMBER> // sleep immediately until<PHONE_NUMBER>
addsleep 10.s.later, 20.s.later // sleep 10s later and wake up 20s later
addsleep 20.s.later // sleep immediately and wake up 20s later
addsleep 20.s.later, forever // sleep 20s later forever
addsleep // sleep immediately forever
> addsleep 1.hour.later, forever
AGREE
> showsleep
bbfb3b79-942c-4fba-bc37-ab9d18dabda5: Sun Sep 22 04:00:00 SGT 2019 to eternity
> rmsleep 'bbfb3b79-942c-4fba-bc37-ab9d18dabda5'
AGREE
> showsleep
> addsleep 1.hour.later, 2.hours.later
AGREE
> showsleep
87de2dec-db29-4b34-a93c-775bfe8c68c5: Sun Sep 22 04:02:36 SGT 2019 to Sun Sep 22 05:02:36 SGT 2019
We see that the
phy
agent provides the SCHEDULER service in Unet audio. We add a sleep schedule, check that it shows up, remove it, and check that it is deleted. We then add another schedule.
The
addsleep
,
showsleep
, and
rmsleep
commands use the
AddScheduledSleepReq
,
GetSleepScheduleReq
, and
RemoveScheduledSleepReq
messages to achieve their functionality. We can manually send this messages to confirm this, if we like:
> phy << new GetSleepScheduleReq()
SleepScheduleRsp:INFORM[(1 item)]
> ans.sleepSchedule
[87de2dec-db29-4b34-a93c-775bfe8c68c5: Sun Sep 22 04:02:36 SGT 2019 to Sun Sep 22 05:02:36 SGT 2019]
We can also use the web interface to manage the sleep schedule, if we like:
A big advantage of working with the web interface for sleep scheduling is that the user interface displays date/time in a human readable format. On the other hand, programmatic access with messages requires times to be specified as Unix epoch time.
26.3. Epoch time
The Unix epoch is the number of seconds that have elapsed since January 1, 1970 (midnight UTC), not counting leap seconds. While computers find it easy to work with epoch time, we find it hard to interpret. So UnetStack introduces syntactic sugar such as "1.hour.later" that computes the Unix epoch time 1 hour from now:
> 1.hour.later
1569097078
> 2.minutes.later
1569093616
There are online calculators that’ll help you convert between Unix epoch time and human readable date/time. This works well, if you need to manually convert a few date/times, but what if you needed to do this programmatically? Java provides simple APIs to deal with date/times:
> import java.time.Instant
> Instant.now().epochSecond (1)
1569094526
> Instant.ofEpochSecond(1569093616) (2)
2019-09-21T19:20:16Z
> Instant.parse("2019-09-21T19:20:16Z").epochSecond (3)
1569093616
| 1 | Get current epoch time. |
| 2 | Convert epoch time to human readable time in UTC. |
| 3 | Convert human readable time in UTC to epoch time. |
If you want even nicer looking date/time strings, you should check out Java’s SimpleDateFormat class.
27. Shell
org.arl.fjage.shell.Services.SHELL
The SHELL service provides a way to run commands and access files on a node. It is used by agents providing the REMOTE service to support remote execution of commands from other nodes.
27.1. Overview
Agents providing the SHELL service support execution of commands, and access to files on the node. While shell agents typically provide interactivity using a terminal/console, they also support messages for other agents to request execution of commands or access to files.
27.1.1. Messages
-
ShellExecReq
⇒AGREE
/REFUSE
/FAILURE
— execute a command -
GetFileReq
⇒FileGetRsp
/REFUSE
/FAILURE
— read a file or directory contents -
PutFileReq
⇒AGREE
/REFUSE
/FAILURE
— write contents to a file, or delete a file
27.2. Script engines
The language in which the commands are written is not defined by the service, but depends on the shell agent. fjåge supports a pluggable mechanism for an
ShellAgent
to use any
ScriptEngine
. Various script engines are available in fjåge and UnetStack, including the
GroovyScriptEngine
,
EchoScriptEngine
, and
ATScriptEngine
.
27.3. Examples
Start the 2-node network and connect to node A:
> agentsForService org.arl.fjage.shell.Services.SHELL (1)
[websh]
> websh.send new ShellExecReq(cmd: 'file("foobar").text = "FOOBAR";') (2)
websh >> AGREE
> ls (3)
foobar [6 bytes]
README.md [759 bytes]
> file("foobar").text (4)
FOOBAR
| 1 |
We find that
websh
is the agent that provides us the SHELL service.
|
| 2 |
We send a command to
websh
to execute, and it agrees to do so. Since the commands we type are also executed by the
websh
agent, we need to be careful to not block the execution. Hence we use a
send
rather than a
request
(or equivalently
<<
).
|
| 3 |
The command was to create a
foobar
file, so we check that the file is created.
|
| 4 |
We read the contents of the
foobar
file to confirm that
FOOBAR
was correctly written to it.
|
Next, let’s try the
GetFileReq
and
PutFileReq
messages to read, write and delete this file:
> websh.send new GetFileReq(filename: 'scripts/foobar') (1)
websh >> INFORM: GetFileRsp
> ntf.contents (2)
[70, 79, 79, 66, 65, 82]
> new String(ntf.contents) (3)
FOOBAR
> websh.send new PutFileReq(filename: 'scripts/foobar', contents: 'foooobaaaar')
websh >> AGREE (4)
> file('foobar').text (5)
foooobaaaar
> websh.send new PutFileReq(filename: 'scripts/foobar', contents: null)
websh >> AGREE (6)
> ls
README.md [759 bytes]
> websh.send new GetFileReq(filename: 'scripts') (7)
websh >> INFORM: GetFileRsp
> new String(ntf.contents)
README.md<PHONE_NUMBER>372000 (8)
| 1 |
The file
foobar
was created in the
scripts
folder, which is the default location for the
file()
function. We ask to read the file, and get a
GetFileRsp
response back.
|
| 2 | The file contents are read back as a list of bytes. |
| 3 |
We convert the list of bytes to a String to get our
FOOBAR
contents.
|
| 4 |
We send a
PutFileReq
to change the contents of the file.
|
| 5 | We verify that the file contents were indeed changed, as requested. |
| 6 |
Sending a
PutFileReq
with
contents
set to
null
deletes the file.
|
| 7 |
Asking for the contents of a directory using
GetFileReq
gets us the directory listing back.
|
| 8 |
The listing consists of all files in the directory, one file per line. Each line has a filename, file size and file modificiation timestamp (epoch time). If a file is a directory, the filename is suffixed by a
/
.
|
We interacted with the SHELL service provider using a shell! That’s not very useful in practice, but served to show you how these messages work. Typically, these messages are sent by other agents that wish to get the shell to run commands and access files for them (e.g.
RemoteControl
agent in
Section 24.2
). The agents may be running remotely in a fjåge slave container or on a gateway (via the UnetSocket API), where they may not have direct access to the filesystem of the node.
Part V: Extending UnetStack
28. Developing your own agents
By now, you should be very familiar with the concept of agents. You have interacted with them via commands and messages throughout this handbook, but what exactly is an agent?
If you lookup the Wikipedia entry for a software agent, you’ll find:
The term agent describes a software abstraction, an idea, or a concept, similar to object-oriented programming terms such as methods, functions, and objects. The concept of an agent provides a convenient and powerful way to describe a complex software entity that is capable of acting with a certain degree of autonomy in order to accomplish tasks on behalf of its host. But unlike objects, which are defined in terms of methods and attributes, an agent is defined in terms of its behavior.
retrieved 8 September 2019
In this chapter, we take this somewhat abstract concept and crystallize it by developing a simple agent. While the idea of writing your own agent might sound daunting at first, you’ll soon see that it is actually quite easy!
28.1.  They exchange messages, provide services and implement protocols. While what is expected from a well-behaved agent is quite demanding, most of the necessary core behaviors are already implemented for you by the
UnetAgent
base class. All you need to do is to extend it, and add in a little code to teach the agent what you want it to do.
| While you have the option of writing agents in Java or Groovy (or any other language running on the Java VM), we recommend writing agents in Groovy, as Groovy agents tend to need less biolerplate code and are more readable and maintainable. They are also easier to test as Groovy classes can be dynamically loaded from source, without having to pre-compile them. However, if you are already an expert in Java and prefer to use it, you’re welcome to do so. |
The basic skeleton of a Groovy agent looks like this:
import org.arl.fjage.*
import org.arl.unet.*
class MyAgent extends UnetAgent {
@Override
void setup() {
// this method is called when the stack is initialized
// register services and capabilities that you provide here
}
@Override
void startup() {
// this method is called just after the stack is running
// look up other agents and services here, as needed
// subscribe to topics of interest to get notifications
}
@Override
Message processRequest(Message msg) {
// process requests supported by the agent, and return responses
// if request is not processed, return null
return null
}
@Override
void processMessage(Message msg) {
// process other messages, such as notifications here
// if a message is not interesting, it can be safely just ignored
}
}
While you don’t strictly need the
@Override
annotations, it is a good practice to use them whenever you are overriding a method from a superclass. The annotation tells the compiler that this is what you intend, and so if you make a typographical mistake and type in a wrong method name (one that doesn’t exist in the superclass), the compiler will warn you.
If you do not need any of these methods, you can skip the definition as the base class provides default implementations. There are a several other methods that you can override to customize your agent, but these are less commonly needed and so we’ll skip them for now. You’ll come across them later.
If you happen to be already familiar with the
fjåge agent lifecycle
, you may wish to note that the
setup()
method is called from the
init()
method of the agent. The
startup()
method is called from a one-shot behavior scheduled during initialization. The
processRequest()
and
processMessage()
methods are called from a message behavior added during initialization.
|
28.2. Groovy echo daemon
It’s best to illustrate with a simple example.
Let’s develop an
echo daemon
that will respond to each incoming
echo request
datagram with an
echo response
datagram containing the same data as the echo-request. We need a way to identify which datagram is an echo request, as we don’t want to be echoing datagrams intended for other agents or for the user. We do this by defining an echo request datagram as any datagram with protocol
USER
(recall that protocol numbers from
USER
onwards are available for your own applications to use). We do not want the response to use the same protocol, otherwise our daemon (running on the source node) could get confused and echo the response, which would in turn be echoed again by the destination node’s daemon, ad infinitum. So we use protocol
DATA
for the echo response datagram, as this protocol is intended for generic application data.
Here’s our daemon:
import org.arl.fjage.*
import org.arl.unet.*
class EchoDaemon extends UnetAgent {
@Override
void startup() {
// subscribe to all agents that provide the datagram service
subscribeForService(Services.DATAGRAM)
}
@Override
void processMessage(Message msg) {
if (msg instanceof DatagramNtf && msg.protocol == Protocol.USER) {
// respond to protocol USER datagram with protocol DATA datagram
send new DatagramReq(
recipient: msg.sender,
to: msg.from,
protocol: Protocol.DATA,
data: msg.data
)
}
}
}
Let’s walk through the above code:
-
Our agent does not provide any formal services or capabilities, so we we skip the
setup()
andprocessRequest()
methods from the skeleton. -
The
startup()
method looks up all agents providing the DATAGRAM service, and subscribes to any notifications from any of these agents. These notifications will inlcude theDatagramNtf
messages that are published when datagrams are received from another node. When a notification arrives, theprocessMessage()
method will be called. -
In the
processMessage()
method, we check for datagram notifications with protocolUSER
, and respond to each of them by sending aDatagramReq
to the sender of the notification, requesting it to send a datagram with protocolDATA
to the node that sent the echo request, with the data copied from the echo request.
That’s it!
Do not get confused between
sender
and
from
, and
recipient
and
to
fields in datagram messages. The
sender
and
recipient
always
refer to the agents that generate and consume the message, respectively. These are entities within a single Unet node. The
from
and
to
are node addresses that tell us which node is transmitting the datagram, and which node is the intended destination.
|
It’s time for us to test this agent. Create a file called
EchoDaemon.groovy
in the
classes
folder and copy the above daemon code into it.
Now start the 2-node network simulation that we have been using as a testbed, and on node B, load the agent:
> container.add 'echo', new EchoDaemon(); (1)
> 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.phy.Ranging - IDLE
uwlink: org.arl.unet.link.ECLink - 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
echo: EchoDaemon - IDLE (2)
router: org.arl.unet.net.Router - IDLE
mac: org.arl.unet.mac.CSMA - IDLE
WebGW-5c9c1c68385a388f: REMOTE
| 1 |
Create an agent called
echo
based on the
EchoDaemon
class.
|
| 2 |
We see that the
echo
agent is now running.
|
Our daemon is up and running!
Once the daemon is successfully loaded on node B, we can test it from node A:
> subscribe phy (1)
> phy << new DatagramReq(to: host('B'), protocol: Protocol.USER, data: [42]) (2)
AGREE
phy >> TxFrameNtf:INFORM[type:DATA txTime:2809812247]
phy >> RxFrameStartNtf:INFORM[type:DATA rxTime:2811767943]
phy >> RxFrameNtf:INFORM[type:DATA from:31 to:232 rxTime:2811767943 (1 byte)]
> ntf.data
[42] (3)
| 1 |
We subscribe to
phy
so that we can see the incoming echo response from the peer node.
|
| 2 | Transmit a physical layer frame containing the echo request and some data. |
| 3 | The data we sent was echoed back. |
We have written our first agent! Was easy, wasn’t it?
Unet modems also have a
classes
folder that accepts Groovy source files or compiled Java/Groovy class files. You can use the web interface of the modem to upload files to that folder. If your code has many class files, you may wish to package them together into a jar archive and place it in the
jars
folder.
|
28.3. Java echo daemon
If you’re a Java programmer and find the Groovy syntax daunting, you might prefer to write your agents in pure Java (at the expense of verbosity and more steps for testing). This is the equivalent Java code below for the Groovy agent we developed in the last section:
import org.arl.fjage.*;
import org.arl.unet.*;
public class EchoDaemon extends UnetAgent {
@Override
public void startup() {
// subscribe to all agents that provide the datagram service
subscribeForService(Services.DATAGRAM);
}
@Override
public void processMessage(Message msg) {
if (msg instanceof DatagramNtf && ((DatagramNtf)msg).getProtocol() == Protocol.USER) {
// we got an echo request!
// respond with a protocol DATA datagram
DatagramNtf ntf = (DatagramNtf)msg;
DatagramReq req = new DatagramReq(ntf.getSender());
req.setTo(ntf.getFrom());
req.setProtocol(Protocol.DATA);
req.setData(ntf.getData());
send(req);
}
}
}
In Java, you’ll first need to compile the Java code. Create a
EchoDaemon.java
file with the above contents. To compile it, you’ll need to have fjåge and unet-framework jar files on the classpath:
$ javac -cp lib/fjage-1.8.0.jar:lib/unet-framework-3.2.0.jar EchoDaemon.java
You should now have a
EchoDaemon.class
file which you copy to the
classes
folder. To avoid duplicate classes, remember to first delete the
EchoDaemon.groovy
file!
Finally, you can run the 2-node network simulator and test the agent, just as you did in the previous section.
28.4. Behaviors
Agents implement most of their functionality with behaviors.
| UnetStack is implemented on top of the fjåge agent framework. fjåge provides a set of standard behaviors for agents to extend. We will explore some of these behaviors in this section, but you are encouraged to read the fjåge documentation at your leisure to learn more. |
We have been implicitly using two behaviors so far. The
startup()
method is called by the
UnetAgent
base class using a
OneShotBehavior
, and the
processMessage()
method is called from a
MessageBehavior
. While you could have manually added these behaviors, the
UnetAgent
base class does this for you, because almost all Unet agents require this.
Let’s next look at a use case for explicitly adding other behaviors. Say we wanted our echo daemon to not respond immediately, but after 7 seconds. How would we do that?
We could of course add a
delay(7000)
in the
processMessage()
method, but that would be a bad idea. If we did that, the agent would sleep for 7 seconds on receiving a request and not process any request from any other nodes! We want the agent to be responsive while waiting, and so do not want to block execution. Instead, we want a behavior that will occur 7 seconds later — this is precisely what a
WakerBehavior
does. Here’s our new
processMessage()
method:
@Override
void processMessage(Message msg) {
if (msg instanceof DatagramNtf && msg.protocol == Protocol.USER) {
// respond to protocol USER datagram with protocol DATA datagram after 7 seconds
add new WakerBehavior(7000, {
send new DatagramReq(
recipient: msg.sender,
to: msg.from,
protocol: Protocol.DATA,
data: msg.data
)
})
}
}
The
WakerBehavior
that we add is triggered 7000 ms later, and the echo response is sent in that behavior. Simple!
Behaviors in Groovy use closures to make the syntax easy to work with. If you were writing your agent in Java, you’d need to create an anonymous class and override the
onWake()
method.
|
Go ahead and replace the
processMessage()
method in your
EchoDaemon.groovy
file and try it! In order to reload the agent, all you need to do on node B is:
> container.kill echo
true
> container.add 'echo', new EchoDaemon();
And now you can send an echo request from node A as before and see that the response is delayed by 7 seconds.
You could also send a second request during those 7 seconds, and the echo daemon on node B would process that concurrently. You can send 2 echo requests right after each other, and you’ll see the corresponding echo responses 7 seconds later, but right after each other.
28.5. Parameters
We have seen many agents with parameters that you can get/set. If we wanted to make our echo daemon delay configurable, it would be perfect to expose it as a parameter. Let’s do that next.
With the echo daemon loaded on node B, we see that it doesn’t have any configurable parameters by default:
> echo
<<< EchoDaemon >>>
Let’s add a title, description and one
delay
parameter to our daemon:
import org.arl.fjage.*
import org.arl.fjage.param.Parameter
import org.arl.unet.*
class EchoDaemon extends UnetAgent {
enum Params implements Parameter { (1)
delay
}
final String title = 'Echo Daemon' (2)
final String description = 'Echoes any USER datagrams back as DATA' (3)
int delay = 7000 (4)
@Override
void startup() {
// subscribe to all agents that provide the datagram service
subscribeForService(Services.DATAGRAM)
}
@Override
void processMessage(Message msg) {
if (msg instanceof DatagramNtf && msg.protocol == Protocol.USER) {
// respond to protocol USER datagram with protocol DATA datagram after 7 seconds
add new WakerBehavior(delay, {
send new DatagramReq(
recipient: msg.sender,
to: msg.from,
protocol: Protocol.DATA,
data: msg.data
)
})
}
}
List<Parameter> getParameterList() { (5)
allOf(Params)
}
}
| 1 | Declare a list of parameters that the agent advertises. We have declared this enum as an inner class, but you could choose to declare it as a separate class if you wish. |
| 2 | Provide a descriptive title for the agent. |
| 3 | Provide a descriptive text for the agent. |
| 4 | Declare the parameter. |
| 5 | Advertise the list of parameters. |
| Note that we had to take 3 steps to add a parameter: declare a list of parameters, declare the parameter, and advertise the parameter. While this might seem like a lot, bear in mind that parameters are much more than just an agent’s class attributes. Parameters can be get/set remotely, even from a different Java VM, different computer, or through a UnetSocket gateway API. |
If you were writing the agent in Java instead of Groovy, you’d need to implement getters and setters for parameter
delay
, rather than simply declare the attribute. This is because Groovy automatically creates the getters and setters for you.
|
Let’s see how the agent looks with parameters. Reload the agent on node B and check its parameters:
> container.kill agent('echo')
true
> container.add 'echo', new EchoDaemon();
> echo
<<< Echo Daemon >>> (1)
Echoes any USER datagrams back as DATA (2)
[EchoDaemon.Params]
delay = 7000
> echo.delay
7000
> echo.delay = 5000
5000
> echo.delay
5000
| 1 | Notice the change in title. |
| 2 | The description is shown here. |
We have changed the delay from 7 seconds to 5 seconds. Go ahead and send a echo request from node A and see that you get a response back in 5 seconds!
If you want to compute parameter values on demand or validate parameters, you can implement getters/setters for the parameter, and they will be called. If you want a read-only parameter, you can declare the attribute as
private
and implement only a getter for that parameter.
|
While our example above uses a static description, the description can also be dynamic. This can be useful if you want to display agent’s status information in the description. To implement dynamic descriptions, simply replace the
description
attribute by a getter
getDescription()
that returns a
String
description when called.
|
28.6. Services, capabilities, and notifications
Most of the agents we have been interacting with, have advertised services, and sometimes, optional capabilities. They also honor requests and publish unsolicited notifications. All of these are quite straightforward to implement, and you can explore some of these features in this blog article on how to implement a simple PHYSICAL service agent (modem driver). We will explore some of these in the next chapter, along with other cool features like finite state machine behaviors and protocol data unit (PDU) codecs.
29. Implementing network protocols
You now know how to write simple agents. But real world network protocols demand more complexity such as advertising services, looking up other agents, providing parameters that are computed on demand, encoding/decoding complex PDUs, generating random variates, and describing behaviors as finite state machines (FSMs). In this chapter, we illustrate how to do all these things with ease, using a few examples.
In
Chapter 20
, we looked at the MAC service in detail. In the next few sections, we develop three simple MAC agents (
MySimplestMac
,
MySimpleThrottledMac
and
MySimpleHandshakeMac
) to illustrate how network protocols and services are implemented by agents. The MAC agents are intentionally kept simple and not optimized for performance, as we wish to illustrate the key aspects of MAC agent development without getting lost in the details of optimal protocols.
29.1. Simple MAC without handshake
To illustrate how a MAC agent might work, let us start with a simple MAC agent that grants every reservation request as soon as it is made:
import org.arl.fjage.*
import org.arl.unet.*
import org.arl.unet.mac.*
class MySimplestMac extends UnetAgent {
@Override
void setup() {
register Services.MAC // advertise that the agent provides a MAC service
}
@Override
Message processRequest(Message msg) {
if (msg instanceof ReservationReq) {
// check requested duration
if (msg.duration <= 0) return new RefuseRsp(msg, 'Bad reservation duration')
// prepare START reservation notification
ReservationStatusNtf ntf1 = new ReservationStatusNtf(
recipient: msg.sender,
inReplyTo: msg.msgID,
to: msg.to,
status: ReservationStatus.START)
// prepare END reservation notification
ReservationStatusNtf ntf2 = new ReservationStatusNtf(
recipient: msg.sender,
inReplyTo: msg.msgID,
to: msg.to,
status: ReservationStatus.END)
// send START reservation notification immediately
add new OneShotBehavior({
send ntf1
})
// wait for reservation duration, and then send END reservation notification
add new WakerBehavior(Math.round(1000*msg.duration), {
send ntf2
})
// return a reservation response, which defaults to an AGREE performative
return new ReservationRsp(msg)
}
return null
}
}
Note a number of interesting features of the code above:
-
The
setup()
method is used to advertise the service provided by this agent. -
We provide basic error checking, and refuse a request that is invalid, providing a descriptive reason.
-
We prepare the AGREE response as well as the START and END status notification messages, all at once. We send out the START notification immediately (using a
OneShotBehavior
), use aWakerBehavior
to schedule the END notification to be sent out at an appropriate time, and then simply return the AGREE response. The use of theOneShotBehavior
ensures that the START notification is sent after the AGREE response, and not before. -
We return a
null
if we don’t understand the request, allowing the superclass to respond with a NOT_UNDERSTOOD message.
While the above code implements a fully functional MAC agent, it needs to respond to
ReservationCancelReq
,
ReservationAcceptReq
and
TxAckReq
messages, and provide
channelBusy
,
reservationPayloadSize
,
ackPayloadSize
,
maxReservationDuration
and
recommendedReservationDuration
parameters in order to comply with the MAC service specification (
Chapter 20
). We add this functionality trivially, by responding to the messages with
RefuseRsp
(message with a
REFUSE
performative and a descriptive reason), and returning default values for all the parameters. The resulting complete source code is shown below:
import org.arl.fjage.*
import org.arl.fjage.param.Parameter
import org.arl.unet.*
import org.arl.unet.mac.*
class MySimplestMac extends UnetAgent {
@Override
void setup() {
register Services.MAC
}
@Override
Message processRequest(Message msg) {
switch (msg) {
case ReservationReq:
if (msg.duration <= 0) return new RefuseRsp(msg, 'Bad reservation duration')
ReservationStatusNtf ntf1 = new ReservationStatusNtf(
recipient: msg.sender,
inReplyTo: msg.msgID,
to: msg.to,
status: ReservationStatus.START)
ReservationStatusNtf ntf2 = new ReservationStatusNtf(
recipient: msg.sender,
inReplyTo: msg.msgID,
to: msg.to,
status: ReservationStatus.END)
add new OneShotBehavior({
send ntf1
})
add new WakerBehavior(Math.round(1000*msg.duration), {
send ntf2
})
return new ReservationRsp(msg)
case ReservationCancelReq:
case ReservationAcceptReq: // respond to other requests defined
case TxAckReq: // by the MAC service with a RefuseRsp
return new RefuseRsp(msg, 'Not supported')
}
return null
}
// expose parameters defined by the MAC service, with just default values
@Override
List<Parameter> getParameterList() {
return allOf(MacParam) // advertise the list of parameters
}
final boolean channelBusy = false // parameters are marked as 'final'
final int reservationPayloadSize = 0 // to ensure that they are read-only
final int ackPayloadSize = 0
final float maxReservationDuration = Float.POSITIVE_INFINITY
final Float recommendedReservationDuration = null
}
Now we have a fully-compliant, but very simple, MAC agent!
29.2. Testing our simple MAC
The
MySimplestMac
agent from the previous section is available in the
samples
folder of your Unet simulator. To test it, fire up the 2-node network simulator and connect to node A:
> ps
remote: org.arl.unet.remote.RemoteControl - IDLE
state: org.arl.unet.state.StateManager - IDLE
rdp: org.arl.unet.net.RouteDiscoveryProtocol - IDLE
ranging: org.arl.unet.phy.Ranging - IDLE
uwlink: org.arl.unet.link.ECLink - 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 see that the
org.arl.unet.mac.CSMA
agent is the current
mac
. To use our
MySimplestMac
agent, you first need to kill the
org.arl.unet.mac.CSMA
agent, and then load the
MySimplestMac
agent:
> container.kill mac
true
> container.add 'mac', new MySimplestMac()
mac
> ps
remote: org.arl.unet.remote.RemoteControl - IDLE
state: org.arl.unet.state.StateManager - IDLE
rdp: org.arl.unet.net.RouteDiscoveryProtocol - IDLE
ranging: org.arl.unet.phy.Ranging - IDLE
uwlink: org.arl.unet.link.ECLink - 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: MySimplestMac - IDLE
> mac
« MySimplestMac »
[org.arl.unet.mac.MacParam]
ackPayloadSize ⤇ 0
channelBusy ⤇ false
maxReservationDuration ⤇ Infinity
recommendedReservationDuration ⤇ null
reservationPayloadSize ⤇ 0
It’s loaded and working!
Now, you can ask for a reservation and see if it responds correctly:
> mac << new ReservationReq(to: 31, duration: 3.seconds)
ReservationRsp:AGREE
mac >> ReservationStatusNtf:INFORM[to:31 status:START]
mac >> ReservationStatusNtf:INFORM[to:31 status:END]
Indeed it does! The START notification arrives immediately after the AGREE response, and the END notification arrives about 3 seconds later.
29.3. Simple MAC with throttling
While the above simple MAC would work well when the traffic offered to it is random, it will perform poorly if the network is fully loaded. All nodes would constantly try to access the channel, collide and the throughput would plummet. To address this concern, one may add an exponentially distributed random backoff (Poisson arrival to match the assumption of Aloha) for every request, to introduce randomness. The backoff could be chosen to offer a normalized network load of approximately 0.5, since this generates the highest throughput for Aloha.
Here’s the updated code with some bells and whistles:
import org.arl.fjage.*
import org.arl.fjage.param.Parameter
import org.arl.unet.*
import org.arl.unet.phy.*
import org.arl.unet.mac.*
class MySimpleThrottledMac extends UnetAgent {
private final static double TARGET_LOAD = 0.5
private final static int MAX_QUEUE_LEN = 16
(1)
private AgentID phy
boolean busy = false // is a reservation currently ongoing?
Long t0 = null // time of last reservation start, or null
Long t1 = null // time of last reservation end, or null
int waiting = 0
@Override
void setup() {
register Services.MAC
}
@Override
void startup() {
phy = agentForService(Services.PHYSICAL) (2)
}
@Override
Message processRequest(Message msg) {
switch (msg) {
case ReservationReq:
if (msg.duration <= 0) return new RefuseRsp(msg, 'Bad reservation duration')
if (waiting >= MAX_QUEUE_LEN) return new RefuseRsp(msg, 'Queue full')
ReservationStatusNtf ntf1 = new ReservationStatusNtf(
recipient: msg.sender,
inReplyTo: msg.msgID,
to: msg.to,
status: ReservationStatus.START)
ReservationStatusNtf ntf2 = new ReservationStatusNtf(
recipient: msg.sender,
inReplyTo: msg.msgID,
to: msg.to,
status: ReservationStatus.END)
// grant the request after a random backoff (3)
AgentLocalRandom rnd = AgentLocalRandom.current() (4)
double backoff = rnd.nextExp(TARGET_LOAD/msg.duration/nodes) (5)
long t = currentTimeMillis()
if (t0 == null || t0 < t) t0 = t
t0 += Math.round(1000*backoff) // schedule packet with a random backoff
if (t0 < t1) t0 = t1 // after the last scheduled packet (6)
long duration = Math.round(1000*msg.duration)
t1 = t0 + duration
waiting++
add new WakerBehavior(t0-t, { (7)
send ntf1
busy = true
waiting--
add new WakerBehavior(duration, {
send ntf2
busy = false
})
})
return new ReservationRsp(msg)
case ReservationCancelReq:
case ReservationAcceptReq:
case TxAckReq:
return new RefuseRsp(msg, 'Not supported')
}
return null
}
// expose parameters defined by the MAC service, and one additional parameter
@Override
List<Parameter> getParameterList() {
return allOf(MacParam, Param)
}
enum Param implements Parameter {
nodes (8)
}
int nodes = 6 // number of nodes in network, to be set by user
final int reservationPayloadSize = 0
final int ackPayloadSize = 0
final float maxReservationDuration = Float.POSITIVE_INFINITY
boolean getChannelBusy() { (9)
return busy
}
float getRecommendedReservationDuration() { (10)
return get(phy, Physical.DATA, PhysicalChannelParam.frameDuration)
}
}
| 1 | We define a few attributes to keep track of channel state and reservation queue. |
| 2 |
We lookup other agents in
startup()
after they have had a chance to advertise their services during the setup phase.
|
| 3 | Requests are no longer granted immediately, but after a random backoff instead. |
| 4 |
Random numbers are generated using a
AgentLocalRandom
utility. This utility ensures repeatable results during discrete event simulation, aiding with debugging, and so is the preferred way of generating random variates.
|
| 5 |
The
nextExp()
function generate a exponentially distributed random number with a specified rate parameter. The rate parameter is computed such that the average backoff introduced helps to achieve the specified target load.
|
| 6 |
In Groovy, a comparison with
null
(initial value of
t1
) is permitted, and will always be false.
|
| 7 | Note that we no longer send the START notification immediately. Instead we schedule it after a backoff, and then schedule the END notification after the reservation duration from the START. |
| 8 |
We implement one user configurable parameter
nodes
, and advertise it.
|
| 9 |
Parameter
busy
is no longer always false, since we now keep track of reservations. We return
busy
to be true only during the time between a reservation START and END.
|
| 10 |
Parameter
recommendedReservationDuration
is now determined based on the frame duration of the PHYSICAL service, assuming that most reservations are for transmitting one frame. A client is free to choose a longer reservation time, if it wishes to transmit many frames in one go (as it should for efficient use of the channel).
|
A copy of this code is available in the
samples
folder of your Unet simulator. We encourage you to test it out, in the same way as we tested
MySimplestMac
in
Section 29.2
. You’ll find that the START notification no longer arrives immediately after the AGREE response, but arrives a few seconds later, after a random backoff.
29.4. Simple MAC with handshake
While the MAC agents we have developed so far are fully functional, they are simple, and do not involve any signaling for channel reservation. Many MAC protocols such as MACA and FAMA involve a handshake using RTS and CTS PDUs. To illustrate how more complex protocols are developed using UnetStack, we implement a simple RTS-CTS 2-way handshake-based MAC agent next.
Many communication protocols are best described using an FSM. We illustrate the FSM for our simple handshake-based MAC agent in Figure 10 .
When the channel is free, the agent is in an IDLE state. If the agent receives a
ReservationReq
, it switches to the RTS state and sends an RTS PDU to the intended destination node. If it receives a CTS PDU back, then it switches to a TX state and urges the client to transmit data via a
ReservationStatusNtf
with a START status. After the reservation period is over, the agent switches back to the IDLE state. If no CTS PDU is received in the RTS state for a while, the agent times out and returns to the IDLE state after informing the client of a reservation FAILURE.
If the agent receives an RTS PDU in the IDLE state, it switches to the RX state and responds with a CTS PDU. The node initiating the handshake may then transmit data for the reservation duration. After the duration (plus some allowance for 2-way propagation delay), the agent switches back to the IDLE state. If the agent overhears (aka snoops) RTS or CTS PDUs destined for other nodes, it switches to a BACKOFF state for a while. During the state, it does not initiate or respond to RTS PDUs. After the backoff period, it switches back to the IDLE state.
Our RTS and CTS PDUs are identified by a protocol number. Since we are implementing a MAC protocol, we choose to tag our PDUs using the protocol number reserved for MAC agents (
Protocol.MAC
). We also define some timeouts and delays that we will need to use:
int PROTOCOL = Protocol.MAC
float RTS_BACKOFF = 2.seconds
float CTS_TIMEOUT = 5.seconds
float BACKOFF_RANDOM = 5.seconds
float MAX_PROP_DELAY = 2.seconds
Communication protocols often use complicated PDU formats. UnetStack provides a
PDU
class to help encode/decode PDUs. Although the RTS and CTS PDUs have a pretty simple format, the PDU is still useful in defining the format clearly:
int RTS_PDU = 0x01
int CTS_PDU = 0x02
PDU pdu = PDU.withFormat {
uint8('type') // RTS_PDU/CTS_PDU
uint16('duration') // ms
}
Here we have defined a PDU with two fields — type (8 bit) and duration (16 bit). The type may be either of RTS_PDU or CTS_PDU, while the duration will specify the reservation duration in milliseconds. We will later use this
pdu
object to encode and decode these PDUs.
Now comes the heart of our MAC protocol implementation –- the FSM shown in Figure 10 . First we define the FSM states and the events that the FSM reacts to:
enum State {
IDLE, RTS, TX, RX, BACKOFF
}
enum Event {
RX_RTS, RX_CTS, SNOOP_RTS, SNOOP_CTS
}
Next we use the
FSMBuilder
utility class to construct a
FSMBehavior
from a declarative concise representation of the FSM.
The FSM states are defined using the
state(…)
declarations. The actions to take when entering/exiting a state are defined in the
onEnter
/
onExit
clauses. The behavior of the FSM in response to events are defined using the
onEvent(…)
clauses. Timers that operate in a state are defined using the
after(…)
clauses. Finally actions to take continuously while in a state are defined using the
action
clause.
It should be easy to see the direct mapping between the FSM diagram and the FSM code below:
int MAX_RETRY = 3
int MAX_QUEUE_LEN = 16
Queue<ReservationReq> queue = new ArrayDeque<ReservationReq>(MAX_QUEUE_LEN)
FSMBehavior fsm = FSMBuilder.build {
int retryCount = 0
float backoff = 0
def rxInfo
def rnd = AgentLocalRandom.current()
state(State.IDLE) {
action {
if (!queue.isEmpty()) {
// add random backoff for each reservation to allow other nodes
// a chance to reserve, especially in case of a heavily loaded network
after(rnd.nextDouble(0, BACKOFF_RANDOM)) {
setNextState(State.RTS)
}
}
block()
}
onEvent(Event.RX_RTS) { info ->
rxInfo = info
setNextState(State.RX)
}
onEvent(Event.SNOOP_RTS) {
backoff = RTS_BACKOFF
setNextState(State.BACKOFF)
}
onEvent(Event.SNOOP_CTS) { info ->
backoff = info.duration + 2*MAX_PROP_DELAY
setNextState(State.BACKOFF)
}
}
state(State.RTS) {
onEnter {
Message msg = queue.peek()
def bytes = pdu.encode(
type: RTS_PDU,
duration: Math.ceil(msg.duration*1000))
phy << new TxFrameReq(
to: msg.to,
type: Physical.CONTROL,
protocol: PROTOCOL,
data: bytes)
after(CTS_TIMEOUT) {
if (++retryCount >= MAX_RETRY) {
sendReservationStatusNtf(queue.poll(), ReservationStatus.FAILURE)
retryCount = 0
}
setNextState(State.IDLE)
}
}
onEvent(Event.RX_CTS) {
setNextState(State.TX)
}
}
state(State.TX) {
onEnter {
ReservationReq msg = queue.poll()
retryCount = 0
sendReservationStatusNtf(msg, ReservationStatus.START)
after(msg.duration) {
sendReservationStatusNtf(msg, ReservationStatus.END)
setNextState(State.IDLE)
}
}
}
state(State.RX) {
onEnter {
def bytes = pdu.encode(
type: CTS_PDU,
duration: Math.round(rxInfo.duration*1000))
phy << new TxFrameReq(
to: rxInfo.from,
type: Physical.CONTROL,
protocol: PROTOCOL,
data: bytes)
after(rxInfo.duration + 2*MAX_PROP_DELAY) {
setNextState(State.IDLE)
}
rxInfo = null
}
}
state(State.BACKOFF) {
onEnter {
after(backoff) {
setNextState(State.IDLE)
}
}
onEvent(Event.SNOOP_RTS) {
backoff = RTS_BACKOFF
reenterState()
}
onEvent(Event.SNOOP_CTS) { info ->
backoff = info.duration + 2*MAX_PROP_DELAY
reenterState()
}
}
}
Do note that the above FSM includes a couple of details that were missing from the FSM diagram. Firstly, we implement a random backoff before switching to the RTS state to minimize contention. Secondly, we implement a
retryCount
counter to check the number of times a single
ReservationReq
has been tried. If it exceeds
MAX_RETRY
, we discard it. Thirdly, we have a
backoff
variable that allows different backoff times for different occasions. The variable is set each time, just before the state is changed to
State.BACKOFF
or before the backoff state is re-entered.
The FSM uses a simple utility method to send out