8  Data transfers & QoS

In Chapter 2, we saw how to send and receive text messages (using tell) and datagrams (using dtx). In Chapter 3, we learnt that the tell and dtx commands are implemented using the underlying messages RemoteTextReq and DatagramReq respectively. In this chapter, we are going to look at DatagramReq (and some related messages) in much more depth, as there is much more to datagrams than meets the eye. In particular, we are interested in various datagram attributes that enable us to define fine-grained Quality of Service (QoS) requirements to utilize limited bandwidth most effectively.

In addition to datagrams, we encountered file transfers via fget and fput commands in Chapter 5. We will also explore file transfers in some depth and understand the differences between datagrams and file transfers.

8.1 Datagrams

Datagrams are parcels of data that are delivered atomically. Datagrams may be fragmented into smaller chunks for delivery, but they are reassembled at the destination prior to delivery. Either the whole datagram is delivered or it is lost, but it is never partially delivered.

A datagram has a from address, to address, protocol number, and some data. In addition to these basic attributes, datagrams may have several additional attributes:

  • reliability (boolean) – controls end-to-end acknowledgement of successful delivery. If reliability is set to true, the delivery of the datagram is acknowledged by the recipient, and a DatagramDeliveryNtf is generated. If the delivery failed or timed out, a DatagramFailureNtf is generated instead. If reliability is set to false, a DatagramTransmissionNtf is generated when the datagram is transmitted by the node, or a DatagramFailureNtf is generated if the datagram was dropped for some reason.
  • robustness (Robustness) – controls trade-off between data rate and likelihood of loss. The default robustness is NORMAL. If a datagram is tagged as ROBUST, the stack makes best effort to deliver it using a robust communication scheme. A robust scheme is one that is designed to minimize loss at the expense of data rate.
  • priority (Priority) – controls order in which datagrams in a queue are processed. When many datagrams are queued for transmission, higher priority datagrams are given preference. Possible priorities include URGENT, HIGH, NORMAL, LOW and IDLE.
  • ttl (float) – time-to-live (TTL, in seconds) after which the stack may choose to drop the datagram. TTL is typically used to tag data that is not useful if stale. If the data is undelivered beyond its lifetime, the stack may opt to drop the data and save valuable bandwidth and energy.
  • progress (boolean) – controls progress notifications for large datagram transfers. For large datagram transfers that take substantial time to deliver, setting progress to true requests UnetStack to generate ProgressNtf to indicate progress of transfer. The notifications are generated on a best effort basis.
  • route (string) – selects a specific route for data transfer. When unspecified, routes are selected automatically by UnetStack. If a datagram must use a specific route, it should be tagged with the ID of that route.
  • shortcircuit (boolean) – controls an optimization to reduce data transfer overhead. When an agent in the stack determines that its functionality is not required in delivering a datagram, it can choose not to add any headers if shortcircuit is set to true (default). This enables the stack to be frugal with transmissions, only adding headers when they carry information that is necessary for that datagram.

Let us explore some of these attributes in more detail.

8.1.1 Reliability & robustness

The terms robustness and reliability are often confused. Robustness refers to a trade-off between data rate and likelihood of fragment loss. A robust datagram is likely to use a lower rate communication scheme that minimizes the chance of losing a fragment. Lost datagram fragments can be detected and retransmitted by the stack, but that requires feedback from the receiver. Requesting robustness reduces the need for acknowledgement/retransmission at the expense of data rate. Reliability, on the other hand, refers to end-to-end delivery notifications.

Let’s start a 2-node-network to observe the effect of some of these attributes:

$ bin/unet samples/2-node-network.groovy

2-node network
--------------

Node A: tcp://localhost:1101, http://localhost:8081/shell.html
Node B: tcp://localhost:1102, http://localhost:8082/shell.html

On node A:

> dtx host('B'), [1,2,3]
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019a1738-6e0a-77e0-f271-e6cb98762ffa to:31]
> dtx host('B'), [1,2,3], reliability: true
AGREE
caddy >> DatagramDeliveryNtf:INFORM[id:019a1738-8d46-7c98-b549-81bb78c3ed2b]
> dtx host('C'), [1,2,3]
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019a1739-5b18-70bc-e982-a55d30af8295 to:74]
> dtx host('C'), [1,2,3], reliability: true
AGREE
caddy >> DatagramFailureNtf:INFORM[id:019a1738-c8ae-7e57-956e-25bdc8d71484]

