Tinverse logo Tinverse LLC

Tin DDS · C++ · evaluation release

Tin DDS for C++

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.

rmw_tindds_cpp
Tin DDS exposes related-sample primitives used to compose request/reply, but does not advertise a standalone DDS-RPC client/service facade. The catalog below states its actual public surface.

Tin DDS · C++

The Tin DDS implementation

Tin DDS exposes DDS entities for application types and serialized data, explicit publisher and subscriber groups, listener-driven status handling, and low-level composition points where an application needs direct control.

constexpr tin::dds::domain_id DOMAIN_ID{ 0U };
constexpr char TOPIC_NAME[]{ "sensors/temperature" };

struct temperature {
  static constexpr std::string_view tin_dds_type_name{
    "sensors.Temperature" };
  float degrees_celsius{};
};

tin::dds::domain_participant participant{ DOMAIN_ID };
auto topic = participant.create_topic<temperature>(TOPIC_NAME);
auto writer = participant.create_writer(topic);
auto reader = participant.create_reader(topic);
Writer and reader creation return std::expected; topic creation returns a topic value. Check endpoint creation before dereferencing the result. This example creates local DDS entities, not a running network service. The type supplies its stable DDS name; application code does not provide a type hash. ROS is not required.

Tin DDS · data and state

C++ feature APIs and algorithms

DCPS entities and groups

participant.create_topic<T>(name) · participant.create_writer(topic) · participant.create_reader(topic) · create_publisher(participant, qos) · create_subscriber(participant, qos)

Indexed endpoint matching. Tin DDS indexes readers and writers by topic and type, then connects pairs whose QoS is compatible. Removing an endpoint or one of its parent groups also removes its matches.

A participant joins a DDS domain. A topic names the data being exchanged; writers publish samples and readers receive them. Publisher and subscriber groups let related endpoints share policy and coherent-update boundaries.

auto topic = participant.create_topic<Temperature>("temperature");
auto writer = participant.create_writer(topic);
auto reader = participant.create_reader(topic);

Type identity

T::tin_dds_type_name · tin::dds::describe_type<T>() · topic.type()

Type comparison during discovery. Tin DDS attaches the declared type identity to the topic and advertises it with each endpoint. A writer and reader match only when their advertised types are compatible.

The C++ class name is local to one program, so it cannot identify data across processes. The stable DDS type name gives both processes the same wire-level identity.

struct Temperature {
  static constexpr std::string_view tin_dds_type_name{
    "sensors.Temperature" };
  float degrees_celsius{};
};

auto type = tin::dds::describe_type<Temperature>();
auto topic = participant.create_topic<Temperature>(
  "sensors/temperature");
assert(type.name == "sensors.Temperature");
assert(topic.type().name == type.name);

Read, take, and batches

reader.read() · reader.take() · reader.read(batch) · reader.take(batch)

Bounded reader-history selection. Read preserves a history entry; take removes it. Rings and descriptor ownership keep access finite.

Use read when later calls should still see the sample. Use take when the sample has been consumed. Batch calls apply the same choice to several caller-owned slots at once.

auto observed = reader->read(); // remains in history
auto consumed = reader->take(); // removed from history

std::array<tin::dds::sample_slot<Temperature>, 8> batch{};
auto const count = reader->take(batch);

Topic instances and lifecycle

writer.write(value, key) · reader.take_instance(key) · writer.dispose_instance(key) · writer.unregister_instance(key) · reader.instance_status(key)

Key-indexed object lifecycle. Bounded per-key history scans and a lifecycle table preserve state independently for each object.

One topic can carry many robots or sensors, identified by a key such as a robot ID. DDS keeps separate history and lifecycle state for each object. A reader can select one object or observe when it is disposed or unregistered.

writer->write(state, "robot-17");
auto sample = reader->take_instance("robot-17");
writer->dispose_instance("robot-17");

QoS matching and partitions

