Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

After completing this lesson you will be able to do the following:

  • Use KRL to program a pico to serve as the "back end" for a single page application (SPA)

  • Use AJAX in the SPA to maintain state information in the pico

  • Use AJAX in the SPA to query for the state of the pico

Prerequisites

You should have completed the following lessons

Also, read the page on Creating an Echo Server, which has another example of a single page application, created "Using HTML and JQuery."

Contents

Table of Contents
maxLevel2

Single Page Application

In this lesson, we will build a very simple single page application which maintains start and finish times. Each measurement or timing is identified by a number (with a leading "N" or "n") and a name.

...

You may use your favorite tools for building this front-end. Or you may simply copy and paste the HTML and JavaScript shown below for an Angular app.

Angular HTML

true
Code Block
linenumbers
<!doctype html>
<html>
<head>
  <title>Timing</title>
  <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
  <script src="timing.js"></script>
  <style type="text/css">
    span.finished { cursor: pointer; color: blue; text-decoration: underline; }
  </style>
</head>
<body ng-app="timing" ng-controller="MainCtrl">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-header"> 
  <h1>Timing</h1>
</div>
  <form ng-submit="addTiming()" style = "margin-top:30px;">
    <input type="text" ng-model="number" placeholder="number" autofocus></input>
    <input type="text" ng-model="name" placeholder="name"></input>
    <button type="submit" focus-input>timing started</button>
  </form>
  <div ng-repeat="timing in timings | orderBy: '-ordinal'">
    {{timing.number}} &middot;
    {{timing.name}} &middot;
    {{timing.time_out}} &middot;
    {{timing.time_in}}
    <span class="finished" ng-click="finished(timing.number)" ng-if="!timing.time_in">timing finished</span>
    <span ng-if="!!timing.time_in">&middot; {{timeDiff(timing)}}</span>
  </div>
</div>
</div>
</body>
</html>

...

Additionally, the code obtains the value for eci in line 17, and makes the initial call to the function getAll in line 44.

linenumbers
Code Block
true
angular.module('timing', [])
.directive('focusInput', function($timeout) {
  return {
    link: function(scope, element, attrs) {
      element.bind('click', function() {
        $timeout(function() {
          element.parent().find('input')[0].focus();
        });
      });
    }
  };
})
.controller('MainCtrl', [
  '$scope','$http','$window',
  function($scope,$http,$window){
    $scope.timings = [];
    $scope.eci = $window.location.search.substring(1);

    var bURL = '/sky/event/'+$scope.eci+'/eid/timing/started';
    $scope.addTiming = function() {
      var pURL = bURL + "?number=" + $scope.number + "&name=" + $scope.name;
      return $http.post(pURL).success(function(data){
        $scope.getAll();
        $scope.number='';
        $scope.name='';
      });
    };

    var iURL = '/sky/event/'+$scope.eci+'/eid/timing/finished';
    $scope.finished = function(number) {
      var pURL = iURL + "?number=" + number;
      return $http.post(pURL).success(function(data){
        $scope.getAll();
      });
    };

    var gURL = '/sky/cloud/'+$scope.eci+'/timing_tracker/entries';
    $scope.getAll = function() {
      return $http.get(gURL).success(function(data){
        angular.copy(data, $scope.timings);
      });
    };

    $scope.getAll();

    $scope.timeDiff = function(timing) {
      var bgn_sec = Math.round(Date.parse(timing.time_out)/1000);
      var end_sec = Math.round(Date.parse(timing.time_in)/1000);
      var sec_num = end_sec - bgn_sec;
      var hours   = Math.floor(sec_num / 3600);
      var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
      var seconds = sec_num - (hours * 3600) - (minutes * 60);
  
      if (hours   < 10) {hours   = "0"+hours;}
      if (minutes < 10) {minutes = "0"+minutes;}
      if (seconds < 10) {seconds = "0"+seconds;}
      return hours+':'+minutes+':'+seconds;
    }
  }
]);

...

Once you have set up your pico engine, you can create a pico using the My Picos page "About" tab of the developer UI.Install the timing_tracker ruleset (described in detail below). Enter this URL to its

...

After clicking the "Add" button, you will see a link to the new pico.

...