We first transmit an unreliable datagram to node B and get a DatagramTransmissionNtf when it is transmitted. We then transmit a reliable datagram to node B and get a DatagramDeliveryNtf when it is successfully delivered. This takes noticeably longer, since it requires an acknowledgement from node B. Next, we transmit an unreliable datagram to node C. No such node exists in our network, yet we get a DatagramTransmissionNtf when the datagram is transmitted. On the other hand, when we finally transmit a reliable datagram to node C, since no such node exists in our network, we get a DatagramFailureNtf. However, this takes some time as UnetStack tries to deliver the datagram several times before giving up.

To observe the effect of robustness, let’s enable display of all datagram fragments on node B:

> dshow on

Now, we first transmit a normal 32-byte datagram from node A:

> dtx host('B'), 1..32
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019f4239-f134-7368-c5be-07d7ffa5fc15 to:31]

On node B, we see that the datagram is delivered as a single fragment of type DATA:

phy >> RxFrameNtf:INFORM[type:DATA from:232 to:31 rxStartTime:2380532743 rssi:-72.6 (32 bytes)]
  0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20

We can check the data rate for the DATA channel:

> phy[DATA]
« PHY »

[org.arl.unet.DatagramParam]
  MTU ⤇ 123
  RTU ⤇ 123

[org.arl.unet.bb.BasebandDetectorParam]
  capture ⤇ 24000
  coherence ⤇ 0
  inhibit ⤇ 0
  metricStatistics ⤇ 0.0
  preamble = [2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0 ... , 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
  stride ⤇ 1
  threshold ⤇ 0.0

[org.arl.unet.phy.PhysicalChannelParam]
  dataRate = 1003.9216
  fec ⤇ default
  fecList ⤇ null
  frameDuration ⤇ 1.02
  frameLength = 128
  framing ⤇ default
  framingList ⤇ null
  maxFrameLength = 4096
  modulation ⤇ default
  modulationList ⤇ null
  powerLevel = -10.0
  schemeCode ⤇ null

We see that the DATA channel uses a communication scheme that provides a data rate of 1004 bps in this simulation. Each transmitted DATA frame takes 1.02 seconds.

We now transmit a robust 32-byte datagram from node A:

> dtx host('B'), 1..32, robustness: Robustness.ROBUST
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019f4239-fd12-7b15-f903-994fe7cc9047 to:31]

We observe a noticeable delay before we see the DatagramTransmissionNtf. On node B, we see:

phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 protocol:2 rxStartTime:2383966743 rssi:-72.6 (8 bytes)]
  00fb00000020000d
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 protocol:2 rxStartTime:2384610743 rssi:-72.6 (13 bytes)]
  00000a0102030405060708090a
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 protocol:2 rxStartTime:2385244743 rssi:-72.6 (13 bytes)]
  00100a0b0c0d0e0f1011121314
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 protocol:2 rxStartTime:2385879743 rssi:-72.6 (13 bytes)]
  00200a15161718191a1b1c1d1e
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 protocol:2 rxStartTime:2386517743 rssi:-72.6 (9 bytes)]
  0030021f2000000000
uwlink >> DatagramNtf:INFORM[from:232 to:31 (32 bytes)]
  0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 protocol:2 rxStartTime:2387155743 rssi:-72.6 (13 bytes)]
  00400a970609cbab0d6de38398

We see the expected DatagramNtf with the 32 bytes of data that we transmitted. But we see that this datagram was delivered using multiple transmissions of type CONTROL, each reported here as a RxFrameNtf. We can check the data rate for the CONTROL channel:

> phy[CONTROL]
« PHY »

[org.arl.unet.DatagramParam]
  MTU ⤇ 13
  RTU ⤇ 13

[org.arl.unet.bb.BasebandDetectorParam]
  capture ⤇ 13488
  coherence ⤇ 0
  inhibit ⤇ 0
  metricStatistics ⤇ 0.0
  preamble = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0 ... , 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
  stride ⤇ 1
  threshold ⤇ 0.0

