Shapes example¶
Prerequisites: 4 - School (object discovery, subscriptions).
This example illustrates how to manage interests on objects using C++.
The idea is:
- You publish some shapes on a Sen bus.
- Shapes move around, emit events and change color.
- In another process, you instantiate a
ShapeListener. - Using a Sen shell, you tell the listener to start/stop listening to shapes using some criteria.
- You will see how the listener is automatically informed by Sen on the objects that match the given criteria.
Interface¶
This is the data model:
package shapes;
/// Centimeters encoded in 32-bit floats.
quantity<f32, cm> Centimeters;
/// Degrees encoded in 32-bit floats.
quantity<f32, deg> Degrees;
/// A color enumeration.
enum Color : u8 { red, green, blue }
// Specification of a triangle.
struct Triangle { side : Centimeters }
// Specification of a rectangle.
struct Rectangle { width : Centimeters, height : Centimeters }
// Specification of a circle.
struct Circle { radius : Centimeters }
// A 2-D 32 bit vector
struct Vec2 { x : Centimeters, y : Centimeters }
// The geometry of a shape.
variant Geometry { Triangle, Rectangle, Circle }
// The wall that we might collide with.
enum Wall : u8 { top, bottom, left, right }
// A shape.
class Shape
{
var geometry : Geometry [static]; // Shape's geometry.
var frozen : bool [writable]; // True if the shape should move.
var color : Color [writable]; // Shape's color.
var position : Vec2; // Shape's center.
// Emitted when the shape collides with a wall.
//
// @param wall the wall that we collided with.
event collidedWithWall(wall : Wall);
}
import "shapes.stl"
package shapes;
// An distance interval
struct DistanceInterval
{
min : Centimeters,
max : Centimeters
}
// A color or nothing
optional<Color> MaybeColor;
// An interval or nothing
optional<DistanceInterval> MaybeInterval;
// A list of names
sequence<string> NameList;
// Base class for listening to various shapes
class ShapeListener
{
// Number of shapes being tracked
var detectedShapesCount : u32;
// Number of active queries
var queryCount : u32;
// Start listening for shapes that fulfil certain conditions.
// Returns the name of the query that was created.
//
// @param bus the bus where to find the shapes.
// @param color if present, the color that the shape must have.
// @param xRange if present, the interval along the x axis where the shape should be.
// @param yRange if present, the interval along the y axis where the shape should be.
fn startListeningTo(bus : string, color : MaybeColor, xRange : MaybeInterval, yRange : MaybeInterval) -> string;
// Deletes a previously created query.
//
// @param queryName the name of the query (the one returned by calls to startListeningTo)
fn stopListeningTo(queryName : string);
// Deletes all previously created queries.
fn stopListening();
// Get the names of all the detected shapes
fn getAllDetectedShapes() -> NameList;
}
Implementation¶
The shape implementation you can find the shapes.cpp file defines the update() function. In it,
we just move it around, making it bounce in a box. The shape emits a collidedWithWall(wall) event
when it collides with a wall.
The more interesting part is in shape_listener.cpp. Let's see how the startListening function is
implemented:
std::string startListeningToImpl(const std::string& bus,
const MaybeColor& color,
const MaybeInterval& xRange,
const MaybeInterval& yRange) override
{
const auto queryName = makeQueryName(); // create a name for our query
// build and log the query string, then create the subscription
subscriptions_.emplace(queryName,
api_->selectFrom<ShapeInterface>(
bus,
buildQuery(queryName, bus, color, xRange, yRange),
[queryName, this](const auto& addedObjects) { shapesDetected(queryName, addedObjects); },
[queryName, this](const auto& removedObjects) { shapesGone(queryName, removedObjects); }));
// update the query count
setNextQueryCount(
sen::std_util::checkedConversion<uint32_t, sen::std_util::ReportPolicyIgnore>(subscriptions_.size()));
return queryName;
}
The buildQuery() function builds a Sen Query Language string adapted to the conditions defined by
the user, for example:
SELECT shapes.Shape FROM my.tutorial
WHERE position.x BETWEEN 1.0 AND 2.0 AND
position.y BETWEEN 0.0 AND 3.0 AND
color IN (green, blue)
The listener prints the shapes that is able to detect.
Subscription lifecycle¶
The ShapeListener manages two independent things, created at different moments and torn down separately.
The subscription to a bus, one per query. api_->selectFrom<ShapeInterface>(bus, query, onAdded,
onRemoved) returns a std::shared_ptr<Subscription<ShapeInterface>> holding an ObjectList, with
both callbacks already installed so that they also fire for shapes that were already there. The
listener keeps it in subscriptions_, keyed by query name. Erasing that entry destroys the
Subscription, which unregisters the list from the bus. Nothing else has to be kept alive:
onAdded and onRemoved are plain callbacks, and installing one replaces whatever was there
before.
The collision callback on each discovered shape, one per shape. shape->onCollidedWithWall(...)
returns a ConnectionGuard, which unregisters the callback when it is destroyed. These do have to
be kept alive, so the listener stores them in shapeGuards_, keyed by object id. Erasing a shape's
entry drops its guards and unregisters its callbacks.
How to run it¶
Let's define what we want to run in our Sen kernel.
# $schema: ../base/schema.json
include:
- ../base/ether.yaml
- ../base/term.yaml
build:
- name: shapes_producer
group: 4
freqHz: 5
imports: [shapes]
objects:
- class: shapes.ShapeImpl
name: blueCircle
bus: my.tutorial
color: blue
x: 50.0
y: 50.0
geometry:
type: Circle
value:
radius: 10
- class: shapes.ShapeImpl
name: redTriangle
bus: my.tutorial
color: red
x: 15.0
y: 22.0
geometry:
type: Triangle
value:
side: 4
- class: shapes.ShapeImpl
name: greenRectangle
bus: my.tutorial
color: green
x: 40.0
y: 40.0
geometry:
type: Rectangle
value:
width: 20
height: 10
# $schema: ../base/schema.json
include:
- ../base/ether.yaml
build:
- name: shapes_listener
group: 4
freqHz: 5
imports: [shapes]
objects:
- class: shapes.ShapeListenerImpl
name: listener
bus: my.tutorial
Listening to all shapes¶
In one terminal (let's call it A):
In another terminal (let's call it B):
In another terminal (let's call it C):
In terminal C, type:
In terminal A you will see:
- The query that was built for expressing the interest.
- The shapes that the listener is now detecting.
- Logs that are printed when the detected shapes emit some events.
If you now type the following in terminal C:
In terminal A now you see that we don't get any new notification.
Filtering by color¶
Now, try the same as before, but specify a color:
Filtering by location¶
In both dimensions:
my.tutorial.listener.startListeningTo "my.tutorial", null, {"min": 0, "max": 20}, {"min": 0, "max": 20}
Filtering by all criteria¶
my.tutorial.listener.startListeningTo "my.tutorial", "green", {"min": 0, "max": 20}, {"min": 0, "max": 20}
Using the explorer¶
You can replace the producer config with the explorer variant to visually monitor shape positions, property changes, and collision events in real time:
In terminal A (listener, unchanged):
In terminal B (producer with explorer GUI):
Then use the explorer to open the my.tutorial bus and monitor the shapes.