offered.compatibility_with(requested) · participant.create_writer(topic, qos) · participant.create_reader(topic, qos)

DDS requested/offered compatibility. Admission runs before graph mutation; partition matching uses bounded exact and */? wildcard comparisons.

A writer offers delivery behavior and a reader requests what it needs. They match only when those policies are compatible. Partitions divide one domain into named groups without changing topic names.

tin::dds::qos_profile offered{};
offered.reliability = tin::dds::reliability_kind::reliable;
offered.partitions = { "control/*" };
auto result = offered.compatibility_with(requested);

History and resource limits

participant.create_writer(topic, qos) · participant.create_reader(topic, qos)

Bounded KEEP_LAST/KEEP_ALL admission. KEEP_LAST evicts the oldest entry; KEEP_ALL rejects admission at its configured limit.

KEEP_LAST drops the oldest sample when its depth is full. KEEP_ALL retains samples until an explicit resource limit is reached, then rejects new admission. These limits make memory use and backlog a deployment choice.

qos.history = tin::dds::history_kind::keep_last;
qos.depth = 8U;
qos.resource_limits.max_samples = 64;
qos.resource_limits.max_samples_per_instance = 8;

Content-filtered data

reader.set_filter(predicate) · reader.clear_filter() · subscriber.create_serialized_datareader(name, type, qos, validator)

Pre-admission content filtering. The predicate runs before reader-history admission on local, shared-memory, and RTPS ingress paths.

The reader's predicate runs before a sample enters its history. Samples that do not match use no reader-history space. The application can replace or clear the filter without recreating the reader.

reader->set_filter([](Temperature const& value) {
  return value.degrees_celsius > 80.0F;
});
reader->clear_filter();

Durability and late joiners

participant.create_writer(topic, qos) · durability.start() · durability.checkpoint() · durability.close()

Bounded durability replay with atomic checkpointing. Writer retention preserves original sequence metadata.

Durability lets a reader receive state written before it joined. Transient-local history lasts while its writer is running; the persistent service can recover retained state after a restart. Follow the late-joiner example.

qos.durability = tin::dds::durability_kind::persistent;
qos.durability_service.history_depth = 8;
durability.start();
durability.checkpoint();

Time, liveliness, ownership, and order

participant.create_writer(topic, qos) · writer.refresh_deadline_status() · writer.refresh_liveliness_status() · reader.take()

Lease expiry, strongest-writer arbitration, and source-order selection. Each policy uses bounded status or history scans.

Deadline states how often data is expected, and liveliness detects an inactive writer. Exclusive ownership chooses the strongest eligible writer. Destination or source order determines how readers arrange accepted samples.

qos.deadline = {
  std::chrono::milliseconds{ 10 }
};
qos.liveliness = tin::dds::liveliness_kind::manual_by_topic;
qos.ownership = tin::dds::ownership_kind::exclusive;
qos.ownership_strength = 100;
DDS + RTPS

Presentation and coherent changes

publisher.begin_coherent_changes() · publisher.end_coherent_changes() · publisher.cancel_coherent_changes()

Staged coherent-set commit. Samples are released by publisher scope only when the complete indexed set is available; incomplete remote sets remain hidden.

begin and end mark several related writes as one update. A reader requesting coherent access sees the complete set, not an intermediate state, whether the samples arrive locally or over RTPS.

publisher.begin_coherent_changes();
trajectory_writer.write(trajectory);
limits_writer.write(limits);
publisher.end_coherent_changes();

Listeners and communication status

reader.set_listener(listener) · writer.set_listener(listener) · reader.refresh_matched_status() · reader.refresh_deadline_status()

Delta-based status propagation. Trackers compare cumulative counters with the last observed baseline and dispatch changes through the entity hierarchy.

A listener lets DDS notify the application instead of requiring it to poll continuously. Separate statuses report new data, endpoint matches, loss, rejection, incompatible QoS, missed deadlines, and liveliness changes.