[org.arl.unet.phy.PhysicalChannelParam]
  dataRate = 247.42267
  fec ⤇ default
  fecList ⤇ null
  frameDuration ⤇ 0.582
  frameLength = 18
  framing ⤇ default
  framingList ⤇ null
  maxFrameLength = 128
  modulation ⤇ default
  modulationList ⤇ null
  powerLevel = -10.0
  schemeCode ⤇ null

We see a CONTROL data rate of 247 bps for this simulation. Each transmitted CONTROL frame takes 0.582 seconds. The normal datagram took a single DATA transmission (about 1 second), whereas the robust datagram required 6 CONTROL transmissions (total 3.5 seconds). Each CONTROL frame is, however, much less likely to be lost. And even if one CONTROL frame is lost, the datagram is encoded in a way that it is likely to be successfully delivered. This provides more robustness to the delivery of the second datagram.

8.1.2 Compression

On node A, we transmit a 4 kB datagram:

> dtx host('B'), new byte[4096]
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-1530-7096-f274-a1c872cb6321 to:31]

At a data rate of 1004 bps that DATA channel gives us in this simulation, we would expect a 4 kB datagram to take at least 32 seconds. However, you should see the DatagramTransmissionNtf within a second or so. And if you had the shell on node B open, you’d have seen the DatagramNtf appear almost immediately:

phy >> RxFrameNtf:INFORM[type:DATA from:232 to:31 protocol:6 rxStartTime:2389738743 rssi:-72.6 (28 bytes)]
  8080789cedc1010d000000c2a0f74f6d0f0714000000f06e10000001
caddy >> DatagramNtf:INFORM[from:232 to:31 (4096 bytes)]
  0000000000000000000000000000000000000000000000000000000000000000
  0000000000000000000000000000000000000000000000000000000000000000
  0000000000000000000000000000000000000000000000000000000000000000
  0000000000000000000000000000000000000000000000000000000000000000
  ...

Most UnetStack agents support data compression. A 4 kB buffer of zeros is very compressible, and so it was transmitted in a single DATA frame of 28 bytes!

If we generate 4 kB of random data and transmit it:

> x = new byte[4096];
> new Random().nextBytes(x);
> dtx host('B'), x
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019a1776-230f-75c8-b6c5-d9fea38a2f8c to:31]

we see that this indeed takes more than 32 seconds to deliver. And we see lots of fragments come through on node B before we see the DatagramNtf:INFORM[from:232 to:31 (4096 bytes)] appear.

To reduce clutter, let us turn off the display of all datagram fragments on node B for the rest of this chapter:

> dshow off       // turn off all datagram display
> dshow 0         // turn on display of user datagrams only

We could have combined these two commands as dshow off, 0 if we wanted to save some typing!

8.1.3 Priority

NotePremium feature

QoS is a premium feature (see Section 13.3) in UnetStack, and may not be available in the community edition. If unavailable, the priority attribute in the DatagramReq is silently ignored.

On UnetStack-compatible modems, you can check if your modem supports prioritization:

> remote << new CapabilityReq(DatagramCapability.PRIORITY)
CONFIRM

If you see CONFIRM as a response, your modem supports it. If you see DISCONFIRM, see Section 13.3 for how to enable it.

On node A, let’s transmit 5 datagrams:

> 5.times {
-   dtx host('B'), [it+1]
- }
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-32e2-7ed7-a15f-932c35c86a8f to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-32e3-756d-a94e-5ac616acdaa7 to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-32e4-7e47-838c-ac8c14815552 to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-32e5-7f03-a269-6336397a81a2 to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-32e5-74e4-f879-6c340bcee341 to:31]

The local variable it here refers to the loop counter that runs from 0 to 4, so 5 datagrams with data [01], [02], [03], [04] and [05] are transmitted. We see all 5 of them arrive in sequence on node B:

phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2397339743 rssi:-72.6 (1 byte)]
  01
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2397985743 rssi:-72.6 (1 byte)]
  02
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2398624743 rssi:-72.6 (1 byte)]
  03
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2399262743 rssi:-72.6 (1 byte)]
  04
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2399902743 rssi:-72.6 (1 byte)]
  05

We repeat the transmissions from node A, but this time we tag the first 3 transmissions as NORMAL priority (default) and the last 2 transmissions as HIGH priority:

