Collection picos

Compatibility

This page has not been adjusted for version 1.0 of the pico-engine

Having a pico that maintains a collection of other picos is a pattern that is often useful.

The best practice is to have the collection pico use a subscription to each of the other picos.

Elements of a collection ruleset

The strategy we recommend is to use a very simple ruleset whose only job is to manage the collection. Other rulesets can be installed in the collection pico to use the member picos in various ways. The collection ruleset provides access to the member picos, accepts new member picos into the collection, and removes all members before its own deletion. And that is all it does. 

Providing the member picos

Here is KRL code that could be used in a collection ruleset to provide the members of the collection to another ruleset.

  meta {
    use module io.picolabs.subscription alias Subs
    provides members
  }
  global {
    members = function(){
      Subs:established("Tx_role","member")
    }
  }

Line 2 uses the io.picolabs.subscription ruleset as a module. Line 6-8 define a function which computes all of the members, with each one represented by a subscription, while line 3 makes this function available to a more specialized ruleset (which will use this one as a module). Line 7 selects only those subscriptions where the other pico is playing the role of member.

Accepting a new member pico

Here is KRL code which will accept subscription requests from other picos which are becoming members of this collection. We will use subscription roles to identify the subscriptions of interest. Doing so allows the collection pico to participate in other kinds of subscriptions. The collection pico will play the role "collection" and the role "member" will be played by the picos we are collecting.

  rule auto_accept {
    select when wrangler inbound_pending_subscription_added
    pre {
      acceptable = event:attr("Rx_role")=="collection"
                && event:attr("Tx_role")=="member";
    }
    if acceptable then noop();
    fired {
      raise wrangler event "pending_subscription_approval"
        attributes event:attrs
    } else {
      raise wrangler event "inbound_rejection"
        attributes { "Rx": event:attr("Rx") }
    }
  }

This rule will be triggered by a request for a member pico to be added to the collection (one example of how this might be done is shown later). Lines 4 and 5 ensure that the subscription roles are as expected. Line 7 conditionally accepts (lines 9-10) or rejects (lines 12-13) the proposed subscription.

Cleanup

When the collection pico is no longer needed, it can be deleted. First, though, it needs to cancel all of the member subscriptions.

  rule delete_member_subscriptions {
    select when wrangler deletion_imminent
    foreach members() setting(subs)
    fired {
      raise wrangler event "subscription_cancellation"
        attributes { "Rx": subs{"Rx"}, "Tx": subs{"Tx"} };
      raise wrangler event "ready_for_deletion" on final;
    }
  }

When cleanup is needed, this rule will be selected. Line 3 gets all the member subscriptions and the rest of the rule loops over them, with each one in turn bound to the name subs. Lines 5-6 removes each subscription. Line 7, which is only evaluated when handling the last subscription, signals the collection pico's readiness for deletion.

made up events

Note that the events wrangler:deletion_imminent and wrangler:ready_for_deletion are fictional, in the sense that Wrangler does not raise the former event before a pico is deleted (whether manually or programmatically) and does not listen for the latter event. To use this rule, you would first send the first event (likely from the "Testing" tab) and then manually delete your collection pico.

Ruleset

The complete code for this ruleset can be found at https://raw.githubusercontent.com/Picolab/pico-engine/master/packages/pico-engine/krl/io.picolabs.collection.krl

Creating a collection pico

We will show the creation of a collection pico, and providing it with member picos, using this example setup. Here we see a pico named "enMotion ITB", which we will call the "building pico", with four and twenty child picos. Each child pico has a tag id (visible as the pico name (ex. "ITB1208")) and a room name (not visible here, but maintained by a ruleset in the building pico). We've laid out the building pico to leave room for some collection picos which we will be creating. Its child picos are laid out automatically beside it.

We add a special ruleset to the building pico just for the purpose of creating collection picos. This rulest can be installed in the pico when a collection is needed, do its work, and then be removed from the pico until another collection is needed.

Identify child picos to be collected

This KRL code will provide an array of picos which we want in a collection. For simplicity, we are going to use a prefix of a child pico's room name, but any logic could be used to identify them.

  meta {
    use module edu.byu.enMotion.building alias building
    shares __testing, by_prefix
  }
  global {
    __testing = { "queries": [ { "name": "__testing" },
                               { "name": "by_prefix", "args": [ "name_prefix" ] } ],
                  "events": [ {"domain":"grouper","type":"creation","attrs":["name_prefix"]}] }
    by_prefix = function(name_prefix) {
      prefix_re = ("^"+name_prefix).as("RegExp");
      building:dispenser_rooms().filter(function(v){v like prefix_re})
    }
    ...
  }

We'll use the primary ruleset of the building pico, with alias building, which provides a function dispenser_rooms. It computes a map from each child pico's tag_id to its room_name. We get the map in line 11, and return a map with only those entries whose room_name has the given name_prefix. This is done using the string operator like based on a regular expression built from the provided prefix.

Notice that we have allowed this function to be called from the "Testing" tab of the UI for convenience in seeing which child picos will be selected. This is done by sharing the function in line 3, and including it in the list of __testing queries (line 7).

Here we verify that we have the desired child picos.

Creating the collection pico

This KRL code will create the collection pico.

  rule collection_needed {
    select when grouper creation
    pre {
      name_prefix = event:attr("name_prefix");
      group_name = name_prefix + " Rooms";
      rids = "io.picolabs.collection;io.picolabs.subscription";
      child_specs = { "name": group_name, "name_prefix": name_prefix,
        "rids": rids, "color": "#002e5d" };
    }
    fired {
      raise wrangler event "new_child_request" attributes child_specs;
    }
  }

