s7webserverapi
Version:
Unofficial Simatic-S7-Webserver JSON-RPC-API Client for S7-1200/1500 PLCs
170 lines (169 loc) • 5.92 kB
JavaScript
import { Subject } from "rxjs";
/**
* Prefix-Trie implementation for storing subscribers.
* Since we call the PLC/HMI-Vars with a flattened-key e.g. "test.key.value" but we also are able to subscribe to an object instead of a leaf value, we store the subscribers in a trie.
*
* With that we call all subscribers of a key, when either the key itself or a children changes.
*
* @export
* @class SubscriberTrie
* @typedef {SubscriberTrie}
* @template FlattenKeyType
*/
export class SubscriberTrie {
/**
* Creates an instance of SubscriberTrie.
*
* @constructor
*/
constructor() {
this.root = new SubscriberTrieNode();
}
parseFlattenedKey(flattenedKey) {
return flattenedKey.split('.').map(key => {
// Check if the key is a number and convert it if so
return isNaN(Number(key)) ? key : Number(key);
});
}
/**
* returns the subscriber at that key, if it exists.
*
* @param {FlattenKeys<FlattenKeyType>} keyString
* @returns {Subject<{value:PLCConnectorDataTypes, changedKey: string}> | undefined}
*/
get(keyString) {
let currentNode = this.root;
const keyArray = this.parseFlattenedKey(keyString);
for (const element of keyArray) {
if (!currentNode.children.has(element)) {
return;
}
currentNode = currentNode.children.get(element);
}
return currentNode.subscriber;
}
getSubscriptionCountMap() {
const map = new Map();
// Go through all the children of the root and get the subscriber count
for (const [key, value] of this.root.children) {
this.getSubscriptionCountMapRecursive(value, key, map);
}
return map;
}
getSubscriptionCountMapRecursive(value, key, map) {
if (value.subscriberCount > 0) {
map.set(key, value.subscriberCount);
}
for (const [childKey, childValue] of value.children) {
this.getSubscriptionCountMapRecursive(childValue, key + "." + childKey, map);
}
}
incrementSubscriberCount(key) {
let currentNode = this.root;
const keyArray = this.parseFlattenedKey(key);
for (const element of keyArray) {
if (!currentNode.children.has(element)) {
return;
}
currentNode = currentNode.children.get(element);
}
currentNode.subscriberCount++;
}
decrementSubscriberCount(key) {
let currentNode = this.root;
const keyArray = this.parseFlattenedKey(key);
for (const element of keyArray) {
if (!currentNode.children.has(element)) {
return;
}
currentNode = currentNode.children.get(element);
}
currentNode.subscriberCount--;
}
/**
* Checks if a subscriber at the key exists.
* Important: It checks for the subscriber, not if the key exists in the trie.
*
* @param {FlattenKeys<FlattenKeyType>} key
* @returns {boolean}
*/
has(key) {
let currentNode = this.root;
const keyArray = this.parseFlattenedKey(key);
for (const element of keyArray) {
if (!currentNode.children.has(element)) {
return false;
}
currentNode = currentNode.children.get(element);
}
return currentNode.subscriber !== undefined;
}
/**
* Inserts a new key into the trie and automatically creates a subscriber.
* Every key only has one subscriber.
*
* @param {FlattenKeys<FlattenKeyType>} key
*/
insert(key) {
let currentNode = this.root;
const keyArray = this.parseFlattenedKey(key);
for (const element of keyArray) {
if (!currentNode.children.has(element)) {
currentNode.children.set(element, new SubscriberTrieNode());
}
currentNode = currentNode.children.get(element);
}
// finally add the subscriber to the last node
currentNode.subscriber = new Subject();
}
/**
* Notifies all subscriber of the key with all its parent-prefixes.
*
* E.g if we call notifySubscriber("test.key.value"); We call the subscribers for test, for test.key and for test.key.value (if they exist)
*
* The data key is put into this function as a helper, because we need to notify the subscriber with the respective data.
* The data-object has to follow the strucutre of the trie or rather of the key that is notified.
*
* If the key is "test.key.value" and the data is {test: {key: {value:1}}}. test.key is notified with {value: 1} and test.key.value is notified with 1
*
*
* @param {FlattenKeys<FlattenKeyType>} key
* @param {PLCConnectorKeys<PLCConnectorDataTypes>} data
*/
notifySubscriber(key, data) {
let currentNode = this.root;
let reference = data;
const keyArray = this.parseFlattenedKey(key);
const residualKey = key;
for (const element of keyArray) {
if (!currentNode.children.has(element)) {
return;
}
currentNode = currentNode.children.get(element);
if (reference[element] == undefined) {
return;
}
reference = reference[element];
currentNode.subscriber?.next({ value: JSON.parse(JSON.stringify(reference)), changedKey: residualKey });
}
}
}
/**
* Node of the subscriber trie
*
* @export
* @class SubscriberTrieNode
* @typedef {SubscriberTrieNode}
* @template FlattenKeyType
*/
export class SubscriberTrieNode {
/**
* Creates an instance of SubscriberTrieNode.
*
* @constructor
*/
constructor() {
this.subscriberCount = 0;
this.children = new Map();
}
}