tin::dds::data_reader_listener listener{};
listener.data_available = [](tin::dds::sample_info const&) {
  schedule_reader_work();
};
reader->set_listener(std::move(listener));
Tin DDS request/reply

Request/reply correlation

reply_writer.write_with_related_sample(payload, sequence, request_guid, request_sequence) · reply_reader.take_related_sample(request_guid, request_sequence)

Related-sample identity correlation. A reply carries the requesting writer GUID and sequence number for bounded indexed lookup.

A reply records the writer and sequence number of its request. The requester can therefore find the matching reply even when many requests are in flight. The application still owns the request and reply topics and service behavior.

reply_writer.write_with_related_sample(
  reply_payload, reply_sequence,
  request_writer_guid, request_sequence);

Tin DDS · C++ durability

Configure retention and persistent recovery

A writer keeps a bounded history for readers that arrive later. Persistent durability saves that history to a checkpoint and restores it when the participant restarts.

tin::dds::qos_profile qos{};
qos.durability = tin::dds::durability_kind::persistent;
qos.history = tin::dds::history_kind::keep_last;
qos.depth = 8U;
qos.max_samples = 64U;
qos.durability_service.history = tin::dds::history_kind::keep_last;
qos.durability_service.history_depth = 8;
qos.durability_service.max_samples = 64;
qos.durability_service.max_instances = 16;
qos.durability_service.max_samples_per_instance = 8;

auto topic = participant.create_topic<RobotState>("robot/state", qos);
auto writer = participant.create_writer(topic, qos);

tin::dds::persistent_durability_service durability{
  participant,
  tin::dds::persistent_durability_service_options{
    .checkpoint_path = "state/domain.durability",
    .recover_on_start = true,
    .checkpoint_on_close = true,
  },
};

if (!durability.start()) {
  return EXIT_FAILURE;
}

if (!writer || !writer->write(state)) {
  return EXIT_FAILURE;
}

if (!durability.checkpoint()) {
  return EXIT_FAILURE;
}

Tin DDS · network and runtime

C++ delivery mechanisms

RTPS

Discovery

participant_service::connect(configuration) · service.progress() · writer.wait_for_reader(timeout) · reader.wait_for_writer(timeout)

SPDP lease discovery with SEDP requested/offered endpoint matching. Participant leases and endpoint announcements feed the indexed match graph.

Participants announce themselves, then announce the topics and QoS of their readers and writers. Compatible endpoints match automatically. When a participant stops renewing its lease, the runtime removes it and its endpoints.

// configuration, TOPIC_NAME and match_timeout come from the deployment.
auto service = tin::dds::participant_service::connect(configuration);
if (!service) {
  return EXIT_FAILURE;
}
auto topic = service->create_topic<Temperature>(TOPIC_NAME);
auto writer = service->create_writer(topic);
if (!writer || !writer->wait_for_reader(match_timeout)) {
  return EXIT_FAILURE;
}
RTPS

Reliability and fragmentation

participant.create_writer(topic, reliable_qos) · writer.write(sample) · reader.take()

RTPS stateful reliable writer/reader repair. HEARTBEAT/ACKNACK repairs sequence ranges; DATA_FRAG and NACK_FRAG drive selective fragment repair.

Best-effort delivery sends a sample without repairing loss. Reliable delivery retains bounded history and retransmits missing samples or fragments requested by readers. Duplicate and reordered packets do not become duplicate application samples.

qos.reliability = tin::dds::reliability_kind::reliable;
qos.history = tin::dds::history_kind::keep_last;
qos.depth = 32U;
writer->write(sample);
Transport selection

UDP and shared memory

writer.write(value) · runtime.poll_once()

Per-reader transport selection. Each match uses an eligible same-host descriptor path or RTPS/UDP; mixed fanout chooses independently.

In a service configured for both transports, eligible same-host matches use SHM and remote matches use RTPS/UDP. This needs an active runtime and compatible SHM attachment; constructing a core writer alone does not start either. The participant_service facade is specifically a static-peer UDP service.