> 5.times {
-   dtx host('B'), [it+1], priority: (it < 3 ? Priority.NORMAL : Priority.HIGH)
- }
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-4cb0-77c3-cc62-54f75b8225e9 to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-4cb3-73df-ab09-29bf91da21aa to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-4cb3-7c31-e159-f282789f4fe0 to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-4cb1-7cb3-d246-9ef99fdbc8b1 to:31]
caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-4cb2-7775-f6c0-d67beeb4ce24 to:31]

Observe the sequence in which the datagrams appear on node B:

phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2403945743 rssi:-72.6 (1 byte)]
  01
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2404589743 rssi:-72.6 (1 byte)]
  04
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2405226743 rssi:-72.6 (1 byte)]
  05
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2405872743 rssi:-72.6 (1 byte)]
  02
phy >> RxFrameNtf:INFORM[type:CONTROL from:232 to:31 rxStartTime:2406519743 rssi:-72.6 (1 byte)]
  03

Datagram [01] was transmitted as soon as it was offered and so arrives first. By the time the next datagram is transmitted, all the remaining ones are in the queue. Since datagrams [04] and [05] are marked as HIGH priority, they are delivered next. The remaining datagrams [02] and [03] are delivered only after that.

8.1.4 Progress

Progress notifications are useful for data transfers that take long. To see how they work, we initiate a 5 kB data transfer from node A:

> x = new byte[5120];
- new Random().nextBytes(x);
- remote << new RemoteMessageReq(to: host('B'), data: x, progress: true)
AGREE
caddy >> ProgressNtf:INFORM[to:31 (719/5124 bytes, 14%)]
caddy >> ProgressNtf:INFORM[to:31 (1438/5124 bytes, 28%)]
caddy >> ProgressNtf:INFORM[to:31 (2157/5124 bytes, 42%)]
caddy >> ProgressNtf:INFORM[to:31 (2876/5124 bytes, 56%)]
caddy >> ProgressNtf:INFORM[to:31 (3595/5124 bytes, 70%)]
caddy >> ProgressNtf:INFORM[to:31 (4314/5124 bytes, 84%)]
caddy >> ProgressNtf:INFORM[to:31 (5033/5124 bytes, 98%)]
caddy >> ProgressNtf:INFORM[to:31 (5124/5124 bytes, 100%)]
caddy >> DatagramTransmissionNtf:INFORM[id:019f23ff-b166-7c30-dbe5-932497ae3140 to:31]

We see regular ProgressNtf showing the progress of the transfer, and a DatagramTransmissionNtf at the end to indicate that the transmission was completed.

8.2 Remote messages

Agents providing the REMOTE service support a special type of datagrams called remote messages. These datagrams are sent using the RemoteMessageReq message, and are delivered with a RemoteMessageNtf message. Remote messages support some additional attributes:

  • mimeType (string) – MIME type defines the content type of the message. Content type may be used by UnetStack for content-aware processing. The default MIME type is application/octet-stream. If the data being sent is textual, we can set the MIME type to text/plain. All other MIME types are considered reserved and should not be used by end-user applications.
  • remoteRecipient (string) – specifies the topic or agent ID on the remote node to send the datagram to. Usually datagrams are delivered to the Topics.DATAGRAM topic. Setting this attribute allows the sender to specify an agent ID or topic to deliver the RemoteMessageNtf to.
  • mailbox (string) – is used only if TTL is set to TTL_MAILBOX, to name a mailbox for this message. We discuss mailboxes in detail below.
  • messageClass (string) – specifies a message class that may be used to route the message. We discuss message class based routing in detail below.
  • inReplyTo (string) – specifies the messageID of a RemoteMessageNtf being responded to. Setting this attribute creates a threaded response, where the corresponding RemoteMessageNtf is tagged with a inReplyTo field of the original RemoteMessageReq. We discuss this in detail below.

8.2.1 Mailboxes

NotePremium feature

QoS is a premium feature (see Section 13.3) in UnetStack, and may not be available in the community edition. If unavailable, the ttl and mailbox attributes in the RemoteMessageReq are silently ignored.

On UnetStack-compatible modems, you can check if you have the TTL feature available:

> remote << new CapabilityReq(DatagramCapability.TTL)
CONFIRM

