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 subscriptions 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.
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 = "edu.byu.enMotion.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:attr("rs_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 (wrangler renames it rs_attrs
for some reason).
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 (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:
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 edu.byu.enMotion.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 additonal 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.