Chapter 1: Getting your feet wet
Welcome! This interactive tutorial teaches you the basics of DDS and the Cyclone DDS Python backend. To give it a fun spin, we will use DDS to follow along on the journey of Captain Corsaro with his ship Cyclone.
All story fragments from the journal will be presented in boxes like the one above. Explanations and instructions are presented as normal text like this. Please enable "Live Code" by selecting the rocket in the top bar and then execute the code cell below to initialize your personal copy of Captain Corsaro's journal.
from questing import Journal
journal = Journal(seed=None)
print(journal.seed)
Sailing the DDS sea
DDS is a publish-subscribe based networking system that allows you to write applications that talk to eachother without worrying about shipping the bits and bytes around and retaining compatibility between platforms and programming languages. We will first explore the different entities central to a DDS system and learn how to create and use them in Python.
To join our captain on the DDS sea, or rather DDS Domain we use a DomainParticipant
. A DomainParticipant
is the central entity in any DDS application. The Domain itself is more of a virtual concept, not directly created but made up of all the participants on a network. You can have multiple domains running next to each other, identified by a domain id. They will remain completely separated.
Tasks:
- Import the
DomainParticipant
fromcyclonedds.domain
and instantiate it without arguments.- Pass the
DomainParticipant
toquest.check("domain-participant", participant)
quest = journal.quest("domain-participant")
quest.start()
# The import:
# Make participant
participant = ...
quest.check("domain-participant", participant)
quest.finish()
Click to show hint 1.
The import is `from cyclonedds.domain import DomainParticipant`Click to show hint 2.
The instantiation is `participant = DomainParticipant()`Click to show the solution.
quest = journal.quest("domain-participant")
quest.start()
from cyclonedds.domain import DomainParticipant
participant = DomainParticipant()
quest.check("domain-participant", participant)
quest.finish()
Remaining on-topic
In order for DDS applications to talk to each other they have to be talking about the same thing: the same Topic
. A Topic
in DDS consists of a name and a type. The types are usually defined using the Object Management Group Interface Definition Language, OMG IDL or just IDL for short. With the powerful introspection and duck-typing we don't have to rely on an IDL compiler to help us define these types, we can write Python classes directly and let Cyclone DDS Python directly generate the DDS necessities behind the scenes. If you want to learn IDL so you can interop with other languages there are other tutorials available.
You can turn Python classes into IDL structs by inheriting from IdlStruct
from cyclonedds.idl
and type hinting the class attributes, as if you are using python dataclasses. You can then implement an __init__
method or generate one by applying @dataclass
.
Create a datatype for the
CuriousFish
that has aFishType
(Shimmering, Matte or Metallic), an integer number of dorsal fins and a string name. Then importTopic
fromcyclonedds.topic
and create aTopic
namedfollowers
. ATopic
takes three arguments: aDomainParticipant
, a name and the datatype.Note: your variables persist between cells, you can use the participant from the previous quest! However, a time-out can occur, which will usually result in a DDS_PRECONDITION_NOT_MET error. Try to re-run previous cells to re-initialize variables if this happens.
Example datatype:
@dataclass
class LogbookEntry(IdlStruct):
timestamp: int
text: str
author: str
Tasks:
- Pass the
CuriousFish
datatype toquest.check("curious-fish", CuriousFish)
- Pass the
Topic
you created toquest.check("followers-topic", topic)
quest = journal.quest("remain-on-topic")
quest.start()
from dataclasses import dataclass
from cyclonedds.idl import IdlEnum, IdlStruct
class FishType(IdlEnum):
Shimmering = 0
Matte = 1
Metallic = 2
@dataclass
class CuriousFish(IdlStruct):
fish_type: FishType
# define dorsal_fins
# define fish_name
quest.check("curious-fish", CuriousFish)
# import
# create the topic
topic = ...
quest.check("followers-topic", topic)
quest.finish()
Click to show hint 1.
The fields are `dorsal_fins: int` and `fish_name: str`.Click to show hint 2.
The topic import is `from cyclonedds.topic import Topic`.Click to show hint 3.
The topic instantiation is `topic = Topic(participant, "followers", CuriousFish)`Click to show the solution.
quest = journal.quest("remain-on-topic")
quest.start()
from dataclasses import dataclass
from cyclonedds.idl import IdlEnum, IdlStruct
class FishType(IdlEnum):
Shimmering = 0
Matte = 1
Metallic = 2
@dataclass
class CuriousFish(IdlStruct):
fish_type: FishType
dorsal_fins: int
fish_name: str
quest.check("curious-fish", CuriousFish)
from cyclonedds.topic import Topic
topic = Topic(participant, "followers", CuriousFish)
quest.check("followers-topic", topic)
quest.finish()
Taken, a fishy story
We will now finally interact with the DDS system. By subscribing to the follower-fish
topic and taking
a sample we will discover what the fish our captain talked about actually looked like. This is done through Subscribers
and DataReaders
. We will disregard the Subscriber
for now and only work with a DataReader
. It has several reading and taking methods that allow you to receive data from the network. They are read
, take
, read_next
, take_next
, read_iter
, take_iter
, read_aiter
and take_aiter
. We will stick with a simple take
for now, which gives you a list of available samples. A sample is simply an instance of the datatype of the Topic
.
Create a
DataReader
, imported fromcyclonedds.sub
using the participant and the topic as arguments, then take a fish.Tasks:
- Pass the
DataReader
you created toquest.check("fish-reader", reader)
- Pass the
CuriousFish
you took toquest.check("freshly-caught", fish)
quest = journal.quest("a-fishy-story")
quest.start()
# The import
# Create the reader
reader = ...
quest.check("fish-reader", reader)
# Take the fish
fish = ...
print(fish)
quest.check("freshly-caught", fish)
quest.finish()
Click to show hint 1.
The `DataReader` import is `from cyclonedds.sub import DataReader`Click to show hint 2.
The `DataReader` instantiation is `reader = DataReader(participant, topic)`Click to show hint 3.
Taking a single fish from the reader is `fish = dr.take()[0]`Click to show the solution.
quest = journal.quest("a-fishy-story")
quest.start()
from cyclonedds.sub import DataReader
reader = DataReader(participant, topic)
quest.check("fish-reader", reader)
fish = reader.take()[0]
quest.check("freshly-caught", fish)
quest.finish()
Growing the fish supply
We now know how to take a sample but normally these samples don't appear out of thin air: something somewhere has to be writing them. This is done with Publishers
and DataWriters
, where we again will leave the Publisher
out for now. A DataWriter
is instantiated the exact same way as a DataReader
. A writer can write
a sample and dispose
a sample, or do both right after each other with write_dispose
.
Create a
DataWriter
, imported fromcyclonedds.pub
using the participant and the topic as arguments, then write aCuriousFish
.Tasks:
- Pass the
DataWriter
you created toquest.check("fish-writer", writer)
- Write a
CuriousFish
with parameters of your choosing.
quest = journal.quest("grow-the-fish-supply")
quest.start()
# The import
# Create the writer
writer = ...
quest.check("fish-writer", writer)
# Create a fish
fish = ...
# Write the fish
quest.finish()
Click to show hint 1.
The `DataWriter` import is `from cyclonedds.pub import DataWriter`.Click to show hint 2.
The `DataWriter` instantiation is `writer = DataWriter(participant, topic)`.Click to show hint 3.
Instantiating a fish can be done like this: `fish = CuriousFish(fish_type=FishType.Matte, dorsal_fins=6, fish_name="Harry")`.Click to show hint 4.
Writing a fish can be done like this: `writer.write(fish)`.Click to show the solution.
quest = journal.quest("grow-the-fish-supply")
quest.start()
from cyclonedds.pub import DataWriter
writer = DataWriter(participant, topic)
quest.check("fish-writer", writer)
fish = CuriousFish(fish_type=FishType.Matte, dorsal_fins=6, fish_name="Harry")
writer.write(fish)
quest.finish()
Land ahoy!
Let's put together what we learned so far. Create a new Island
datatype with an X and Y coordinate as floating points, a floating point Size and a string name. Then create a topic named DisposedAtolls
and write a new island within 10
near the center (0,0)
with a size between 1 and 10. Lastly create a reader and read all the samples.
Tasks:
- Create the
Island
datatype and pass it toquest.check("island", Island)
.- You will have to annotate the
Island.name
askey
. An example is provided.- Create the
DisposedAtolls
topic.- Create a
DataWriter
and write a central island. Then check the writer withquest.check("writer-written", writer)
- Create a
DataReader
and read all samples. Check your resulting set of samples withquest.check("the-disposed-atolls", islands)
Notes:
- You will have to create the reader before you write the new island sample to be able to receive it.
- You can use
reader.take(N=100)
N=100
means a maximum of 100 but doesn't block, it returns how many it has now.
quest = journal.quest("land-ahoy")
quest.start()
from cyclonedds.idl.annotations import key
# You will have to annotate the Island.name as key. Here is an example of how to do that
class Person(IdlStruct):
a: int
key('a')
@dataclass
class Island(IdlStruct):
pass
quest.check("island", Island)
participant = ...
topic = ...
writer = ...
reader = ...
# create central island
island = ...
# write central island
quest.check("writer-written", writer)
# read all islands
islands = ...
quest.check("the-disposed-atolls", islands)
quest.finish()
There are no hints for this quest, it is composed of familiar parts. Try to go back to previous quests for inspiration.
Click to show the solution.
from cyclonedds.idl.annotations import key
quest = journal.quest("land-ahoy")
quest.start()
@dataclass
class Island(IdlStruct):
X: float
Y: float
size: float
name: str
key('name')
quest.check("island", Island)
participant = DomainParticipant()
topic = Topic(participant, "DisposedAtolls", Island)
writer = DataWriter(participant, topic)
reader = DataReader(participant, topic)
# write central island
writer.write(Island(X=0.7, Y=-6.6, size=4, name="Dominio"))
quest.check("writer-written", writer)
# read all islands
islands = reader.take(N=100)
quest.check("the-disposed-atolls", islands)
quest.finish()
Well that covers some of the DDS basics, enough to write a simple application. There is much more to explore, you can continue to the next chapter to continue learning.