If you see CONFIRM as a response, your modem supports TTL and mailboxes. If you see DISCONFIRM instead, see Section 13.3 for how to enable it.

Consider a scenario where node A is an underwater surveillance robot and node B is the mothership operating the robot. The robot typically broadcasts a status message every 2 seconds, and the mothership receives it. Whenever the robot’s surveillance sensors detect an important event, the robot prioritizes transmission of the details of the event to the mothership. This transmission takes several seconds, and during that time the status messages cannot be delivered.

If a status message is still in the queue when a new status message becomes available, there is no point sending the old one since its contents are out-of-date! The old one should be replaced by the new one and sent out at the next available opportunity. We can indicate this desired behavior by setting the ttl for the status message to be TTL_MAILBOX and specifying the mailbox name.

We simulate regular status message on node A using a TickerBehavior that ticks every 2 seconds:

> count = 0;
> ticker = agent.add new TickerBehavior(2000, {
-   remote << new RemoteMessageReq(data:[count], ttl:RemoteMessageReq.TTL_MAILBOX, mailbox:'STATUS')
-   count = (count + 1) % 256
- })
org.arl.fjage.TickerBehavior@4cc5e5f8
TipTicker behavior

We will learn more about behaviors in Chapter 33. For now, all you need to know is that a TickerBehavior executes the code given in {...} every 2000 ms as a background task. The behavior can be stopped by calling ticker.stop() when we no longer want it to tick.

We see a DatagramTransmissionNtf on node A and a RemoteMessageNtf on node B every 2 seconds once this is running. After a short while, we simulate the transmission of an important event from node A:

> x = new byte[1024];                     // generate 1 kB of
> new Random().nextBytes(x);              // random data
> remote << new RemoteMessageReq(data:x)   // and transmit it
AGREE
caddy >> DatagramTransmissionNtf:INFORM[id:019a17bf-5eb7-7d26-eea7-7037c8fe9343 to:0]

If everything is set up correctly, during the time the event details are being transferred, we expect status messages to be dropped. The status messages should resume as soon as the transfer is done. We can verify that this is indeed what happens by looking at the shell on node B:

caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  00
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  01
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  02
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  03
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1024 bytes)]
  49b5f80e13020deb19e66fd9abc71ea699b8a85b14ac870d4457b7415d0cf07a
  c27c71bcb26033433a3218b820ce6976472ea2c9eec21265fb5f8ebac4681663
      :
  a8b80f888740e9fa36fc74cf8a5f75b8036e8850b36cad7afe7bf7cd98d8d74a
  0ef81abc0d581d2682f884d2248a76a5dd2232d37a0f2d7d6b6249cc191c23cb
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  08
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  09
caddy >> RemoteMessageNtf:INFORM[from:232 mimeType:application/octet-stream (1 byte)]
  0a

Status messages 00 to 03 are delivered before the event details, and then the status messages continue from 08 onwards. The queued outdated status messages 04 through 07 are dropped.

We can now stop the TickerBehavior simulating the status messages on node A:

> ticker.stop()

8.2.2 Threaded responses

Continuing with the scenario with the robot and the mothership, say the mothership sends a series of measurement requests to the robot. The robot makes the measurements and sends them back to the mothership. However, different measurements take different amounts of time, and so the order in which the measurements are sent back may not be the same order in which the requests were made. In order not to confuse the operator at the mothership, we need to associate each response with the corresponding request. This is the idea of threading. The request starts a conversation. Responses can add to the conversation thread.

To simulate this, we create two measurement requests (m1 and m2) on node B, and send the first one:

> m1 = new RemoteMessageReq(to: host('A'), data: 'Q1');
> m2 = new RemoteMessageReq(to: host('A'), data: 'Q2');
> remote << m1
AGREE

We receive it on Node A and store it as ntf1:

caddy >> RemoteMessageNtf:INFORM[from:31 to:232 mimeType:application/octet-stream (2 bytes)]
  5131
> ntf1 = ntf;

We send the second one from node B:

> remote << m2
AGREE

and store the corresponding notification on node A as ntf2:

caddy >> RemoteMessageNtf:INFORM[from:31 to:232 mimeType:application/octet-stream (2 bytes)]
  5132
