Module: explorer



Introduction

The explorer module is a generic tree interface to access and edit any elements data/methods of the Weble system. By analogy it is similar to the filesystem of an operating system (Windows / Unix), except that each node (file) not only holds data but also has callable methods (api). Below stands a (simplified) schema of the Weble supervision structured as a tree.

                                                |
                                                |(http path : "http://supervision.weble.ch:8080")
                                                |(absolute path : "|")
                                                |
core modules  __________________________________|__________________________________________________________________________
             |                     |            |              |                       |                   
          gateways              loggers       logics       schedulers                trees(absolute path: "|trees")
    _________|_________            |          __|__            |              _________|___________
   |         |         |      mysqlDriver    |  |  |       myScheduler       |                     |     
modbus     knx1      knx2                   l1 l2 l3                        visu1                 visu2
   |   ______|______   |                                                  ___|___              ____|____
  ``` |      |      | ```                                                |       |            |         |
    1/1/0  1/1/1  1/1/2(absolute path: "|gateways|knx1|1/1/2")           e1      e2           e1        e2
                                                                    _____|____   
                                                                   |          |
                                                                  sub1      sub2(absolute path: "|trees|visu1|e2|sub2")
                                                                                (internal absolute path: "/el/sub2")
                                                                                (direct path: "|937ee8cb-52c4-48ea-ae53-a87999d1d372")


Paths

The seperator for the paths is the vertical bar character '|'. There is 4 types of paths:

  • Direct (UUID). Gives access to the node independently of its position in the tree through its UUID: '|937ee8cb-52c4-48ea-ae53-a87999d1d372'. The UUID is generated once at the node creation.
  • Absolute. It is the same idea as absolute paths in operating systems. Example: '|gateways|knx1|1/1/2'.
  • Http. To refer to external nodes the absolute path can be prefixed with the hostname and port of the webserver like an url : 'http://supervision.weble.ch:8080/trees|visu1|e2|sub2'. The first vertical bar of the absolute path ('|') is replaced by the http url 'http://supervision.weble.ch:8080/'. It also works with direct UUIDs: http://supervision.weble.ch:8080/937ee8cb-52c4-48ea-ae53-a87999d1d372
  • Relative. It is the same idea as relative paths in operating systems. The '..' is supported and designs the parent node. As an example, the '..|sub2' path relative to '|trees|visu1|e2|sub1' refers to its sibling node '|trees|visu1|e2|sub2'.

The tree, as the system is configured, can be extended with custom user tree structures. Those are included and managed by the core/trees module ('|trees|..'). It's main use is for the edition of custom client web interfaces (visualizations), but it could also be used to implement any functionality taking advantage of a (mutable) tree structure. Each child of the '|trees' module (in the schema above '|trees|visu1' and '|trees|visu2') can be viewed and managed as an independent sub-tree. In order to reflect this, the nodes encompassed in the custom sub-tree can use their own simplified internal path notation:

  • The seperator for the internal paths is the slash character '/' instead of the vertical bar '|'.
  • The internal root path of the sub-tree is '/'. In the schema above the node '|trees|visu1|e2|sub2' can refer to '|trees|visu1' by using the internal sub-tree path '/'.
  • Internal paths can also be relative or absolute.

A node does not only have a name, but also a numerical index. The node index is an integer ranging from 0 to n-1, where n is the total number of siblings (number of children in the parent node). The index is used to order the children in the parent node. Inside paths, it is possible to use the node index instead of its name. Considering the schema above, '|trees|visu1|e2|sub2' refers to the same node as '|trees|0|0|1'.

In order to avoid intricate considerations the paths in the tree respects the following principles/constraints:

  • The node names should not contain the vertical bar character '|'. The bar in the names can be escaped with 2 vertical bars '||'.
  • The node names encompassed in the core/trees module ('|trees|..') should not contain the slash character '/' and dot character '.'
  • The node names encompassed in the core/trees module ('|trees|..') should contain only letters (A-z), underscore character ('_'), and digits (0-9). They should not start with a digit (0-9).
  • The node UUID is generated by the system at the node creation and cannot be modified afterwards.
  • A parent node cannot have two or more children with same name
  • There is no built-in symbolic / hard links (Unix) or shurtcuts (Windows) in the tree. This functionality can be implemented inside nodes since it is possible to refer one node from another by storing its direct, absolute, or relative path.
  • One path can refer to only one single node: it is not possible to use wildcards (*) in the paths (unlike Unix command shell paths).



