Tinverse logo Tinverse LLC

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
These models do not imply a standalone DDS-RPC client/service facade or a standalone DCPS WaitSet API. The catalog below states Retina's actual public surface.

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()?;
This is local data flow; 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();
Local scope

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

retina-runtime

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)?;
retina-runtime

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)?;
retina-rtps

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?);
}
retina-shm

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.
Transport traits

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)?;
retina-security-runtime

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::Participant shares 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() returns Result<Option<TypedSample<T>>>. An error, an empty history, and a returned sample are different outcomes. read leaves the selected entry in history; take removes it. Neither return value is a loan into arbitrary application memory.
Caller storage
DataWriter<T>::write_fixed requires T: FixedCdrType and borrows a mutable byte slice for serialization during the call. write instead obtains owned serialized bytes. Both return Result<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) returns Result<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-core supplies the entity model; retina-runtime and transport crates supply network services.
Security
retina-security supplies security mechanisms, retina-security-openssl the OpenSSL provider, and retina-security-runtime the composed participant service.
Teleoperation
retina-teleop supplies command and robot policy. retina-teleop-security binds 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 result and 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.