> ntf2 = ntf;

After making both measurements, node A responds with the measurement for m2 first:

> remote << new RemoteMessageReq(to: ntf2.from, data: 'A2', inReplyTo: ntf2.messageID)
AGREE

By setting the inReplyTo field to the messageID of the request being responded to, we create a thread. On node B, we receive the corresponding RemoteMessageNtf with the measurement:

caddy >> DatagramTransmissionNtf:INFORM[id:019f423a-7e31-777c-cb82-6d47eaf32c89 to:232]
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  4132
> ntf.inReplyTo
019f423a-7e31-777c-cb82-6d47eaf32c89
> m1.messageID
019f423a-765d-73ad-d6f7-4230b9596b39
> m2.messageID
019f423a-7e31-777c-cb82-6d47eaf32c89

By comparing the inReplyTo field with the messageID for m1 and m2, we see that the RemoteMessageNtf just received corresponds to m2.

A short while later, we send the measurement for m1 from node A:

> remote << new RemoteMessageReq(to: ntf1.from, data: 'A1', inReplyTo: ntf1.messageID)
AGREE

and receive it on node B:

caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  4131
> ntf.inReplyTo
019f423a-765d-73ad-d6f7-4230b9596b39

We see that it corresponds to the messageID for m1.

8.2.3 Message classes

NotePremium feature

QoS is a premium feature (see Section 13.3) in UnetStack, and may not be available in the community edition. If unavailable, it is not possible to add user-defined routing rules.

On UnetStack-compatible modems, you can check if you have the rule-based routing feature available:

> caddy << new CapabilityReq(DatagramCapability.PRIORITY)
CONFIRM

The rule-based routing is part of the QoS prioritization feature suite supported by the agent caddy, and hence we ask caddy if it supports DatagramCapability.PRIORITY. If you see CONFIRM as a response, your modem supports rule-based routing. If you see DISCONFIRM instead, see Section 13.3 for how to enable it.

Remote messages can be tagged with a messageClass. This attribute is not used by UnetStack, but is available for user-defined rules to utilize. Let’s take an example application to explore how a rule could use message classes.

Continuing with the example from previous sections, say our robot transmits two types of status messages every 5 seconds. The first type is small and designed for acoustic transmission in water. The second one is large and is only to be transmitted if we have a WiFi connection to the robot when it is on the surface. Let’s label the small status message with messageClass value of STATUS1 and the large status message with STATUS2.

NoteSmall and large messages

In practice, the small message may be a few tens of bytes long, and the large message may be perhaps several kilobytes long. For the purposes of this demonstration, however, we will use a 1-byte “short” message and a 2-byte “long” message. The key point to illustrate is that long messages are only sent if the WiFi connection is available, and the exact length of the messages isn’t important to us here.

To simulate a WiFi connection between nodes A and B, we set up a UDP link as described in Section 7.1. On node A:

> container.add 'udplink', new UdpLink();
> addroute 31, 31, udplink
OK
> addroute 31, 31, uwlink
OK
> routes
    uuid      to nextHop         link reliability hops dataRate metric enabled  auto  poll
------------------------------------------------------------------------------------------
  agwxlp      31      31      udplink        true    1 100000000    8.0    true  true   0.0
  1g5xbo      31      31       uwlink        true    1     1962    3.3    true  true   0.0

and on node B:

> container.add 'udplink', new UdpLink();

We see that the metric for route agwxlp is higher than for route 1g5xbo. So the udplink will be selected preferentially when both routes are available. However, if the udplink is disabled because the robot is underwater, then the acoustic link (uwlink) will be used.

We are now ready to write our rule that STATUS2 messages should only be sent if a udplink is available. On node A:

> container.getAgent(caddy).addRule({ msg, routes ->
-   if (msg.messageClass == 'STATUS2') {
-     for (r in routes)
-       if (r.link == udplink) return r
-   }
- })

As noted above, caddy is the agent providing us the rule-based routing functionality. The rule we added checks if the messageClass is STATUS2, and if so, selects the route that uses udplink.

A rule is a function that takes in 2 arguments – the RemoteMessageReq being processed, and a List of RouteInfo objects denoting all possible routes for delivering the message (including disabled routes). If the rule decides that a specific route should be used, it returns that RouteInfo object. If not, it returns null. If the returned route is disabled, transmission is deferred until the route becomes enabled again.

