Skip to content

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.

Producer configuration
# $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
Listener configuration
# $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):

sen run config/11_shapes/1_shapes_listener.yaml

In another terminal (let's call it B):

sen run config/11_shapes/1_shapes_producer.yaml

In another terminal (let's call it C):

sen term

In terminal C, type:

open my.tutorial
my.tutorial.listener.startListeningTo "my.tutorial", null, null, null

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:

my.tutorial.listener.stopListening

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:

my.tutorial.listener.startListeningTo "my.tutorial", "green", null, null

Filtering by location

my.tutorial.listener.startListeningTo "my.tutorial", null, {"min": 0, "max": 20}, null

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):

sen run config/11_shapes/1_shapes_listener.yaml

In terminal B (producer with explorer GUI):

sen run config/11_shapes/2_shapes_producer_exp.yaml

Then use the explorer to open the my.tutorial bus and monitor the shapes.