Node structure

Each node in the tree has the following elements:
  • uuid: unique generated id (UUID version 4. example: 165b313e-9bab-4eab-8aa8-4b5ef969b368)
  • name: unique among siblings
  • index: integer for ordering the siblings (integer from 0 to n-1, where n is the number of siblings)
  • path: absolute path
  • data: Javascript native values. The following type of values are supported.
    • Javascript special values: null, undefined, NaN,...
    • Javascript object: {a:1, s:'The quick brown..', [1,2,{name:'Etienne'}]}
    • Javascript array : [1,2,{name:'Etienne'}]
    • Javascript number: 3.4
    • Javascript string: 'The quick brown fox jumps over the lazy dog'
    • Javascript date: new Date('Mon Jan 01 2018 00:00:00')
    • Javascript regular expression (RegEx). Positive integers: /^\d+$/
    • Javascript "standalone" function (no context or reference to external variables/functions).
      • standard function syntax: "function addition(a,b){return a+b}".
      • ECMA6 anonymous function syntax: "(a,b) => a+b".
  • dataModel: The data model is read only. It defines the structure, the read only properties, constant properties, the ones that are stored or not on disk. Could also specify arbitrary constraints on the data (data validation function).
    • Edition aspects: the data may be completely, partially, or not at all editable. Some properties of an object may be defined as "read only".
    • Storage aspects: the data may be completely, partially, or not at all stored in the filesystem. For embedded systems with SD storage, properties frequently changing like a KNX value are traditionnaly not permanently stored in order to avoid recurrent writes diminishing the SD life expectancy. Storage aspects are globally transparent to the API client, even if they may be noticed after a system reboot.
  • methods: each node can have multiple associated callable methods (API calls). Methods are specific to each node. The methods may be dynamically updated depending on the node nature/type (data) or its position in the tree (path). It is not only an object, but a function that can be called to retrieve other nodes methods.
  • node,tree,uuid,path,data,children,methods: explorer methods used for navigation.
  • addListener,on,once,removeListener: part of the explorer methods. It is used to listen to events. Refer to the Node events section below.
  • close: part of the explorer methods. It removes all the listeners. Also removes node.methods listeners if it is an event emitter. If the node is not used anymore, it is important to close it to avoid memory leaks.


Node events

Each type of node event (updates, moves, insertions, and deletions) can be listened to for the node itself, the direct children ("child" prefix), and the descendents ("desc" prefix). For each event name it is possible to define a suffix path (relative or absolute) in order to listen to it. If no suffix path is defined, then the current node is listened to. List of events:
  • updates: updated[ path], childUpdated[ path], descUpdated[ path]
  • moves: moved[ path], childMoved[ path], descMoved[ path]
  • insertions: inserted[ path], childInserted[ path], descInserted[ path]
  • deletions: deleted[ path], childDeleted[ path], descDeleted[ path]