We are now ready to start our TickerBehavior to send out the status messages from node A:

> count = 0;
> ticker = agent.add new TickerBehavior(5000, {
-   remote << new RemoteMessageReq(to: 31, data:[count], ttl:RemoteMessageReq.TTL_MAILBOX, mailbox: 'A', messageClass:'STATUS1')
-   remote << new RemoteMessageReq(to: 31, data:[0,count], ttl:RemoteMessageReq.TTL_MAILBOX, mailbox: 'B', messageClass:'STATUS2')
-   count = (count + 1) % 256
- })
org.arl.fjage.TickerBehavior@10618b2b

This starts sending both status messages every 5 seconds. We use TTL_MAILBOX to avoid building up status message backlog if messages are not transmitted in a timely manner (such as when the route isn’t available).

After a few messages are sent, we disable the route via udplink on node A to simulate the robot diving:

> editroute 'agwxlp', enabled: false
OK

After a few more status messages, we simulate the robot resurfacing by enabling the route via udplink on node A:

> editroute 'agwxlp', enabled: true
OK

Letting a few more status message be emitted, we stop the status messages and clear our rules:

> ticker.stop()
> container.getAgent(caddy).clearRules()

Now let’s look at what we received on node B:

caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  00
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  0000
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  01
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  0001
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  02
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  0002
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  03
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  0003
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  04
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  05
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  06
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  07
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  08
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  0008
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  09
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  0009
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  0a
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  000a
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (1 byte)]
  0b
caddy >> RemoteMessageNtf:INFORM[from:232 to:31 mimeType:application/octet-stream (2 bytes)]
  000b

The 1-byte messages are the short status messages and the 2-byte messages are the long status messages. We initially get both messages on node B (counts 00 through 03) when the robot is at the surface. We then only see the short status messages for a while (counts 04 through 07) while the robot is submerged. Finally, we see both status messages again (counts 08 through 0b) as the robot resurfaces.

This example shows how the messageClass attribute can be used in rules to select routes. In this particular example, we could have used the data length to make the decision instead of tagging the message with a messageClass. However, generally it is a much cleaner and more robust design to tag the message and not rely on its contents for routing decisions.

8.3 Remote text messages

Remote text messages are specialized versions of remote messages, and are typically sent using a RemoteTextReq request and delivered using a RemoteTextNtf message. The MIME type of the remote text message is application/x-chat. When a remote text message is received by a shell, it is displayed as a text message. Remote text messages use a special encoding that uses only 7 bits per character if the message text is ASCII.

In previous chapters, we have been using tell to send text messages. tell simply sends a RemoteTextReq to the REMOTE service provider.

Since RemoteTextReq extends a RemoteMessageReq, all features described above for RemoteMessageReq also work with RemoteTextReq.

8.4 Remote execution requests

Remote execution requests are specialized versions of remote messages, and are typically sent using a RemoteExecReq request. The MIME type of the remote execution request is application/x-command. When a remote execution request is received by a REMOTE service provider, it executes the specified command if remote execution is enabled (and credentials are valid, if the remote agent supports authentication).

In previous chapters, we have been using rsh to send remote execution requests. rsh simply sends a RemoteExecReq to the REMOTE service provider.

Since RemoteExecReq extends a RemoteMessageReq, all features described above for RemoteMessageReq also work with RemoteExecReq.

8.5 Remote file transfers

In Chapter 5, we performed file transfers between nodes using the fget and fput commands. These commands send the RemoteFileGetReq and RemoteFilePutReq messages to the REMOTE service provider. While the messages are typically meant for file transfer operations, they also support data to be provided and/or delivered as an array of bytes.

To demonstrate this, we first enable remote operations on node B:

> remote.enable = true;

Then on node A:

> remote << new RemoteFilePutReq(to: 31, data: 'test'.bytes, filename: 't1.txt')
AGREE
caddy >> RemoteSuccessNtf:INFORM[id:019a1a03-25ef-748f-ed3a-3abf31e173ac]
> remote << new RemoteFileGetReq(to: 31, filename: 't1.txt', localFilename: '')
AGREE
caddy >> RemoteFileNtf:INFORM[from:31 transferDuration:7.616 (4 bytes)]
> new String(ntf.data)
test

