UNPKG

s7webserverapi

Version:

Unofficial Simatic-S7-Webserver JSON-RPC-API Client for S7-1200/1500 PLCs

307 lines (306 loc) 12 kB
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]; } }