We will name the collection pico by the prefix, a space, and "Rooms" (and will change the name manually later – we could have instead added another event attribute for the desired name). Line 2 selects on a particular event domain and type (which are also available in the "Testing" tab because of an entry in events of __testing). Line 11 raises the event which will create the collection pico as a child of the building pico. The specific pico name, color, and rids are established in lines 5-8. We also include the name_prefix which is not required for the child creation, but will be needed in a later step.

Notice that we request that wrangler install two rulesets into the new pico (along with the rulesets which are installed into every pico). These are the collection ruleset described earlier, and the subscription ruleset. When the new child pico is completely ready, it will send us (its parent pico) the wrangler:child_initialized event.

Adding members to the collection

This KRL code will run when the new collection pico is ready.

  rule collection_subscriptions {
    select when wrangler child_initialized
    pre {
      eci = event:attr("eci");
      collection_eci = get_wellKnown_eci(eci);
    }
    if collection_eci then noop();
    fired {
      raise grouper event "need_subscriptions"
        attributes event:attrs().put("collection_eci",collection_eci);
    }
  }

This rule obtains the "well known" ECI for the collection pico (line 5) using a function get_wellKnown_eci shown below. It then passes that on to the next rule by raising an internal event (lines 9-10).

    get_wellKnown_eci = function(eci) {
      url = meta:host+"/sky/cloud/"+eci+"/io.picolabs.subscription/wellKnown_Rx";
      http:get(url.klog("url")){"content"}.decode(){"id"}
    }

We had specified that the io.picolabs.subscription ruleset be installed in the new collection pico, so we can now query it for its well known ECI, using http:get. The result of this function will be the desired ECI if all goes well, and null otherwise.

This KRL code will respond to the internal event, expecting the collection_eci and the child_specs given to wrangler earlier.

  rule collection_subscription_requests {
    select when grouper need_subscriptions
    foreach by_prefix(event:attr("name_prefix")) setting(room_name,tag_id)
    pre {
      eci = building:eci(tag_id);
      wellKnown_Tx = get_wellKnown_eci(eci).klog("wellKnown_Tx");
    }
    if wellKnown_Tx then every {
      event:send({"eci":eci,"domain":"wrangler","type":"subscription",
        "attrs": { "wellKnown_Tx": event:attr("collection_eci"),
          "Rx_role": "member", "Tx_role": "collection",
          "name": tag_id, "channel_type": "subscription" }
      })
    }
  }

This rule selects (line 2) on the internal event, and sets up a loop (line 3) over the desired member picos (produced by the by_prefix function). We get an ECI for each pico from the building pico (line 5). In lines 6 and 8 we ensure, for sanity, that the pico can do subscriptions. Finally (in lines 9-13) we send each soon-to-be member pico an event asking it to request a new subscription to the collection pico with the correct roles, and with the new channels created for the subscription named by the tag_id.

Remember that this rule is running in the building pico. The building pico thus introduces each of its prefix-matching child picos to the newly created collection pico.

This is how the new collection pico fits into things, after re-positioning it manually:

and after renaming manually (in the collection pico's "About" tab)

Repeating the action to create more collection picos give us a final configuration:

Ruleset

The complete code for this ruleset can be found at https://raw.githubusercontent.com/Picolab/pico_lessons/master/collections/edu.byu.enMotion.building.grouper.krl

Using a collection pico

Suppose we wanted to use one of the collection picos to collect some information from its member picos.

To do this we would install another ruleset in the collection pico. It would include code like the following KRL:

  meta {
    use module io.picolabs.collection alias collection
    shares some_function
  }
  global {
    some_function = function() {
      collection:members()// some further processing
    }
  }
  rule some_rule {
    select when something required
    foreach collection:members() setting(subs)
    // some action/postludes for each subs
  }

and specify additional processing in some_function (defined in lines 6-8) that it shares (in line 3) to endpoints, and/or specify rules like some_rule (defined in lines 10-14) that take action on the collection's members when certain events occur.

When a member is added

Since members of the collection are determined by subscriptions, your ruleset can listen for wrangler:subscription_added events to do something when a new member joins your collection. Since your pico might participate in other subscriptions, only those in which the roles are "collection" and "member" would be applicable.

  rule new_member {
    select when wrangler subscription_added
    pre {
      pertinent = event:attr("Rx_role")=="collection"
                && event:attr("Tx_role")=="member";
    }
    if pertinent then noop();
    fired {
      raise collection event "new_member" attributes event:attrs
    }
  }

The event attributes available are those surrounding a subscription in general. Some of these might be useful when a new member arrives into your collection

Useful event attributes

Tx is the event channel identifier (ECI) which your collection would use to send events (use event:send) or queries (use Wrangler:skyQuery) to the member pico

name may be useful as a human-readable identifier for the member pico

Id is a unique identifier of the subscription

When a member is removed

Your ruleset might wish to listen to the event wrangler:subscription_removed which will be raised when a subscription is removed from your pico. Such events which are applicable to membership in your collection could then be handled appropriately.

  rule new_member {
    select when wrangler subscription_removed
    pre {
      pertinent = event:attr("Rx_role")=="collection"
                && event:attr("Tx_role")=="member";
    }
    if pertinent then noop();
    fired {
      raise collection event "member_removed" attributes event:attrs
    }
  }

Copyright Picolabs | Licensed under Creative Commons.