Usage
The loop process creates the segment and the receiver attaches to it. Both sides then use the same handles: the command ring, one event ring per hardware channel, one snapshot slot per channel, and the loop-wide nav ring with its snapshot.
The examples on this page use a heap-backed segment (nothing for the path), so they run in one process on any platform. Across two processes, the loop process calls create_segment(path, config) with a file path, conventionally under /dev/shm on Linux, and the receiver calls attach_segment with the same path.
The constants for event, command and status kinds (EVENT_RECORD, COMMAND_ARM, STATUS_ARMED, ...) are not exported. Qualify them with the module name.
Each band's entry also carries its intermediate frequency: where a signal at zero Doppler sits in the band's samples, including any fixed offset the front end's tuning leaves. Every carrier Doppler in the protocol is relative to it. The loop process writes the band table; the receiver reads it with band_table, for example to acquire around the right frequency.
using HardwareLoopProtocol
const HLP = HardwareLoopProtocol
config = SegmentConfig(; channel_count = 2, bands = [BandEntry(:L1, 4e6; intermediate_frequency_hz = -83.0)])
seg = create_segment(nothing, config)
band_table(seg)1-element Vector{BandEntry}:
BandEntry(FixedName((0x4c, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)), 4.0e6, -83.0, 1, 1, (0x0000000000000000, 0x0000000000000000))Commands: receiver → loop
The receiver sends commands with try_publish!. It returns false if the command ring is full, so a command is never lost silently. Each command carries a sequence number, which the loop echoes in its acknowledgement.
commands = command_ring(seg)
arm = ArmCommand(;
signal = :GPSL1CA,
prn = 7,
carrier_doppler_hz = 1500.0,
code_doppler_hz = 1.5,
code_phase_chips = 100.5,
valid_at_sample = 40_000,
tap_sample_shifts = (-2, 0, 2),
num_taps = 3,
sampling_freq_hz = 4e6,
)
try_publish!(commands, CommandTag(HLP.COMMAND_ARM, 1, 1), arm)trueThe loop process reads the command with peek! and payload, then consumes it with commit!:
status, view, lost = peek!(commands, CommandTag)
if status !== :empty && view.tag.kind == HLP.COMMAND_ARM
received = payload(ArmCommand, commands, view)
commit!(commands, view)
(Symbol(received.signal), received.prn)
end(:GPSL1CA, 7)Events: loop → receiver
The loop process writes events to a channel's event ring with publish!. It never blocks: if the receiver falls behind, the oldest events are overwritten.
events = event_ring(seg, 1)
publish!(events, EventTag(HLP.EVENT_STATUS, 1, 40_000; prn = 7),
StatusEvent(HLP.STATUS_ARMED, HLP.REJECT_NONE, 40_000, 1))
for k = 1:3
publish!(events, EventTag(HLP.EVENT_RECORD, 1, 40_000 + 4000k; prn = 7),
RecordEvent(1.0 + 0im, 4000, 1, 0, 0.0, 1500.0, 1.5))
end
ring_available(events)4The receiver drains the ring. status is :lost when events were overwritten before it read them; lost then says how many.
while true
status, view, lost = peek!(events, EventTag)
status === :empty && break
status === :lost && println("lost $lost events")
if view.tag.kind == HLP.EVENT_STATUS
ack = payload(StatusEvent, events, view)
println("status $(ack.code) for command $(ack.sequence)")
elseif view.tag.kind == HLP.EVENT_RECORD
record = payload(RecordEvent, events, view)
println("record ending at sample $(view.tag.device_sample): $(record.prompt)")
end
commit!(events, view)
end┌ Warning: Assignment to `status` in soft scope is ambiguous because a global variable by the same name exists: `status` will be treated as a new local. Disambiguate by using `local status` to suppress this warning or `global status` to assign to the existing global variable.
└ @ usage.md:89
┌ Warning: Assignment to `view` in soft scope is ambiguous because a global variable by the same name exists: `view` will be treated as a new local. Disambiguate by using `local view` to suppress this warning or `global view` to assign to the existing global variable.
└ @ usage.md:89
┌ Warning: Assignment to `lost` in soft scope is ambiguous because a global variable by the same name exists: `lost` will be treated as a new local. Disambiguate by using `local lost` to suppress this warning or `global lost` to assign to the existing global variable.
└ @ usage.md:89
status 1 for command 1
record ending at sample 44000: 1.0 + 0.0im
record ending at sample 48000: 1.0 + 0.0im
record ending at sample 52000: 1.0 + 0.0imSnapshots
A reader that only needs the newest epoch state of a channel can read its snapshot slot instead of draining the event ring.
slot = snapshot_slot(seg, 1)
state = EpochStateEvent(1500.0, 1.5, 123.5, 0.25, 2e4, 1499.0, 1.49, 48_000, 13, 0, 1, 0)
write_snapshot!(slot, EventTag(HLP.EVENT_EPOCH_STATE, 1, 44_000; prn = 7), state)
tag, newest = read_snapshot(slot)
newest.carrier_doppler_hz1500.0Navigation
A loop process that runs vector tracking writes NAV_VECTOR into the header with set_navigation_mode! before it marks itself running, and publishes every navigation cycle on the loop-wide nav_ring: one NavSatelliteEvent per satellite the cycle knows, then the NavSolutionEvent, which commits the cycle. The tags carry channel 0 and the cycle's epoch. publish_nav_solution! also writes the solution into the nav snapshot, which read_nav_snapshot reads. A loop in NAV_NONE (the default) publishes nothing there; the receiver decodes the bits and solves the position itself.
set_navigation_mode!(seg, HLP.NAV_VECTOR)
epoch = 400_000
publish!(nav_ring(seg), EventTag(HLP.EVENT_NAV_SATELLITE, 0, epoch; prn = 7),
NavSatelliteEvent(1, :GPSL1CA, (2.0e7, 1.0e7, 1.2e7), 0.072, 1.5, -0.1, 45.0, 1,
HLP.NAV_SAT_TRACKED | HLP.NAV_SAT_IN_SOLUTION, HLP.NAV_NOT_RELEASED))
solution = NavSolutionEvent(1, (3.9e6, 3.0e5, 5.0e6), (0.0, 0.0, 0.0), 12.5, 1e-7,
7_000_000, 0.25, 4.5, 1.5, 0.0, (2.0f0, 1.8f0, 1.2f0, 1.0f0, 0.7f0),
Int32(5), Int32(6), HLP.NAV_RUNNING | HLP.NAV_SEEDED | HLP.NAV_VALID)
publish_nav_solution!(seg, EventTag(HLP.EVENT_NAV_SOLUTION, 0, epoch), solution)
navigation_mode(seg), read_nav_snapshot(seg)[2].position_ecef_m(0x01, (3.9e6, 300000.0, 5.0e6))Liveness
Each side stamps its heartbeat regularly. The other side checks it with heartbeat_alive.
loop_heartbeat!(seg)
heartbeat_alive(seg, :loop), heartbeat_alive(seg, :receiver)(true, false)close(seg)