writer->write(sample);
// Same-host reader: shared-memory descriptor
// Remote reader: RTPS DATA or DATA_FRAG
ROS 2 + shared memory

Loaned samples

evaluate_loan_support(request) · try_loan_sample(arena, request) · loan.construct() · loan.publish() · loan.cancel()

Bounded slot reservation with RAII cancellation. An eligible publisher reserves one shared-memory slot; an unfinished loan is returned automatically.

This is the separate ROS adapter's loan helper, not a method on every DDS writer. The arena must outlive its loans. Publishing commits the arena slot; the adapter still owns notification and delivery. Cancellation or destruction of an unfinished loan returns it. Do not modify a published sample through an old reference.

auto loan = tin::dds::ros2::try_loan_sample(arena, request);
if (loan) {
  auto& sample = loan->construct();
  sample.temperature = measured;
  loan->publish();
}
Teleoperation

Timestamp-based command admission

factory.create_command_writer<T>(topic) · factory.create_command_reader<T>(topic, admission) · authority.grant(session, now) · reader.adopt_authority(lease) · reader.take(now, clock_quality)

Exclusive authority with sequence-and-freshness admission. A monotonic lease selects one controller and its epoch; the reader then rejects commands for the wrong robot, session, or epoch as well as stale, duplicate, and out-of-order commands.

This mode checks synchronized command timestamps and explicit clock quality. It is separate from robot-owned command deadlines, which do not compare robot and controller clocks. Neither mode authenticates an operator by itself.

// Lease duration and clock readings come from the product configuration
// and the qualified clock providers, not from a sample command.
control_authority authority{
  control_authority_config{
    .robot = *robot,
    .lease_duration_ticks = lease_duration_ticks,
  }
};
auto grant = authority.grant(*session,
  monotonic_timestamp::from_ticks(now_ticks));
reader->adopt_authority(grant.lease);

auto command = reader->take(
  synchronized_timestamp::from_tai_nanoseconds(now_ns),
  clock_quality::synchronized);
tin::dds_security

Composed DDS Security runtime

participant_security_runtime::create(...) · security.progress(...) · security.authentication() · security.cryptography() · security.user_transport()

PKI-DH authentication state machine with ordered permissions evaluation. Bounded peer sessions and signed policies fail closed.

The composed owner connects authentication, permissions, key exchange, replay state, and protected user transport. A completed handshake alone does not establish an authorized, ready endpoint. Retain the borrowed participant, runtime, and credential-policy objects for its lifetime; keep servicing progress and route protected data through its transport.

// configuration contains the selected credential and protection policies.
auto security = tin::dds::security::participant_security_runtime::create(
  participant, runtime, local_identity,
  trust_store, session_limits, configuration);
if (!security) {
  return EXIT_FAILURE;
}

auto progress = security->progress(tick, now,
  discovery_buffer, user_buffer);
// Inspect progress and endpoint readiness before sending user data.

API contracts

Ownership, lifetimes, and results

Core endpoints
data_writer<T> and data_reader<T> refer to a domain_participant. Keep that participant alive until its endpoints are destroyed. The service facade's participant_data_writer<T> and participant_data_reader<T> instead share ownership of their runtime state and keep it alive.
Creation and access
create_topic<T> returns a topic value; create_writer and create_reader return std::expected. A reader's value-based take() returns std::optional<sample<T>>, not an expected. read leaves the entry queued; take removes it.
Writing and batches
write returns a boolean, not a remote execution receipt. A value-based operation may copy or encode the payload. Batch read and take fill a supplied std::span<sample_slot<T>> and return the number filled; supplying the output array does not remove all other allocation costs.
Loans
A reservation owns its slot until publication or cancellation. Keep the arena or segment alive for all loans. A published descriptor is not permission to keep mutating the payload. The ROS try_loan_sample helper shown above belongs to the optional adapter, not the core writer interface.

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?