peers('supervision').require('explorer', function(err,explorer){ //loads the explorer.
	explorer.node('|gateways|knx1', function(err, gatewayNode){ 	//gets a node..
	
		//now can listen to events.
		
		/// UPDATED EVENT. This event fires when some node data is updated.
		// It also fires at initialization if the data was not known or not up to date. 

		//fires when node data is updated. 
		gatewayNode.on('updated', function(newData, oldData, extend){
			
		})
		//fires when  parent data is updated.
		gatewayNode.on('updated ..', function(newData,oldData, extend){
			
		})
		//fires when the first child data is updated
		gatewayNode.on('updated 0', function(newData,oldData, extend){})
		//fires when the child with name 'childName' data is updated
		gatewayNode.on('updated childName', function(newData,oldData, extend){})
		
		/// MOVED EVENT. This event fires when some node path or index changes. 
		
		//fires when the node moves (path or index changes)
		gatewayNode.on('moved', function(newPath, newIndex, oldPath, oldIndex){
			
		})
		//fires when a children is moved or when a node moves in and becomes a child. 
		gatewayNode.on('childMoved', function(newPath, newIndex, oldPath, oldIndex){
			
		})
		
		/// INSERTED EVENT. fires when a node, child, or descendent is inserted. 
		
		//fires when the node is created (and didn't exists before). 
		gatewayNode.once('inserted', function(path, index){
			
		})
		//fires when the second child is created. 
		gatewayNode.once('inserted 1', function(path, index){
			//here index == 1.
		})
		//fires when a child or grandchild is inserted. 
		gatewayNode.on('descInserted', function(path, index){
			
		})
		
		/// DELETED EVENT. fires when a node is deleted.
		
		//fires when the node is deleted.
		gatewayNode.on('deleted', function(path, index){
			
		})
		//fires when a child is deleted.
		gatewayNode.on('childDeleted', function(path, index){
			
		})
	})
})



tree structure

trees are similar to nodes, except that with the tree approach, the data property includes (merges) the children/descendents data, and unlike nodes the 'inserted','moved', and 'deleted' events that concern the tree also carries the data information. Each tree has the following elements:
  • node the node (root of the tree) object as defined above. It contains uuid,name,index,path, node data, methods, etc..
  • data: Javascript native values. Recursively includes the descendents data.
  • subtrees: A recursive object with children names as keys and the (sub)tree as value: {'child1': subsubtree1, 'child2': subsubtree2}. It is a recursive structure.
  • update,move,insert,delete: part of the explorer methods. Same as the methods in the node object.
  • node,tree,uuid,path,data,children,methods: explorer methods used for navigation.
  • addListener,on,once,removeListener: explorer methods used to listen events. Here events are relative to the whole tree (for example updated events fire when a descedent data changed). Refer to the tree events section below.
  • close: part of the explorer methods. It removes all the listeners. Also removes node.methods listeners if it is an event emitter. If the node is not used anymore, it is important to close it to avoid memory leaks.


tree events

Same as node events except that the update fires also when inserts, moves, deletes, or updates happens in the descendents.

peers('supervision').require('explorer', function(err,explorer){ //loads the explorer.
	explorer.tree('|gateways|knx1', function(err, gatewayTree){ 	//gets a node..
		/// UPDATED EVENT. This event fires when some tree data is updated.
		// It also fires at initialization if the tree was not known or not up to date. 
		
		//fires when tree data is updated or  when inserts/moves/deletes/updates happen in the descendents.
		gatewayTree.on('updated', function(newData, oldData, extend){
			
		})
	})
})


Example for gateways



