This blog series is about an open source project "Script scheduler" I worked on recently, it allows to run scheduled and triggered scripts (jobs).
The series contains the following parts:
- Part 1 - Introduction and simple example "Periodic HelloSinusoid" (Displaying sinusoid data in the console based on a periodic interval)
- Part 2 - Advanced example: "Periodic YouLess" (IOT algorithm for creating Event Frames based on Watt Peak in an Energy Monitor device installed in our company building)
- Part 3 - Advanced example: "Triggered HelloSinusoid" (Writing PIWebAPI channel triggered Sinusoid data to CSV)
- Part 4 - Technical deep dive how everything sticks together, setup your own environment, add your own scripts, etc.
This project is created because as programmer I like to have full control when running scheduled or triggered analytics. So I would like to have something like AF Analytics but with all the power of a mature script / programming language.
It should be lightweight and run independently, easy and familiar to program, have code quality checks, debuggable and testable, and it should be possible to extend the code with lots of external libraries.
The creation process
I created a Typescript project for Node.js with a generator and after searching for scheduling things I picked an agenda package from npm as scheduling platform. (For persisting the jobs it depends on MongoDB). I had some troubles with getting everything to work (Typescript transpiler to ES6, etc) but within a couple of days I build a wrapper that was able to run multiple scripts from a folder with a interval configuration and an algorithm to run. After that I started to improve the code and made more use of Typescript features (Interfaces, Classes, Typings, Async / Await, Arrow functions), added a dependency injection framework so the ability to run unit test would be more easy and I would be able to create single instances of classes in multiple parts of the code. With this blog published just in time I started battling multiple problems to create a PIWebAPI client in Typescript for Node.js based on the Swagger file. (Basic Authentication and Certification rejection are examples of the problems) I wrote a small service as wrapper on top of this client so it would reuseable and easy to get some PI data. After that I wrote some samples and improved them over time. While scheduling was not a problem, I still needed a triggered based solution. So I started to get WebSockets to work as trigger mechanism that would execute an algorithm for every broadcast. I choose this approach with PIWebAPI channels in mind which I also like to get experience with. After trying out 4 different websocket libraries for Node.js I finally had enough experience to get it to work with the first one I tried and with Basic Authentication and Certification rejection working.
So I now have a small framework that is able to run scheduled and triggered Typescript algorithms with the PIWebAPI client as a service included. I really like the freedom I have to program anything I want and it really fits my experience as Web Developer.
Part 4 of this blog will have more detailed instructions how everything works and how to setup your environment. There is still a lot to improve but I think the current functionality is a good start. If you already want to dive and look into my code go to my GitHub Repository and see for yourself.
Periodic HelloSinusoid example
This simple Hello Sinusoid example just logs the actual value and timestamp of the PI Point Sinusoid to the console.
Below is the full Typescript code:
The actual code to run is just 2 lines (line 27 and 29 ) but a lot of other stuff is happening around it to be able to run this algorithm periodic.This will be explained in multiple steps:
1: The script starts with importing external dependencies (Classes, Interfaces, Modules) that are used in the code. (think of usings in C#):
- Lines 2-3 are required because this script will get injected into a periodic processor with dependency injection (see part 3) and the PIWebAPI service will be injected in this code so only a single instance will be created for all scripts.
- Line 5 is also required because this is a periodic job and the defined class should implement the interface.
- Line 6 is used because in this script we would like to use the PIWebAPI service for easy programmatic data access.
- Line 8 is just syntactic sugar so that the result of the service can have an explicit typing in line 27.
2: The class definition:
This class is exported so it can be used in other code. And it implements the IPeriodicJob interface that defines a config object and a run method. The class is decorated with @injectable() for dependency injection into the periodic processor.
3: The config object:
4: Inject the PIWebAPI service as attribute (also possible with constructor injection but this is cleaner):
A private attribute is defined with the required service interface as type and the @inject() decorator with a string key for what should be injected. All dependency injection bindings are setup in one other config file. The service attribute will have a instance of the PIWebAPIService and now all methods defined in the interface will be available to be used in this class.
5: The run function:
This is the run function that will be executed with the rule defined in the interval. It exposes parameters with information about the job which is not used in this case and a done method that should be called when the run function is asynchronous.
The result constant in line 27 had a type of TimedValue which is auto generated in the PIWebAPI client based on the Swagger.json file that is exposed in PIWebAPI 2017. But most of the code will also work for 2016R2: The TimedValue class looks like this:
In line 27 we call the getPIPointDataByPath method on the injected service that will use the provided path to get the actual point data.
In line 29 we simple log some attributes from result which TypeScript automatically detects as defined in the above class. This is done with the new backtick notation so that variables can be inserted in a string without concatenation.
To catch and log errors a try catch block is added around the async functions.
6: Let's see this in action after building the project, setting a breakpoint and running the Visual Studio Code debugger:
The screenshot is showing my mouse hover over the result. It show the result object with all the attributes and values it received from the service.
Below in the Debug Console the result of a previous job is shown. (The periodic processor is also adding information to the console around the output from the run function)
David Golverdingen @ Magion