Retina · Rust · evaluation release
Retina DDS for Rust
DDS is more than publish and subscribe. It combines a data-centric programming model with state for multiple objects, policy-driven matching, bounded histories, discovery, repair, filtering, lifecycle, and status events.
DDS programming models
DDS core
DCPS defines how data is addressed, published, retained, and observed.
Data-centric publish/subscribe
Writers publish application data to topics; readers receive it without naming or connecting to a specific peer.
Participant → Topic<T> → DataWriter<T> / DataReader<T>
Multiple objects on one topic
A field such as a robot ID identifies each object, so DDS can maintain history, ownership, durability, and lifecycle separately for every robot.
write_instance · take_instance · dispose · unregister
State access and event-driven processing
Applications can inspect retained state with read/take operations or react to data and communication-status changes through listeners.
read · take · listener · communication status
Grouped and coherent changes
Publishers and subscribers establish a policy boundary; coherent sets keep readers from observing only part of a related update.
Publisher · Subscriber · begin/end coherent changes
Models built on DDS
Request/reply
A request and its reply are ordinary DDS samples connected by related-sample identity and conventionally paired topics.
request writer + reply reader + correlation identity
ROS 2 execution
Publishers, subscriptions, clients, services, events, and wait sets are mapped onto DDS through the ROS middleware boundary.
retina-rmw
Retina · Rust
The Retina implementation
Retina exposes Rust DCPS entities with explicit QoS builders and capability crates for discovery, reliability, serialization, shared memory, persistence, ROS integration, and security.
use retina_core::{dds_type, Participant};
const DOMAIN_ID: u32 = 0;
const TOPIC_NAME: &str = "sensors/temperature";
const MEASURED_CELSIUS: f32 = 23.5;
dds_type! {
#[derive(Clone, Copy, Debug, PartialEq)]
struct Temperature {
degrees_celsius: f32,
}
}
let participant = Participant::new(DOMAIN_ID);
let topic = participant.topic::<Temperature>(TOPIC_NAME);
let writer = participant.create_writer(&topic)?;
let reader = participant.create_reader(&topic)?;
writer.write(&Temperature { degrees_celsius: MEASURED_CELSIUS })?;
let sample = reader.take()?;
Participant::new does not start a UDP
service. ROS is not required. Retina uses Result values, and
take returns Ok(None) when no sample is available.
Endpoints unregister automatically on drop.
close remains available when the
caller needs to observe cleanup errors.
Retina · data and state
Rust feature APIs and algorithms
DCPS entities
Participant::new(domain) · participant.topic::<T>(name) · participant.create_writer(&topic) · participant.create_reader(&topic)
RAII endpoint leasing. Participant-owned leases preserve identity and remove registrations on close or drop.
Local data flow and grouped entity creation with Rust ownership governing lifetime.
let topic = participant.topic::<Temperature>("temperature");
let writer = participant.create_writer(&topic)?;
let reader = participant.create_reader(&topic)?;
Types and bounded serialization
dds_type! { ... } · value.serialize() · writer.write_fixed(&value, &mut storage)
CDR serialization with deterministic type identity. Fixed-size types can serialize into caller-owned storage.
write serializes into owned bytes. write_fixed reuses caller storage and avoids serialization allocation; it does not promise that history, key storage, or transport performs no allocation. The buffer can be reused after the call returns.
use retina_core::FixedCdrType;
dds_type! { struct Temperature { value: f32 } }
let mut storage = [0_u8; Temperature::MAX_SERIALIZED_SIZE];
writer.write_fixed(&sample, &mut storage)?;
Topic instances
writer.write_instance(&value, key), reader.take_instance(key)
Bounded keyed-history selection. The reader selects and compacts the first matching key; retention and ownership state remain per key.
A topic can represent many independently retained logical objects.
writer.write_instance(&state, b"robot-17")?;
let sample = reader.take_instance(b"robot-17")?;
QoS compatibility and partitions
QosProfile::builder().reliable().partition(name).ownership(kind, strength).build()
DDS requested/offered compatibility. Policy ordering and bounded partition wildcard matching admit only compatible endpoints.
Policy choices become matching contracts rather than application-side peer checks.
let qos = QosProfile::builder()
.reliable()
.partition("control/*")
.build();
History and resource limits
QosProfile::builder().keep_last(depth).max_samples(limit).resource_limits(limits).build()
Bounded KEEP_LAST/KEEP_ALL admission. Admission evicts under KEEP_LAST or rejects overflow under KEEP_ALL, including per-instance limits.
Readers and writers carry explicit storage bounds.
let qos = QosProfile::builder()
.keep_last(8)
.max_samples(64)
.build();
Content filters
reader.filter_by(|value| ...) · reader.set_content_filter(filter) · reader.clear_content_filter() · ContentFilter::on_payload(predicate)
Pre-admission content filtering. Deserialized or payload predicates run before reader-history admission and reject malformed data.
Readers receive only the subset they requested and can replace the predicate at runtime.
reader.filter_by(|value: &Temperature| {
value.degrees_celsius > 80.0
})?;
reader.clear_content_filter()?;
Durability
QosProfile::builder().durability(DurabilityKind::TransientLocal).build() · DurabilityService::open(path) · participant.attach_durability_service(service)
Bounded durability replay. Per-writer and per-key retention preserves original sequence metadata; service policy governs checkpoints.
Late joiners recover retained state under the configured history policy.
let qos = QosProfile::builder()
.durability(DurabilityKind::TransientLocal)
.keep_last(8)
.build();
Time, liveliness, ownership, and order
QosProfile::builder().deadline(duration).lifespan(duration).liveliness(kind, lease).ownership(kind, strength).destination_order(order).build()
Lease expiry, strongest-writer arbitration, and source-order selection. Policy runs at admission or take within configured bounds.
Freshness, authority, activity, and observation order are expressed as DDS policy.
let qos = QosProfile::builder()
.deadline(Duration::from_millis(10))
.ownership(OwnershipKind::Exclusive, 100)
.destination_order(DestinationOrderKind::BySourceTimestamp)
.build();
Coherent changes
publisher.begin_coherent_changes() · publisher.end_coherent_changes() · publisher.discard_coherent_changes()
Publisher-scoped staged commit. Each publisher owns an independent set; readers requesting coherent access receive it only at commit.
Local and shared-memory delivery preserves the set. Network coherent-set identity is not claimed here.
publisher.begin_coherent_changes()?;
trajectory_writer.write(&trajectory)?;
limits_writer.write(&limits)?;
publisher.end_coherent_changes()?;
let qos = QosProfile::builder()
.reliable()
.keep_last(32)
.max_samples(128)
.deadline(Duration::from_millis(10))
.partition("control/*")
.build();
Retina · network and runtime
Rust delivery mechanisms
Discovery and active runtime
RuntimeParticipant::<T>::build(...) · participant.discover(...) · matched.metrics()
SPDP lease discovery with SEDP requested/offered endpoint matching. Participant leases and endpoint records build a bounded matched path.
RuntimeParticipant owns the local DDS endpoints; discover consumes it and drives discovery over the supplied transport until matching succeeds or fails. Entity creation through retina_core::Participant alone does not run network discovery.
let local = RuntimeParticipant::<Temperature>::build(
identity, "temperature/out", "temperature/in", qos)?;
let matched = local.discover(
&discovery, &transport, timeout)?;
Reliability and fragmentation
TypedReliableSession::new(...) · session.write(&value) · session.take_until(deadline) · session.metrics()
RTPS stateful reliable writer/reader repair. DATA/DATA_FRAG, HEARTBEAT/ACKNACK, NACK_FRAG, GAP, and bounded reassembly implement recovery.
Reliable data exchange and explicit protocol metrics under loss and reordering.
let mut session = TypedReliableSession::new(
&transport, &mut matched, config);
session.write(&sample)?;
let received = session.take_until(deadline)?;
Wire-level RTPS
parse_rtps_message(bytes) · submessages.next() · write_rtps_message_to(header, submessage, out)
Zero-copy bounded submessage parsing. Borrowed views validate RTPS submessages without taking payload ownership.
Protocol tools and runtimes can work at the RTPS layer without importing the DCPS entity model.
let (header, submessages) = parse_rtps_message(&packet)?;
for submessage in submessages {
handle(header, submessage?);
}
Implicit same-host transport
writer.write(&value)
Per-reader transport selection. Eligible same-host readers receive a pool descriptor; remote readers receive RTPS DATA or DATA_FRAG.
Automatic selection belongs to a configured runtime with eligible SHM peers and UDP routes. The core Participant and a bare write call do not create a daemon attachment or network service. Native loans and complete RTPS-over-SHM are distinct transport paths.
writer.write(&sample)?;
// The same call serves local SHM and remote UDP readers.
Explicit transport composition
UdpStaticPeer::bind(config) · transport.send_submessage(bytes, &mut packet) · transport.receive_message(&mut packet)
Strategy-based transport composition. Static-peer routing and sync/async traits feed the same RTPS state machines.
Test harnesses and specialized runtimes can choose transport composition explicitly.
let config = StaticPeerConfig::new(local, remote, header);
let transport = UdpStaticPeer::bind(config)?;
transport.send_submessage(bytes, &mut packet)?;
Composed DDS Security runtime
ParticipantAuthenticationDiscovery::bind(...) · security.poll(now) · security.poll_and_admit(...) · security.remote_writer_installation(writer)
Bounded PKI-DH handshake state machine. Transcript verification, permissions decisions, and session transitions fail closed.
The service owns its UDP transport, discovery buffers, peer associations, and installed endpoint keys while borrowing credentials. Despite its name, it also coordinates cryptographic activation and protected traffic. poll_and_admit updates the supplied DDS graph; poll alone has no graph collaborator. The service must remain active for expiry and repair processing.
Use the installed endpoint's protection path for complete records. Parsing a security message, finding a GUID, or authenticating a participant alone is not authorization to deliver a command.
// security and policy were constructed from the deployment credentials.
let progress = security.poll_and_admit(
now, authorization_time, &mut participant, &policy)?;
// Inspect progress and endpoint readiness before sending user data.
TypedReliableSession::new(transport, matched_participant, config)
session.write(value) -> Result<u64>
session.take_until(deadline) -> Result<T>
session.finish_until(deadline) -> Result<()>
session.metrics() -> ReliableMetrics
API contracts
Ownership, lifetimes, and results
- Participant and endpoints
retina_core::Participantshares participant state through reference-counted ownership and a mutex. Endpoint leases keep that state alive. Dropping an endpoint unregisters it;close(self) -> Result<()>consumes the endpoint and lets the caller observe cleanup errors.- Value access
DataReader<T>::take()returnsResult<Option<TypedSample<T>>>. An error, an empty history, and a returned sample are different outcomes.readleaves the selected entry in history;takeremoves it. Neither return value is a loan into arbitrary application memory.- Caller storage
DataWriter<T>::write_fixedrequiresT: FixedCdrTypeand borrows a mutable byte slice for serialization during the call.writeinstead obtains owned serialized bytes. Both returnResult<bool>; inspect the boolean as well as the error. Neither is a remote execution receipt.- Serialized loans
- The lower-level
SerializedDataReader::take_loaned(&mut participant)returnsResult<Option<LoanedSerializedSample<'participant>>>. Its lifetime borrows the participant. Dropping a taken loan releases retained or SHM ownership; deserialization is a separate operation.
Zero-copy and allocation-free describe a particular operation and storage configuration, not the entire library. Creating entities, introducing a new instance key, retaining history, and growing serialization storage have their own costs. A shared-memory loan is different from returning an owned value. The history algorithm reference explains the key-index tradeoffs.
Execution and transport
Who makes communication progress?
retina_core::Participant supplies local DDS entities.
retina_runtime::RuntimeParticipant<T> supplies a runtime
participant that can discover peers through a supplied transport. Its
discover operation consumes the local participant and returns a
MatchedRuntimeParticipant<T> on success.
retina_runtime::TypedReliableSession borrows the transport and
mutably borrows that matched participant. write(&value) returns
a sequence in ReliableResult<u64>;
take_until(deadline) returns ReliableResult<T>;
finish_until(deadline) returns ReliableResult<()>.
These calls drive the session; construction does not spawn an independent
worker. Its reusable serialization buffer can grow when larger samples arrive.
Automatic SHM/UDP selection requires a runtime that supports both, compatible same-host attachment, and matched reader capabilities. A shared domain ID is not sufficient. Native SHM loans carry slot descriptors; complete RTPS-over-SHM carries wire records. Both must honor DDS admission and ownership, but they do different work. See the delivery paths.
Dependencies
Choose the modules your application uses
- Core DDS
retina-coresupplies the entity model;retina-runtimeand transport crates supply network services.- Security
retina-securitysupplies security mechanisms,retina-security-opensslthe OpenSSL provider, andretina-security-runtimethe composed participant service.- Teleoperation
retina-teleopsupplies command and robot policy.retina-teleop-securitybinds that policy to authenticated DDS endpoint installations and application approval.
Core DDS can operate without either optional module. Security can protect ordinary DDS data without Teleoperation. Teleoperation policy can be used without Security, but then it does not establish an authenticated remote operator. Linking a module does not activate a security policy or grant control. ROS remains a separate adapter, not a prerequisite for these APIs. These are code dependency names, not a claim that every binary package has been qualified for every target.
Teleoperation contract
Robot control sessions and command permits
Import RobotControlSession, RobotCommandPermit, and
RobotWindowCommand<Payload> from retina_teleop.
RobotControlSession::new(profile, lease_config) returns
Option<Self>; invalid lease configuration returns
None. The profile type is
retina_runtime::product_config::ProductConfig. The session is not
cloneable. Operations below require
&mut self and return Result values.
Construction creates an empty session; it does not grant a lease. The owner holds operator authority, permit deadlines, and the last accepted sequence. It owns no socket, encryption key, sensor, or actuator. Serialize calls to this one owner across UDP and SHM ingress.
apply(request, now)- Returns an authority change.
Inspect its
resultand lease: a successful function return can still report a denied authority request. Renewal preserves existing permit deadlines; successful handover, revocation, and expiry discard grants. issue(nonce, observation, observed_at, now)- Returns a permit or a specific rejection. Supply a fresh unpredictable challenge and a robot observation timestamp. The robot stores the deadline locally; the permit contains no controller deadline.
admit(permit, sequence, now)- Returns success only for the exact live permit and a newer sequence while the operator lease remains live. Failure does not consume the sequence. This is admission, not delivery acknowledgement or actuator completion.
advance(now)- Checks idle expiry and returns an optional expired lease inside the operation result. Call it during idle service turns as well as handling commands. Active operations repeat the checks, so late timer dispatch cannot extend authority.
retire()- Permanently stops the owner. It has no reset operation and returns no success value. Restart creates a fresh owner with fresh challenges, not restored permits.
Time and rejection results
The product configuration supplies teleop_command_window_ms,
teleop_observation_max_age_ms, teleop_window_refresh_ms,
and teleop_max_active_windows. Configure these together rather
than adding per-call timeout literals. These tuning defaults are not clinical
safety limits.
Every now and observed_at is a monotonic timestamp
in nanoseconds from the same robot elapsed-time clock. No controller clock
comparison is used. The effective deadline is the earlier of the command
window and observation-age limit; arrival at the deadline is rejected.
Lease duration is configured separately. Clock regression retires the session.
RobotControlSessionError distinguishes Retired,
ClockRegressed, NoControlLease, and
Window(CommandWindowError).
Window errors distinguish unknown or expired permits, repeated or out-of-order
sequences, unavailable observations, refresh spacing, capacity, and arithmetic
limits. Treat these as policy results, not interchangeable network timeouts.
Binding the session to DDS Security
retina_teleop_security::SecuredRobotControlSession::new(security,
authorization, profile, lease) returns
Result<Self, SecuredRobotControlSessionError>.
Its security argument borrows ParticipantAuthenticationDiscovery.
This owner requires an approved operator bound to an installed DDS Writer.
Pass the current security service and operator authorization to each
apply, issue, admit, and
advance call. Explicit handover additionally requires
the target operator's approval. Losing or replacing the current binding
retires the owner; an invalid target does not replace a still-valid binding.
Authenticate the complete command through its bound endpoint before admission. The wrapper checks the binding; it does not decrypt arbitrary input or turn an operator ID into authorization. Application approval is separate from DDS topic permissions. The application still supplies qualified clocks, sensor provenance, randomness, scheduling, and the safe actuator response. See the deadline algorithm and its cost.