peers('supervision').require('explorer', function(err, explorer){
	explorer.node('|gateways|knx1',function(err,knxGatewayNode){
		if(err) {
			console.error(err)
			return;
		}
		// gatewayData contains the knx gateway parameters. Excludes children (knx addresses).
		// {
		// 	"name" : "knx1",
		// 	"description" : "...",
		// 	"active" : 1,
		// 	"driver": "knx"  // links to the knx driver.
		// 	"json"  : {
		// 		"host":"192.168.1.11", // IP address of the knx IP gateway.
		// 		"port":3671,    //port of the knx IP gateway
		// 		"mode":"ipt",  // mode of connection to the knx IP gateway. 
		// 					// Possible values are 'ip', 'ipt' (ip tunneling), and 'iptn' (ip tunneling nat).
		// 		"server":1     // Activate (1) or deactivate (0) the knxd IP multicast server. 
		// 					// This feature is useful to remotely connect to the KNX bus for maintenance, but creates multicast traffic on the LAN.
		// 	 }
		// }
		var gatewayMethods = knxGatewayNode.methods
		Object.keys(gatewayMethods).forEach(m => console.log("method " + m)) //display available methods
		gatewayMethods.write('1/1/1',0, err => err && console.error(err)) //write the KNX address.
		gatewayMethods.on('newValue', function(addressName, newValue, newRawValue, date){
			if(addressName == '1/1/1'){
				console.log('new value for knx address 1/1/1: ', newValue)
			}
		})
		knxGatewayNode.on('updated', function(newGateway,oldGateway, difference){ //equivalent to gatewayUpdated
			console.log("knx gateway was updated")
		})
		knxGatewayNode.on('moved', function(newPath,newIndex, oldPath,oldIndex){
			
		})
		knxGatewayNode.on('deleted', function(){ //equivalent to gatewayDeleted
			console.log("knx gateway was deleted")
			// automatically cleans all listeners (by calling knxGatewayNode.close method)
			// the deleted event also fires for descendents elements.
		})
		knxGatewayNode.on('close', function(){
			//fires after the knxGatewayNode.close is called or after the element is deleted.
			console.log("knxGatewayNode closed")
		})
		knxGatewayNode.close() //removes the listeners
		
		//it is also possible to listen events directly on the knx address nodes (i.e. in the children).
		knxGatewayNode.children(function(err, childrenNames){ //retrieves the children..
			childrenNames.forEach(function(childName,childIndex){
				knxGatewayNode.node(childName, function(addressNode){
					var addressMethods = addressNode.methods
					addressMethods.write(1, err => err && console.error(err)) // write the KNX address.
					addressNode.on('updated', function(newValue){
						console.log('new value for knx address 1/1/1: ', newValue)
					})
					addressNode.on('deleted', function(){
						console.log("address knx 1/1/1 was deleted")
						//automatically cleans listeners
					})
					setTimeout(function(){
						addressNode.close() //To avoid memory leaks it is important to close the explorer at some point.
					},60000)
				})
			})
			//note that once loaded, children are accessible from cache with : 
			Object.keys(knxGatewayNode.children).forEach(function(childName, childIndex){
				
			})
			// you can check when the children has been loaded for the last time by checking the non-enumerable ltime. 
			console.log(knxGatewayNode.children.ltime) //timestamp in milliseconds
		})
		
		
		//replacing the whole node (excluding children) :
		knxGatewayNode.update({
			'__replace__' : {
				"name" : "knx1",
				"description" : "...",
				"active" : 1,
				"driver": "knx"  // links to the knx driver.
				"json"  : {
					"host":"192.168.1.15", // IP address of the knx IP gateway.
					"port":3671,    //port of the knx IP gateway
					"mode":"ipt",  // mode of connection to the knx IP gateway. 
								// Possible values are 'ip', 'ipt' (ip tunneling), and 'iptn' (ip tunneling nat).
					"server":0     // Activate (1) or deactivate (0) the knxd IP multicast server. 
								// This feature is useful to remotely connect to the KNX bus for maintenance, but creates multicast traffic on the LAN.
				}
			}
		}, function(err){
			
		})
	})
	
	
	//loads all the supervision data including descendents : 
	explorer.tree('|', function(err,supervisionTree){
		//here supervisionTree.data contains all the supervision data.
	})
	
	//loading the whole supervision with optional progression callback parameter:
	explorer.tree('|', function(err, partialData, loadedBytes, totalBytes){
		console.log("loaded " + loadedBytes/totalBytes + "%") //progression in percent.
	}, function(err,supervisionTree){
		console.log("complete data", supervisionTree.data)
		var gatewaysTree = supervisionTree.subtrees['gateways']
		var gatewaysApi = gatewaysTree.node.methods;
	})
	
	//loading only the gateways api with the explorer : 
	explorer.node('|gateways', function(err, gatewaysNode){
		//in this context explorer refers to the gateways explorer. 
		var gateways = gatewaysNode.methods
		gatewaysApi.on('gatewayInserted', function(newGateway){...}) //normal gateways methods. (refer to core/gateways module).
	})
	
	//loading only the gateways api without the node data and explorer : 
	explorer.methods('|gateways', function(err, gatewaysApi){
		gatewaysApi.on('gatewayInserted', function(newGateway){...}) //normal gateways methods. (refer to core/gateways module).
	})
	//code above is the same as loading the gateways api through the direct method : 
	peers('supervision').require('gateways', function(err,gatewaysApi){
		gatewaysApi.on('gatewayInserted', function(newGateway){...})
	})
})