Include tin/dds/participant_service.h for the static-peer UDP facade. participant_service::connect(configuration) returns std::expected<participant_service, participant_service_error>. It sets up the participant and transport; it does not start a background thread. Call service.progress() to advance discovery and protocol work.

Matching waits belong to the endpoints: writer.wait_for_reader(timeout) and reader.wait_for_writer(timeout) drive progress while waiting. reader.take() only checks history; reader.take_for(timeout) drives progress until a sample is available or the wait ends. writer.write_for(value, timeout) waits for pending fragmented transmission to finish, not for robot execution. This facade serves UDP; it is not an implicit SHM service.

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
Link tin::dds for the core DDS and runtime interfaces.
Security
Add tin::dds_security. The composed owner is declared in tin/dds/security/participant_security_runtime.h.
Teleoperation
Add tin::dds_teleop for robot policy. Add tin::dds_teleop_security for the security-bound robot-control owner.

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

Include tin/dds/teleop/robot_control_session.h for tin::dds::teleop::robot_control_session. Construct it from a product_configuration and control_authority_config. Invalid lease configuration makes subsequent operations fail with invalid_configuration. The owner cannot be copied. Operations below return std::expected values. robot_window_messages.h declares robot_command_permit and robot_window_command<Payload>; include robot_window_serialization.h for their CDR customizations.

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.

robot_control_session_error distinguishes robot_control_session_failure from command_window_error. Lifecycle failures include invalid configuration, retirement, clock regression, and a missing control lease. 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

tin::dds::teleop::secured_robot_control_session::create(security, authorization, profile, lease) returns an expected session or secured_robot_control_session_error. Its declaration is in tin/dds/teleop/secured_robot_control_session.h. Its security argument is a participant_crypto_runtime reference, available through the composed security owner's cryptography() accessor, not the authentication-only object. 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.

Tin DDS · C++ guides

Building DDS applications

Start with a topic, add state for multiple objects and reliable delivery, then use the same writer call across UDP and shared memory. The final guides authenticate a peer and admit teleoperation commands by authority, sequence, and freshness.

Tutorial 1 · about 20 minutes

Connect two processes through one topic

A participant is one application's membership in a DDS domain. It owns that application's topics, readers, writers, discovery state, histories, and status. Its runtime owns the sockets and protocol timers that connect those local objects to participants in other processes.

DDS feature

Participants, topics, and endpoint matching

Each process creates its own participant and topic object. DDS connects their endpoints when the domain, topic name, type identity, and QoS are compatible.

What exists in each process

Process A memory                       Process B memory
participant 1                         participant 2
├─ topic metadata                     ├─ topic metadata
├─ writer and writer history          ├─ reader and reader history
├─ discovered reader + match          ├─ discovered writer + match
└─ RTPS runtime + UDP sockets         └─ RTPS runtime + UDP sockets
          │                                      ▲
          └──────── CDR sample in RTPS DATA ─────┘

The two topic objects do not share an address or C++ lifetime. “The same topic” means that both processes independently declare the same distributed identity. SPDP discovers the participants; SEDP exchanges their writer and reader descriptions; RTPS carries the serialized sample after they match.

Share the data contract

Both executables include the same type definition and CDR mapping.

struct Temperature {
  static constexpr std::string_view tin_dds_type_name{
    "sensors.Temperature" };
  float degrees_celsius{};
};

template<typename Archive, typename Self>
  requires std::same_as<std::remove_cvref_t<Self>, Temperature>
bool tin_dds_serde(Archive& archive, Self& value) {
  return archive(value.degrees_celsius);
}

Process A: publish

auto config = tin::dds::static_peer_configuration::loopback(
  7U, 1U, 2U); // domain 7, this participant 1, peer 2
auto dds = tin::dds::participant_service::connect(*config);

auto topic = dds->create_topic<Temperature>(
  "sensors/temperature");
auto writer = dds->create_writer(topic);

