Skip to content

Shapes example

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 collidedWith(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 query = makeQueryName();                                // create a name for our query
    auto sub = std::make_shared<sen::Subscription<ShapeInterface>>();  // create up a subscription
    sub->source = api_->getSource(bus);                                // get the bus

    // install the callbacks
    std::ignore = sub->list.onAdded([query, this](const auto& iterators) { shapesDetected(query, iterators); });
    std::ignore = sub->list.onRemoved([query, this](const auto& iterators) { shapesGone(query, iterators); });

    auto interest = makeInterest(query, bus, color, xRange, yRange);  // build the interest.
    sub->source->addSubscriber(interest, &sub->list, true);           // connect the list.
    subscriptions_.emplace(query, std::move(sub));                    // save the subscription.

    // update the query count
    setNextQueryCount(subscriptions_.size());
    return query;
  }

The makeInterest() 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.

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/shell.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 shell

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}