By setting the data attribute, we specify that we wish to transmit a byte array (and not a local file) and store it into a remote file. By setting the localFilename to an empty string (or equivalently RemoteFileGetReq.NONE), we specify that we wish to retrieve the contents of a remote file as a byte array (and not create a local file). The array is sent to us as data in the RemoteFileNtf.

We can delete the file t1.txt that we created on node B for this test:

> file('t1.txt').delete()

Although both datagram transfers and remote file transfers can be used to move data from one node to another, they are quite distinct in terms of semantics and use cases. To understand this more, we highlight key differences:

  1. Remote file transfers support several transfer modes: local file → remote file, local array → remote file, remote file → local file, remote file → local array. In all cases, the data on the remote node resides in a file, whereas the data on the local node can reside in a file or in a byte array. On the other hand, datagram transfers are always from a local byte array → remote byte array.
  2. Since remote file transfers access the filesystem on the remote node, they require explicit access control. We have to set remote.enable = true on the remote node (and provide credentials, if the remote agent supports authentication) for file transfers to work. Datagram transfers do not require any special settings or credentials on the remote node.
  3. Datagram transfers are optimized for small atomic payloads. While datagram transfers of several MB are indeed possible, loss in connectivity for an extended period during the transfer can cause the entire transfer to fail. File transfers are optimized for large payloads and handle temporary loss of connectivity much better.
  4. Partially transferred files can be written to persistent storage. In case of loss of connectivity, file transfers can be resumed at a later time without the need to restart the transfer from the beginning. It is also possible to synchronize files across nodes by only retrieving the new data in the remote file if the remote file grows over time (common in cases where the file contains streaming sensor data). Datagram transfers are atomic, and cannot be paused and resumed.
  5. Datagram transfers (remote messages) support many features discussed above (priority, progress notifications, TTL, mailboxes, remote recipients, threaded responses, message class based routing, etc). File transfers do support priority and progress notifications, but not the other features.

We next demonstrate resumable file transfers by creating a scenario where we have a partially downloaded file locally.

NotePremium feature

Resumable file transfers are a premium feature (see Section 13.3) in UnetStack, and may not be available in the community edition. On UnetStack-compatible modems, the feature is generally available as an optional upgrade.

On node B, we create a random file with 10 kB of data:

> x = new byte[10240];
> new Random(42).nextBytes(x);
> file('f1').bytes = x;
> ls
f1 [10 kB]
README.md [759 bytes]

On node A, we create two files with the 9 kB of the same data. These represent partially downloaded versions of the f1 file:

> x = new byte[10240];
> new Random(42).nextBytes(x);
> file('f2').bytes = x[0..9216];
> file('f3').bytes = x[0..9216];
> ls
f2 [9 kB]
f3 [9 kB]
README.md [759 bytes]

We will demonstrate resumable file transfers by downloading remote file f1 to local file f2 without resuming, and to local file f3 by resuming the transfer.

Ensure that node B has remote.enable = true. On node A:

> remote << new RemoteFileGetReq(to: 31, filename: 'f1', localFilename: 'f2', resume: false)
AGREE

This will initiate the transfer. The transfer will take a couple of minutes (110 seconds, as shown below), and then you will see:

caddy >> RemoteFileNtf:INFORM[from:31 filename:scripts/f2 transferDuration:109.72]

Since we explicitly requested that the transfer should not be resumed, the whole file was downloaded. We can check the file size to confirm that we have a 10 kB file and verify its contents on node A:

> ls
f2 [10 kB]
f3 [9 kB]
README.md [759 bytes]
> file('f2').bytes == x
true

Now let’s repeat the download, but this time allowing it to be resumed:

> remote << new RemoteFileGetReq(to: 31, filename: 'f1', localFilename: 'f3')
AGREE
caddy >> RemoteFileNtf:INFORM[from:31 filename:scripts/f3 transferDuration:13.936]

We see that the transfer completes in just 14 seconds! We can again check the file size to confirm that we have a 10 kB file and verify its contents on node A:

> ls
f2 [10 kB]
f3 [10 kB]
README.md [759 bytes]
> file('f3').bytes == x
true