if (!writer ||
    !writer->wait_for_reader(std::chrono::seconds{ 2 })) {
  return EXIT_FAILURE;
}

if (!writer->write_for(
      Temperature{ .degrees_celsius = 23.5F },
      std::chrono::milliseconds{ 100 })) {
  return EXIT_FAILURE;
}

Process B: receive

auto config = tin::dds::static_peer_configuration::loopback(
  7U, 2U, 1U); // domain 7, this participant 2, peer 1
auto dds = tin::dds::participant_service::connect(*config);

auto topic = dds->create_topic<Temperature>(
  "sensors/temperature");
auto reader = dds->create_reader(topic);

if (!reader ||
    !reader->wait_for_writer(std::chrono::seconds{ 2 })) {
  return EXIT_FAILURE;
}

auto sample = reader->take_for(
  std::chrono::milliseconds{ 100 });

What to verify

  • The participant IDs differ, while the domain, topic name, type identity, and QoS agree.
  • The writer and reader match without sharing a C++ object or memory address.
  • write_for() serializes and sends the sample; take_for() returns it from Process B's reader history.
  • Changing the domain, topic name, type identity, or QoS compatibility prevents the match.

Tutorial 2 · about 15 minutes

Represent many robots as instances of one topic

Use each robot's ID as its key on one topic. Configure a bounded transient-local history so a late reader receives recent state.

DDS feature

Topic instances and late-reader replay

History, ownership, durability, and lifecycle are maintained independently for each key.

tin::dds::qos_profile qos{};
qos.reliability = tin::dds::reliability_kind::reliable;
qos.durability = tin::dds::durability_kind::transient_local;
qos.history = tin::dds::history_kind::keep_last;
qos.depth = 8U;
qos.max_samples = 64U;

auto topic = participant.create_topic<RobotPose>("fleet/pose", qos);
auto writer = participant.create_writer(topic, qos);

writer->write(pose, "robot-17");

// This reader may be created after the write. Transient-local durability
// replays retained state from the still-running writer.
auto reader = participant.create_reader(topic, qos);
auto sample = reader->take_instance("robot-17");

What to verify

  • Another robot ID creates another independently managed object, not another topic.
  • take_instance() selects only the requested robot.
  • A late reader receives the retained sample with its original sequence metadata.

Tutorial 3 · about 20 minutes

Request reliable delivery

Reliable QoS makes delivery a writer-reader contract. The writer keeps bounded repair history while RTPS heartbeats and acknowledgements recover missing samples and fragments.

DDS feature

Reliable RTPS

HEARTBEAT and ACKNACK repair missing sequence ranges; NACK_FRAG requests only missing fragments.

tin::dds::qos_profile reliable{};
reliable.reliability = tin::dds::reliability_kind::reliable;
reliable.history = tin::dds::history_kind::keep_last;
reliable.depth = 32U;
reliable.max_samples = 32U;

auto topic = participant.create_topic<Temperature>(
  "sensors/temperature", reliable);
auto writer = participant.create_writer(topic, reliable);
auto reader = participant.create_reader(topic, reliable);

writer->write(Temperature{ .degrees_celsius = 23.5F });
auto sample = reader->take();

What to verify

  • A reliable writer satisfies a reader requesting reliable delivery.
  • A best-effort writer does not match that reader.
  • Repair remains bounded by the configured writer history.

Tutorial 4 · about 10 minutes

Use shared memory without changing publish code

The application writes to a DDS topic. Tin DDS selects an eligible same-host shared-memory path or RTPS/UDP for each matched reader.

This sketch assumes a service configured for both transports and an eligible shared-memory attachment. The static-peer participant_service used in Tutorial 1 serves UDP only; placing those two processes on one host does not turn it into a SHM service. See transport and progress contracts.

DDS feature

Location-transparent publish/subscribe

Transport selection belongs to the runtime, not to each application write.

if (!writer->write(sample)) {
  return EXIT_FAILURE;
}

