The RANGING service measures the distance between nodes, and as a by-product provides clock synchronization between them. Both rely on the fact that sound travels through water at a known, finite speed (about 1500 m/s): if we can measure how long a signal takes to travel between two nodes, we can infer the distance between them.
UnetStack supports two ranging methods:
Two-way travel time (TWTT) ranging requires no prior synchronization. The local node interrogates a peer (using a RangeReq); the peer responds, and the local node measures the round-trip time. After subtracting the known turnaround delay at the peer, the one-way travel time — and hence the range — is computed. This is the method used by the range command.
One-way travel time (OWTT) ranging requires the two clocks to be synchronized. A node transmits a timestamped beacon (using a BeaconReq), and a receiver compares the arrival time against the timestamp to obtain the travel time directly. OWTT is more efficient (no round trip) and supports passive, one-to-many ranging.
The interrogation/response exchange also yields the offset between the two nodes’ physical-layer clocks, which can be used to synchronize them. Once synchronized, nodes can perform OWTT ranging and time-coordinated protocols. Per-peer synchronization state (validity, last sync time, clock offset, lifetime) is tracked in the node-specific parameters below. Because the interrogation and response can be configured as arbitrary acoustic signals, the service can also range to commercial-off-the-shelf (COTS) acoustic transponders that are not Unet nodes. The default ranging agent is accessed as ranging.
25.2 Messages
Agents providing this service honor the following requests:
A RangeReq is answered with an AGREE once the interrogation begins (REFUSE if the request is invalid). When the exchange completes, the measured range is reported to the requester as a RangeNtf (a copy is also published on the agent’s topic); if the peer does not respond, the requester receives a FailureNtf with the reason instead. Notifications generated by this service:
A node that hears a timestamped beacon (and has valid synchronization with its sender) computes the travel time passively, and the resulting RangeNtf is published on the agent’s topic. A node that is interrogated publishes an InterrogationNtf on its topic before responding — an agent may subscribe to these and, when automatic response is disabled, use a RespondReq to control whether and how the node responds (e.g. with a data payload piggybacked on the response). Peer location, when requested, is delivered on the peerLocation field of the RangeNtf rather than as a separate message.
We can see this directly from the shell. Sending the RangeReq ourselves yields a RangeNtf that carries both the measured range and — because TWTT exchanges timestamps in both directions — the offset between the two nodes’ clocks:
Under the hood, node A transmits a timestamped interrogation frame; node B replies with a timestamped response after a known turnaround delay. From the four timestamps involved (transmit and receive at each end), the agent solves simultaneously for the one-way travel time (the range) and the clock offset between the nodes. The range needs no prior synchronization — that is the appeal of TWTT — and the offset it produces is what makes synchronization, and subsequently OWTT ranging, possible.
25.6.2 Synchronization
Each TWTT range measurement leaves the interrogating node with an estimate of its clock offset to the peer. (The peer, having only responded, does not itself learn the offset — synchronization is directional.) The agent stores this per-peer state, which we can inspect through the node-specific parameters:
The offset and lastSync (the epoch time of the last measurement) have been recorded, but sync reads false. That is because synchronization has a finite validity: the sync flag is true only while lastSync + lifetime lies in the future, and the lifetime here is still 0. Clocks drift, so an offset measured long ago is no longer trustworthy. Setting a suitable lifetime (in seconds) tells the agent how long to consider the synchronization valid:
NoteEpoch time
Times like lastSync are Unix epoch times — milliseconds elapsed since 00:00:00 UTC on 1 January 1970, the convention most computer clocks share. To read one as a date on the shell, convert it with java.time.Instant:
With a one-hour lifetime, the recent measurement now counts as valid synchronization.
NoteChoosing a lifetime
The right lifetime depends on how fast the nodes’ clocks drift relative to the range accuracy you need. Concretely, it is the time for the accumulated clock drift — scaled by the speed of sound — to grow beyond your acceptable ranging error. Modems with ordinary crystal oscillators may only hold synchronization to within a useful bound for minutes to tens of minutes; modems with temperature-compensated (TCXO) or oven-controlled (OCXO) oscillators can hold it for hours or days. Set the lifetime conservatively for your hardware, and re-range whenever synchronization lapses.
25.6.3 One-way travel time ranging
Once a node is synchronized to a peer, it no longer needs a round trip: a single timestamped transmission from the peer is enough to measure range. Let node A subscribe to its ranging agent, and have node B transmit a beacon (equivalent to ranging << new BeaconReq()):
> beaconAGREE
Because node A already has valid synchronization to node B, it computes the range from the single beacon and publishes a RangeNtf on its ranging topic (note the absence of an offset field — this is a one-way measurement):
OWTT is efficient and scales naturally: one beacon can be ranged by every synchronized listener in earshot, making it well suited to passive, one-to-many localization. In fact, any timestamped frame serves as a beacon — a node that transmits application data with phy << new TxFrameReq(timestamped: true, data: ...) lets synchronized receivers range on it while they decode the data, so ranging can ride along on ordinary traffic at no extra channel cost.
25.6.4 Per-node configuration
Most ranging parameters have an agent-wide default and a per-node override, set through the indexed ranging[address] parameters. This lets each link be tuned independently — a different channel, turnaround delay, maximum range, synchronization lifetime, or interrogation/response signal for each peer. For example, ranging uses the robust CONTROL channel by default; to range to node B over the higher-rate DATA channel instead, we override the channel just for that link:
> ranging[host('B')].channel = DATA2
An unset per-node parameter falls back to the agent-wide default, so you only need to specify the ones that differ for a given peer. The per-node signal parameters (ipreamble, isignal, rpreamble, rsignal) are what make ranging to non-Unet transponders possible (see Section 25.6.7).
25.6.5 Reporting peer location
A range on its own gives distance but not direction. If the peer knows its own location (from its NODE_INFO service, Chapter 14), we can ask it to report that location as part of the ranging exchange by setting requestLocation on the RangeReq. The peer’s location comes back on the peerLocation field of the RangeNtf:
Here node B reports its location as [600, 0, -15], which — combined with the measured range — lets node A place its peer. Because the response now has to carry the encoded location, requestLocation needs the room of a DATA frame; that is why we switched node B’s link to the DATA channel above. A node can also volunteer its location without being asked, by setting txLocation on a BeaconReq, so that passive listeners receive both a range and the beacon’s location.
25.6.6 Controlling interrogation responses
By default (respond = true), the ranging agent answers interrogations automatically — this is what let node A range node B above without any action on B’s part. The response is still visible: when interrogated, a node publishes an InterrogationNtf on its topic. Subscribing to node B’s ranging agent and ranging it from A shows this:
The responded flag is true, indicating the agent has already sent the automatic response. Sometimes, though, we want an application to decide whether and how to respond — for example, to attach sensor data to the reply, or to answer only selected interrogators. Setting respond = false disables the automatic response, so the agent emits the InterrogationNtf and then waits:
Now responded is false: node B has not replied, and node A’s interrogation will simply time out unless something on node B responds. An agent does that by handling the InterrogationNtf and issuing a RespondReq — optionally with a data payload that is piggybacked on the response frame and delivered to the interrogator on the data field of its RangeNtf:
voidstartup(){ subscribe agentForService(org.arl.unet.Services.RANGING)}voidprocessMessage(Message msg){if(msg instanceof InterrogationNtf &&!msg.responded){// decide whether to answer, and with what payloaddef reply =newRespondReq(msg)// copies to/type/rxStartTime from the interrogation reply.data =sensorReading()// optional piggybacked payloadagentForService(org.arl.unet.Services.RANGING)<< reply}}
A response must be sent promptly — before the interrogator’s turnaround window closes — so the handler should be quick and non-blocking.
25.6.7 Ranging to transponders
The interrogation and response need not be Unet frames at all. Because the per-node signal parameters let us specify an arbitrary acoustic interrogation signal to transmit and an arbitrary response signal to expect, a Unet modem can range to a plain acoustic transponder. Suppose we have a COTS transponder that replies to a 22 kHz, 2 ms pulse with a 30 kHz, 2 ms pulse, 30 ms later. We welcome it into our network as a guest node — say node T with host('T') = 152 — and describe the link to it:
> T = host('T')152> ranging[T].isignal = cw(22.kHz,2.ms); // interrogation pulse to transmit> ranging[T].rsignal = cw(30.kHz,2.ms); // response pulse to expect> ranging[T].rdelay =30.ms; // transponder turnaround delay
The 22.kHz and 2.ms notation is Groovy unit sugar, converting the written values to UnetStack’s preferred units — Hz and seconds (see Chapter 34). With the link configured, ranging to the transponder is then just:
> range T235.7
The modem transmits the 22 kHz interrogation pulse, listens for the 30 kHz reply after the 30 ms turnaround, and converts the measured travel time into a range. The threshold parameter (0–1) sets how strong the matched-filter detection of the reply must be to count. This signal-based ranging needs a real modem (with the BASEBAND service) to generate and detect the pulses; it is not reproduced by the simulator.
The reverse is just as easy: a Unet node is already a transponder to other Unet nodes — with respond = true it answers their interrogations automatically, as we saw above. To make a node reply with a specific acoustic signature instead of a Unet frame — so that a third-party interrogator expecting a particular pulse can range to it — configure the response signal and turnaround delay it should use:
Between the interrogator-side configuration (isignal/rsignal/rdelay) and the responder-side configuration (rsignal/rdelay, with respond/RespondReq), the ranging agent can interoperate with a wide range of acoustic ranging equipment.
25.7 Implementation
25.7.1 Ranging (ranging)
Class
Services
Capabilities
Availability
Ranging
RANGING
—
default stack
The default implementation is the ranging agent, loaded as part of the default stack (Chapter 13).
25.7.1.1 How it works
Ranging exploits the finite, known speed of sound in water: if we can measure how long a signal takes to travel between two nodes, we can infer the distance. In two-way travel time (TWTT) ranging, the local node sends a timestamped interrogation and the peer replies after a known turnaround delay; from the four timestamps involved (transmit and receive at each end) the agent computes both the one-way travel time — and hence the range — and the offset between the two nodes’ clocks. One-way travel time (OWTT) ranging instead uses a single timestamped beacon and requires the clocks to already be synchronized, which makes it more efficient and able to range to many listeners at once.
The clock offset obtained during TWTT is what makes OWTT and time-coordinated protocols possible. Per-peer synchronization state is tracked separately for each peer: the measured offset, the lastSync time it was obtained, and a configurable lifetime. Synchronization is considered valid — and OWTT ranging permitted — only while lastSync + lifetime remains in the future, so the lifetime should be set to reflect how long the modem’s clock stays accurate enough. The sound speed used to convert travel times to distances comes from the NODE_INFO service (Chapter 14).
The interrogation and response can each be an ordinary Unet frame, a bare preamble, or an arbitrary baseband signal. This flexibility, exposed through the per-node ipreamble/isignal/rpreamble/rsignal parameters, is what allows ranging to COTS acoustic transponders that are not Unet nodes. When signals are used, the agent relies on the BASEBAND service (Chapter 27) to transmit and detect them, matched-filtering the reply against the expected signal at the configured threshold. It can also use the MAC service (Chapter 19) to schedule its transmissions so they do not collide with other traffic.
25.7.1.2 Usage notes
For OWTT ranging and time-coordinated protocols, range to the peer first to establish synchronization, and set a lifetime appropriate to your modem’s clock stability.
Synchronization is directional: TWTT synchronizes the interrogating node to the peer, not the other way around. If both nodes need synchronization, each should range the other.
Requesting peer location (requestLocation) adds the encoded location to the response frame; use the DATA channel for that link so the frame has room.
Disable MAC scheduling (set the MAC parameter to 'none') on quiet links where channel contention is not a concern. ```