s7webserverapi
Version:
Unofficial Simatic-S7-Webserver JSON-RPC-API Client for S7-1200/1500 PLCs
307 lines (306 loc) • 12 kB
JavaScript
import { Observable, ReplaySubject } from "rxjs";
import { CacheMethod } from "../util/types";
/**
* When we call the write function of the PLCConnector, we dont instantly get a response back. We basically want a Observable that sends a single value when the transaction is done.
*
* This class handles this. When writing, we create a new transaction. This is especially a shared Observable that we send back to the writing-caller and the polling-loop will collect the write-transaction here and when the transaction is done it will send a value to the Observable.
*/
export class RPCTransactionHandler {
constructor() {
this.transactions = [];
this.nextId = 0;
this.freeSlots = [];
}
getAllTransactionsKeys() {
return this.transactions.map((transaction, index) => transaction === null ? -1 : index).filter((index) => index !== -1);
}
getTransaction(id) {
if (this.transactionExists(id)) {
return this.transactions[id];
}
return undefined;
}
getTransactionFromKey(key) {
const transaction = this.transactions.find((transaction) => transaction !== null && transaction.keys.toString() === key.toString());
return transaction;
}
transactionExists(id) {
return id >= 0 && id < this.transactions.length && this.transactions[id] !== null;
}
removeTransaction(id) {
if (id >= 0 && id < this.transactions.length && this.transactions[id] !== null) {
this.transactions[id] = null;
this.freeSlots.push(id);
}
}
addDependentKey(id, key) {
if (this.transactionExists(id)) {
this.transactions[id].addDependentKey(key);
}
}
}
export class WriteTransactionHandler extends RPCTransactionHandler {
isCurrentlyWriting(hmiKey) {
const keys = Array.isArray(hmiKey) ? hmiKey : [hmiKey];
return this.transactions.some((transaction) => transaction !== null && keys.includes(transaction.keys[0]));
}
createTransaction(key, value) {
let id;
if (this.freeSlots.length > 0) {
id = this.freeSlots.pop();
}
else {
id = this.nextId++;
}
this.transactions[id] = new WriteTransaction(key, value, id);
return this.transactions[id].subject.asObservable();
}
resolveDependentKey(id, key, result, cacheRef) {
if (this.transactionExists(id)) {
const transaction = this.transactions[id];
transaction.resolveDependentKey(key, result);
const [check, resultBool] = transaction.checkAndResolve();
if (check) {
cacheRef?.writeEntry(transaction.keys[0], transaction.value);
transaction.subject.next(resultBool);
transaction.subject.complete();
this.removeTransaction(id);
return resultBool;
}
}
return false;
}
}
export class GetTransactionHandler extends RPCTransactionHandler {
constructor(writeTransactionHandler, cacheRef) {
super();
this.writeTransactionHandler = writeTransactionHandler;
this.cacheRef = cacheRef;
}
getTransactionFromKey(keys, depth, cacheMethod) {
const transaction = this.transactions.find((transaction) => transaction !== null && transaction.keys.toString() === keys.toString() && (depth == undefined ? true : transaction.depth === depth) && (cacheMethod == undefined ? true : transaction.cacheMethod === cacheMethod));
return transaction;
}
createTransaction(key, cacheMethod, depth) {
const matchingTransaction = this.getTransactionFromKey(key, depth, cacheMethod);
let id;
if (matchingTransaction == undefined) {
if (this.freeSlots.length > 0) {
id = this.freeSlots.pop();
}
else {
id = this.nextId++;
}
this.transactions[id] = new GetTransaction(key, id, cacheMethod, depth, this.writeTransactionHandler, this.cacheRef);
}
else {
id = matchingTransaction.id;
}
return new Observable(subscriber => {
// We need this wrapper here so we can delete the transaction when everything is completed.
const trans = this.transactions[id];
if (!trans) {
subscriber.complete();
return;
}
const x = trans.subject.subscribe({
complete: () => {
subscriber.complete();
},
next: (x) => { subscriber.next(x); },
error: (x) => subscriber.error(x),
});
return () => {
x.unsubscribe();
this.removeTransaction(id);
};
});
}
resolveDependentKey(id, key, value) {
if (this.transactionExists(id)) {
const transaction = this.transactions[id];
transaction.resolveDependentKey(key, value);
this.cacheRef?.writeEntry(key, value);
const check = transaction.checkAndResolve();
if (check) {
transaction.publishGetData();
return true;
}
}
return false;
}
}
export class RPCTransaction {
constructor(key, id) {
// ReplaySubject is important. When we have a UseCache-Get-Transaction we may call subject.next() before the other side even has the chance of calling .subscribe. The next-call would be lost.
this.subject = new ReplaySubject();
this.keys = key;
this.id = id;
this.dependentKeys = new Map();
}
addDependentKey(key) {
this.dependentKeys.set(key, null);
}
}
export class GetTransaction extends RPCTransaction {
constructor(key, id, cacheMethod, depth, writeTransactionHandler, cacheRef) {
super(key, id);
this.depth = depth;
this.writeTransactionHandler = writeTransactionHandler;
this.cacheRef = cacheRef;
this.internalReadStack = [];
this.cacheMethod = cacheMethod;
this.initialize();
this.publishGetData = this.publishGetData.bind(this);
}
initialize() {
switch (this.cacheMethod) {
case CacheMethod.IGNORE_CACHE:
return this.initIgnoreCache();
case CacheMethod.USE_CACHE:
return this.initUseCache();
case CacheMethod.USE_WRITE:
return this.initUseWrite();
case CacheMethod.WAIT_FOR_WRITE:
return this.initWaitForWrite();
}
}
initIgnoreCache() {
this.useIgnoreCache();
}
useIgnoreCache() {
for (const key of this.keys) {
this.internalReadStack.push([key, this.depth]);
}
}
useCache() {
this.publishGetData();
}
createConcatenatedObject() {
const concatenatedObject = {};
for (const key of this.keys) {
const keySplit = key.split('.');
let lastKey = keySplit[keySplit.length - 1];
let backIndex = 1;
while (concatenatedObject[lastKey] != undefined) {
if (keySplit[keySplit.length - 1 - backIndex]) {
throw new Error('You asked for multiple keys with the same identifier in the get-function of the plc connector, this results in key conflicts');
}
lastKey = keySplit[keySplit.length - 1 - backIndex] + '.' + lastKey;
backIndex++;
}
concatenatedObject[lastKey] = this.cacheRef.getCopy(key);
}
return concatenatedObject;
}
initUseCache() {
const loaded = this.cacheRef.hmiKeyLoaded(this.keys);
if (loaded) {
this.useCache();
}
else {
this.useIgnoreCache();
}
}
initUseWrite() {
const loaded = this.cacheRef.hmiKeyLoaded(this.keys);
const isCurrentlyWriting = this.writeTransactionHandler.isCurrentlyWriting(this.keys);
if (loaded && !isCurrentlyWriting) {
this.useCache();
}
else if (isCurrentlyWriting) {
if (this.keys.length > 1) {
throw Error("Getting multiple vars with Cache-Method USE_WRITE is currently not supported due to unwanted behaviour, please use another Cache-Method");
}
const val = this.writeTransactionHandler.getTransactionFromKey(this.keys)?.value;
if (val == undefined) {
throw new Error(`Error while trying to get value with key: ${this.keys[0]}. The write-transaction is currently running but the value is undefined.`);
}
this.subject.next(val);
this.subject.complete();
}
else {
this.useIgnoreCache();
}
}
initWaitForWrite() {
const loaded = this.cacheRef.hmiKeyLoaded(this.keys);
const isCurrentlyWriting = this.writeTransactionHandler.isCurrentlyWriting(this.keys);
if (loaded && !isCurrentlyWriting) {
this.useCache();
}
else if (isCurrentlyWriting) {
if (this.keys.length > 1) {
throw Error("Getting multiple vars with Cache-Method WAIT_FOR_WRITE is currently not supported due to unwanted behaviour, please use another Cache-Method");
}
const writeTransaction = this.writeTransactionHandler.getTransactionFromKey(this.keys);
if (writeTransaction) {
writeTransaction.subject.subscribe((status) => {
if (!status) {
this.subject.error(`Error while trying to wait for write. The Write Transaction was not successful for key: ${this.keys[0]}`);
}
this.subject.next(this.cacheRef.getCopy(this.keys[0]));
this.subject.complete();
});
}
else {
this.subject.error(`Error while trying to wait for write. No write-transaction found for key: ${this.keys[0]}`);
}
}
else {
this.useIgnoreCache();
}
}
publishGetData() {
if (this.keys.length === 1) {
this.subject.next(this.cacheRef.getCopy(this.keys[0]));
setTimeout(() => this.subject.complete(), 0);
return;
}
const co = this.createConcatenatedObject();
this.subject.next(co);
setTimeout(() => this.subject.complete(), 0);
}
resolveDependentKey(key, value) {
if (this.dependentKeys.has(key)) {
this.dependentKeys.set(key, true);
this.value = value;
}
else {
throw new Error(`Key ${key} is not a dependent key of this transaction`);
}
}
checkAndResolve() {
for (const [, result] of this.dependentKeys) {
if (result === null) {
return false;
}
}
return true;
}
}
export class WriteTransaction extends RPCTransaction {
constructor(key, value, id) {
super(key, id);
this.value = value;
}
resolveDependentKey(key, result) {
if (this.dependentKeys.has(key)) {
this.dependentKeys.set(key, result);
}
else {
throw new Error(`Key ${key} is not a dependent key of this transaction`);
}
}
// Returns [boolean, boolean] where the first value means "Transaction resolved and all the keys were loaded", and the other bool is the concatenated result of all write transactions
checkAndResolve() {
let resultBool = true;
for (const [, result] of this.dependentKeys) {
if (result === null) {
return [false, false];
}
resultBool = resultBool && result;
}
return [true, resultBool];
}
}