Click on that link and visit the Timing Pico's "Rulesets" tab. There you will install the timing_tracker ruleset (described in detail below). Enter this URL to its source code: https://raw.githubusercontent.com/Picolab/pico_lessons/master/spa/timing_tracker.krl

Image Removed

You will probably also find it informative to install the pre-registered logging ruleset, and enable logging in the "Logging" tab.

...

The final step in preparing your pico for use as the "back end" of the single page application is to create a channel for each timekeeper. Do this in the "Channels" tab.Image Removed

...

Once the channels have been created, copy a channel id (also known as an event channel identifier, or ECI) and use it to complete the URL for a timekeeper.Image Removed

...

The Angular JavaScript code obtains the ECI from the query string of its initial URL. For this example, the URL used by timekeeper one would be 

Code Block
http://localhost:8080/timing.html?cj1zim4tk0002fvddkrce3cs1ckcz14k5j000zyknld68x3ft6

and similarly for other timekeepers. Having a separate ECI for each timekeeper allows you to revoke access to the SPA for one timekeeper while allowing others to continue using it.

The Pico Ruleset

The ruleset shares (line 3) the function entries (defined in lines 6-8), which simply returns the values of an entity variable named ent:entries which will be maintained as a KRL map.

Since the function might be called by our SPA (or some other way) before it contains any entries we make sure it defaults to an empty map (line 7).

The rule timing_first_use defined in lines 10-16 will be selected every time the pico receives either the timing:started event or the timing:finished event. If ent:entries has a map, the rule will "fire" and preform the empty action (i.e. do nothing), and otherwise the notfired postlude will initialize ent:entries to an empty map. This rule will fire before any other rule which selects on either of those events because it appears first, lexically, in the ruleset.

Rule timing_started (lines 17-30) selects on event domain timing and type started when there is an event attribute number that matches the regular expression of line 18 (the letter "N" (or "n") followed by any number of leading zeros and at least one other digit). It extracts the digits of number as a number into the value named ordinal_string. In the rule prelude, a normalized key is built from the ordinal value. If there is already a timing with that key, the rule fires, performing no action. Otherwise, in its notfired postlude, the rule adds an entry to the map in ent:entries which names a map containing the data for this timing. Note that the element names in this map exactly match those expected by the Angular HTML code.

linenumbers
Code Block
true
ruleset timing_tracker {
  meta {
    shares entries
  }
  global {
    entries = function() {
      ent:timings.defaultsTo({}).values()
    }
  }
  rule timing_first_use {
    select when timing started or timing finished
    if ent:timings then noop()
    notfired {
      ent:timings := {}
    }
  }
  rule timing_started {
    select when timing started number re#n0*(\d+)#i setting(ordinal_string)
    pre {
      key = "N" + ordinal_string
    }
    if ent:timings >< key then noop()
    notfired {
      ent:timings{key} := {
        "ordinal": ordinal_string.as("Number"),
        "number": event:attr("number"),
        "name": event:attr("name"),
        "time_out": time:now() }
    }
  }
  rule timing_finished {
    select when timing finished number re#n0*(\d+)#i setting(ordinal_string)
    pre {
      key = "N" + ordinal_string
    }
    if ent:timings >< key then noop()
    fired {
      ent:timings{[key,"time_in"]} := time:now()
    }
  }
}

Rule timing_finished (lines 31-40) selects when the event with domain timing and type finished occurs, provided it has an attribute number matching the regular expression in line 32. If it does, the rule is selected with ordinal_string set to the number. In  In the rule prelude, a normalized key is built from the ordinal value. If there is already a timing with that key, the rule fires, performing no action, and in its fired postlude sets postlude sets (to the current time) an attribute time_in on the map for that timing.

In the "RulesetsTesting" tab for the pico, you can check the box beside the timing_tracker ruleset to labelled "entries" to see its entity variable. The value might look something like this (lightly edited for readability):

Code Block
ent:timings=
{"N1":[
  {"ordinal":1,"number":"n1","name":"Nick Angell","time_out":"2017-04-26T22:35:48.446Z","time_in":"2017-04-26T22:39:47.998Z"},
 "N2": {"ordinal":2,"number":"n2","name":"Connor Grimm","time_out":"2017-04-26T22:36:12.665Z"}
}]