// Same-host match  → shared-memory descriptor
// Remote match     → RTPS DATA or DATA_FRAG over UDP
// Mixed fanout     → one write, transport selected per reader

What to verify

  • The C++ call is identical for shared-memory and UDP readers.
  • An unavailable shared-memory path follows the configured policy: explicitly permitted UDP fallback or rejection, not an assumed fallback.
  • Type matching and reliability remain DDS concerns on either transport.

Tutorial 5 · about 20 minutes

Authenticate and authorize a DDS peer

Bind signed governance and permissions policy to the participant, run bounded PKI-DH handshake progress, and admit the remote identity before activating protected communication.

This is the authentication stage, not a complete encrypted data path. For protected application traffic, use the participant_security_runtime owner in the Security API reference. Authentication alone does not establish endpoint permissions or install every required key.

DDS feature

DDS Security participant authentication

Authentication proves peer identity; governance and permissions determine whether that identity may join the domain and use its endpoints.

using namespace tin::dds::security;

auto graph_admission = permissions_graph_admission::create(
  domain, signed_governance, signed_permissions);
graph_admission->set_authorization_time(authorization_time);

auto config = participant_authentication_runtime_configuration{}
  .with_graph_admission(*graph_admission)
  .with_remote_permissions_validator(remote_permissions);

auto security = participant_authentication_runtime::create(
  participant,
  *runtime,
  local_identity,
  trust_store,
  session_limits,
  config);

std::array<std::byte, 4096> discovery_buffer{};
std::array<std::byte, 4096> user_buffer{};

for (std::uint64_t tick{};
     !security->authenticated(peer_guid);
     ++tick) {
  security->progress(
    tick,
    participant_authentication_runtime::clock::now(),
    discovery_buffer,
    user_buffer);
}

What to verify

  • An untrusted certificate or invalid handshake never produces an authenticated peer.
  • Expired or unauthorized permissions fail before endpoints enter the match graph.
  • Handshake sessions, retries, and message buffers remain within configured limits.

Tutorial 6 · about 20 minutes

Admit a teleoperation command

Give one operator session control authority, attach the resulting lease to the command reader, and accept only commands from that session and epoch that are fresh and strictly ordered.

DDS feature

Exclusive authority and command admission

Typed DDS command envelopes combine an authority lease with robot, session, epoch, sequence, and synchronized-time checks.

using namespace tin::dds::teleop;

auto robot = robot_id::from_value(17U);
auto session = operator_session_id::from_value(42U);
auto sequence = command_sequence::from_value(1U);

endpoint_factory endpoints{ participant };
auto writer = endpoints.create_command_writer<MotionSetpoint>(
  "robot/motion");
auto reader = endpoints.create_command_reader<MotionSetpoint>(
  "robot/motion",
  command_admission_config{
    .robot = *robot,
    .freshness_limit_nanoseconds = 5'000'000U,
  });

control_authority authority{ control_authority_config{
  .robot = *robot,
  .lease_duration_ticks = 1'000U,
} };
auto grant = authority.grant(
  *session, monotonic_timestamp::from_ticks(10U));
reader->adopt_authority(grant.lease);

motion_command<MotionSetpoint> command{
  .robot = *robot,
  .session = *session,
  .epoch = grant.lease.epoch,
  .sequence = *sequence,
  .generated_at = synchronized_timestamp::from_tai_nanoseconds(1'000'000U),
  .requested_mode = operation_mode::enabled,
  .payload = setpoint,
};

writer->publish(command);
auto admitted = reader->take(
  synchronized_timestamp::from_tai_nanoseconds(2'000'000U),
  clock_quality::synchronized);
if (!admitted) {
  enter_safe_hold(admitted.error());
}

What to verify

  • A command from a session without the active lease is rejected.
  • Repeating the same sequence number is reported as duplicate or out of order.
  • A command outside the freshness limit is rejected before the application receives it.