s7webserverapi
Version:
Unofficial Simatic-S7-Webserver JSON-RPC-API Client for S7-1200/1500 PLCs
501 lines (500 loc) • 21.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const cmd_ts_1 = require("cmd-ts");
const fs = require("fs-extra");
const path = require("path");
const xml2js = require("xml2js");
const ts = require("typescript");
const vm = require("vm");
const app = (0, cmd_ts_1.command)({
name: 's7webservergen',
args: {
dbRoot: (0, cmd_ts_1.option)({
type: cmd_ts_1.string,
long: 'db-root-folder',
short: 'd',
description: 'Path of the folder in which all the PLC-DBs of the project are exported as .xml-Files. Files can be structured inside sub-folders after this path. But no other types of xml-files should be present inside this root-folder'
}),
udtRoot: (0, cmd_ts_1.option)({
type: cmd_ts_1.string,
long: 'udt-root-folder',
short: 'u',
description: 'Path of the folder in which all the PLC-UDTs (User Defined Data-Types) are exported as .xml-Files. Can be ordered in sub-folders after this path. No other types of xml-Files should be present inside this root-folder',
}),
hmiDBs: (0, cmd_ts_1.option)({
type: cmd_ts_1.string,
long: 'hmiDBs',
short: 'h',
description: 'Collection of all the DBs that should be used in the HMI (the ones that should be exported) [Without the \"\" wrapping the name], seperated by comma. Example: -h="MyDB,MyOtherDB,HMIDB,WEBDB"'
}),
outputPath: (0, cmd_ts_1.option)({
type: cmd_ts_1.string,
long: 'output-path',
short: 'o',
description: 'Outputpath where the generated files will be stored',
defaultValue: () => './'
}),
force: (0, cmd_ts_1.flag)({
type: cmd_ts_1.boolean,
long: 'force',
short: 'f',
defaultValue: () => false
})
},
handler: ({ dbRoot, udtRoot, hmiDBs, outputPath, force }) => {
const gen = new ConfigGenerator(hmiDBs.split(','), dbRoot, udtRoot, outputPath, force);
}
});
function main() {
(0, cmd_ts_1.run)(app, process.argv.slice(2));
}
if (require.main === module) {
main();
}
const plcNumberDataTypes = ['DInt', 'Int', 'LInt', 'LReal', 'Real', 'SInt', 'UDInt', 'UInt', 'ULInt', 'USInt'];
const plcBoolDataTypes = ['Bool'];
const plcBinaryDataTypes = ['Byte', 'Word', 'DWord', 'LWord'];
const plcTextDataTypes = ['Char', 'String'];
const plcDateDataTypes = ['Date', 'Date_And_Time', 'LTime', 'Time', 'Time_Of_Day'];
class ConfigGenerator {
dbsToImport;
dbRoot;
udtRoot;
outputPath;
force;
// First goes through the xml-Files and parses and stores them in memory here for faster access.
dbCacheMap = new Map();
udtCacheMap = new Map();
structCacheMap = new Map();
parser = new xml2js.Parser();
importantDBs = {};
importantUDTs = {};
udtOutFileName = 'udts';
dbOutFileName = 'dbs';
plcStructureOutFileName = 'plcStructure';
constructor(dbsToImport, dbRoot, udtRoot, outputPath, force) {
this.dbsToImport = dbsToImport;
this.dbRoot = dbRoot;
this.udtRoot = udtRoot;
this.outputPath = outputPath;
this.force = force;
this.readInSubfolderXmlFiles(path.relative(process.cwd(), dbRoot), this.dbCacheMap).then(() => {
return this.readInSubfolderXmlFiles(path.relative(process.cwd(), udtRoot), this.udtCacheMap);
}).then(() => {
this.parseInConfig();
this.generateCode();
});
}
generateImportUdtsCode() {
return `import {${Object.keys(this.importantUDTs).join(', ')}} from './${this.udtOutFileName}'\n\n`;
}
generateImportDbsCode() {
return `import {${Object.keys(this.importantDBs).join(', ')}} from './${this.dbOutFileName}'\n\n`;
}
/**
* Now that we have parsed every key, we want to generate the code.
* For this we first have a file with all the UDT/Struct-Definitions aswell as the DB-Definitions:
*
*/
generateCode() {
const udtFile = this.generateUDTClassesFile(true);
this.saveWithBackup(path.resolve(process.cwd(), this.outputPath, this.udtOutFileName) + '.ts', udtFile);
const dbFile = this.generateDBClassesFile(true);
this.saveWithBackup(path.resolve(process.cwd(), this.outputPath, this.dbOutFileName) + '.ts', dbFile);
const structureFile1 = this.generatePLCStructureObject(true);
const structureFile2 = this.generatePLCStructureTypeCode();
this.saveWithBackup(path.resolve(process.cwd(), this.outputPath, this.plcStructureOutFileName) + '.ts', structureFile1 + '\n' + structureFile2);
}
saveWithBackup(path, content) {
if (fs.existsSync(path)) {
let i = 0;
while (fs.existsSync(path + '.bak' + i.toString())) {
i++;
}
fs.copyFileSync(path, path + '.bak' + i.toString());
}
fs.writeFileSync(path, content);
}
generateUDTClassesFile(shouldExport = false) {
let fileString = '';
for (const udtKey in this.importantUDTs) {
const udt = this.importantUDTs[udtKey];
let classStringDefinition = `${shouldExport ? 'export ' : ''}class ${udtKey} {\n`;
classStringDefinition += this.generateMemberCode(udt);
classStringDefinition += '}\n';
fileString += classStringDefinition;
fileString += '\n\n\n';
}
return fileString;
}
generatePLCStructureObject(shouldExport = false) {
let fileString = '';
if (shouldExport) {
fileString = `${this.generateImportDbsCode()}`;
}
fileString += `${shouldExport ? 'export ' : ''}const PLC_STRUCTURE = {\n`;
for (const dbKey in this.importantDBs) {
fileString += ` "\\"${dbKey}\\"": new ${dbKey}(),\n`;
}
fileString += `}\n`;
return fileString;
}
generatePLCStructureTypeCode() {
let tsFileToRun = `
${this.generateUDTClassesFile()}
${this.generateDBClassesFile()}
${this.generatePLCStructureObject()}
`;
let result = JSON.stringify(runTypeScriptCode(tsFileToRun, 'PLC_STRUCTURE'));
result = result.replace(/\{\}/g, 'number');
return `export type PLC_STRUCTURE_TYPE = ${result};`;
}
generateDBClassesFile(shouldExport = false) {
let fileString = '';
if (shouldExport) {
fileString = this.generateImportUdtsCode();
}
for (const dbKey in this.importantDBs) {
const db = this.importantDBs[dbKey];
let classStringDefinition = `${shouldExport ? 'export ' : ''}class ${dbKey} {\n`;
classStringDefinition += this.generateMemberCode(db);
classStringDefinition += '}\n';
fileString += classStringDefinition;
fileString += '\n\n\n';
}
return fileString;
}
generateMemberCode(memberParent) {
let memberString = '';
for (const memberKey in memberParent) {
const member = memberParent[memberKey];
const memberIsInteger = Number.isInteger(Number(memberKey));
const readOnlyPrefix = member.readonly ? 'readonly ' : '';
if (member.plcType === 'UDT') {
memberString += this.constructUdtMember(member, memberKey, memberIsInteger, readOnlyPrefix);
}
else {
memberString += this.constructLeafMember(member, memberKey, memberIsInteger, readOnlyPrefix);
}
}
return memberString;
}
constructLeafMember(member, memberKey, memberIsInteger, readonlyPrefix) {
if (plcBinaryDataTypes.includes(member.plcType)) {
return ` ${readonlyPrefix}${memberIsInteger ? '"' + memberKey + '"' : memberKey}: number = ${member.data}();\n`;
}
if (member.isArray) {
let innerName = typeof member.data[0].data;
innerName = innerName.charAt(0).toUpperCase() + innerName.slice(1);
let constructorValue = "";
if (member.isArray) {
constructorValue = member.data.map((data) => `${innerName}()`).join(', ');
}
let arrayStringValue = `new Array<${typeof member.data[0].data}>(${constructorValue})`;
if (typeof member.data[0].data == 'string' || typeof member.data[0].data == 'number') {
arrayStringValue = JSON.stringify(member.data.map(d => d.data));
}
return ` ${readonlyPrefix}${memberIsInteger ? '"' + memberKey + '"' : memberKey}: ${typeof member.data[0].data}[] = ${arrayStringValue};\n`;
}
return ` ${readonlyPrefix}${memberIsInteger ? '"' + memberKey + '"' : memberKey} = ${JSON.stringify(member.data)};\n`;
}
constructUdtMember(member, memberKey, memberIsInteger, readonlyPrefix) {
let constructorValue = '';
let valueValue = `new ${member.udt}`;
if (member.isArray) {
constructorValue = member.data.map((data) => `new ${member.udt}()`).join(', ');
valueValue = `new Array<${member.udt}>`;
}
valueValue += `(${constructorValue})`;
return ` ${readonlyPrefix}${memberIsInteger ? '"' + memberKey + '"' : memberKey}: ${member.udt}${member.isArray ? '[]' : ''} = ${valueValue};\n`;
}
parseInConfig() {
for (const db of this.dbsToImport) {
if (!this.dbCacheMap.has(db + '.xml')) {
throw new Error(`The DB ${db} does not exist in the provided PLC-Project-Files. ${db}.xml not found in sub-folder structure of root: ${path.relative(process.cwd(), this.dbRoot)}`);
}
this.importantDBs[db] = {};
const parsed = this.dbCacheMap.get(db + '.xml');
const doc = parsed.Document;
const tiaVersion = doc.Engineering[0].$.version;
this.checkTiaVersion(tiaVersion);
const attributes = doc['SW.Blocks.GlobalDB'][0].AttributeList[0];
const vars = attributes.Interface[0].Sections[0].Section[0].Member;
for (const section of vars) {
this.readInMember(section, this.importantDBs[db]);
}
}
}
readInMember(member, ref) {
let notWriteable = false;
if (member.AttributeList && member.AttributeList[0] && member.AttributeList[0].BooleanAttribute) {
const booleanAttributes = member.AttributeList[0].BooleanAttribute;
const notReachable = booleanAttributes.some((attr) => attr.$.Name === 'ExternalAccessible' && attr._ === 'false');
const notVisible = booleanAttributes.some((attr) => attr.$.Name === 'ExternalVisible' && attr._ === 'false');
notWriteable = booleanAttributes.some((attr) => attr.$.Name === 'ExternalWritable' && attr._ === 'false');
// If we wont be able to access this var from the Web we just exclude this key
if (notReachable || notVisible) {
return;
}
}
ref[member.$.Name] = {};
ref[member.$.Name].readonly = notWriteable;
const isArray = member.$.Datatype.startsWith('Array');
if (isArray) {
this.readInArrayType(member, ref[member.$.Name]);
return;
}
this.plcToJsDataType({
plcDataType: member.$.Datatype, contextMember: member, data: member.StartValue ? member.StartValue[0] : undefined, ref: ref[member.$.Name]
});
}
readInArrayType(section, ref) {
const dataType = section.$.Datatype;
const arrayDataType = dataType.split(' of ')[1];
const arrayLength = Number(dataType.match(/\d+/g)[1]) + 1;
const isUDT = arrayDataType.startsWith('"') && arrayDataType.endsWith('"');
if (isUDT) {
ref.plcType = 'UDT';
ref.udt = arrayDataType.slice(1, -1);
}
else {
ref.plcType = arrayDataType;
}
let arr = new Array(arrayLength).fill(undefined);
if (section.Subelement) {
section.Subelement.forEach((subelement) => {
const ind = Number(subelement.$.Path);
const value = subelement.StartValue ? subelement.StartValue[0] : '';
arr[ind] = value;
});
}
ref.data = [];
ref.isArray = true;
arr = this.plcToJsDataType({
plcDataType: arrayDataType, contextMember: section, array: true, data: arr, ref: ref.data
});
}
cleanDataType(dataType) {
// Needed if the string has a fixed length, we cant represent that in js
if (dataType.startsWith('String[')) {
return 'String';
}
return dataType;
}
readInStructDataType(contextMember, ref) {
const structName = contextMember['$'].Name;
let suffix = "";
while (this.structCacheMap.has(structName + suffix)) {
suffix = +(suffix)++;
}
const finalStructName = structName + suffix;
ref.udt = finalStructName;
ref.plcType = 'UDT'; // Handle a struct just like an UDT
this.importantUDTs[finalStructName] = {};
for (const member of contextMember.Member) {
this.readInMember(member, this.importantUDTs[finalStructName]);
}
}
plcToJsDataType({ plcDataType, contextMember, array, data, ref }) {
if (array) {
for (const element of data) {
ref.push({});
const newRef = ref[ref.length - 1];
this.plcToJsDataType({ plcDataType, data: element, contextMember, array: false, ref: newRef });
}
return;
}
plcDataType = this.cleanDataType(plcDataType);
ref.plcType = plcDataType;
ref.isArray = false;
ref.data = {};
if (plcBoolDataTypes.includes(plcDataType)) {
ref.data = this.plcBooleanToJsDataType(plcDataType, data, ref);
}
else if (plcNumberDataTypes.includes(plcDataType)) {
ref.data = this.plcNumberToJsDataType(plcDataType, data, ref);
}
else if (plcBinaryDataTypes.includes(plcDataType)) {
ref.data = this.plcBinaryToJsDataType(plcDataType, data, ref);
}
else if (plcTextDataTypes.includes(plcDataType)) {
ref.data = this.plcTextToJsDataType(plcDataType, data, ref);
}
else if (plcDateDataTypes.includes(plcDataType)) {
ref.data = this.plcDateToJsDataType(plcDataType, data, ref);
}
else if (plcDataType === 'Struct') {
// A struct is created as a nested object but without any new class, is a struct used in multiple places this creates
return this.readInStructDataType(contextMember, ref);
}
else {
// UDTs are enclosed with "<>" so if it starts and ends with this its an UDT and we read it in
if (plcDataType.startsWith('"') && plcDataType.endsWith('"')) {
return this.readInPlcUserDataType(plcDataType.slice(1, -1), ref);
}
if (!this.force) {
throw new Error(`The Datatype ${plcDataType} is not supported yet! Create an issue/PR on GitHub. In the meantime, you can use --force and add the data-type yourself later on.`);
}
else {
return this.readInArbitraryDataType(plcDataType, data, ref);
}
}
}
readInArbitraryDataType(dataType, data, ref) {
ref.data = undefined;
ref.plcType = dataType;
return;
}
checkTiaVersion(tiaVersion) {
if (!this.force && !(["V19", "V20"].includes(tiaVersion))) {
throw Error('This TIA-Version export is not yet tested and supported. You can try if it will work by using --force');
}
}
readInPlcUserDataType(dataType, ref) {
let parsedResult;
if (this.udtCacheMap.has(dataType + '.xml')) {
parsedResult = JSON.parse(JSON.stringify(this.udtCacheMap.get(dataType + '.xml')));
}
else {
throw new Error(`It appears, that an imported DB is using a User Defined Datatype (UDT): ${dataType}, but the file ${dataType}.xml is not found inside the root folder of the UDTs: ${path.relative(process.cwd(), this.udtRoot)}`);
}
ref.plcType = 'UDT';
ref.udt = dataType;
if (this.importantUDTs[dataType]) {
return;
}
const doc = parsedResult.Document;
const tiaVersion = doc.Engineering[0].$.version;
this.checkTiaVersion(tiaVersion);
const attributes = doc['SW.Types.PlcStruct'][0].AttributeList[0];
const vars = attributes.Interface[0].Sections[0].Section[0].Member;
this.importantUDTs[dataType] = {};
for (const section of vars) {
this.readInMember(section, this.importantUDTs[dataType]);
}
}
plcBooleanToJsDataType(plcDataType, data, ref) {
if (data) {
return data === 'true';
}
return false;
}
plcDateToJsDataType(plcDataType, data, ref) {
data = data ?? 'T#0ms';
const withoutPrefix = data.split('#')[1];
// The date/time is in the format #10h_20m_30s_... and this function parses it to a js-type
let days, hours, minutes, seconds, milliseconds;
switch (plcDataType) {
case 'Date_And_Time':
// Replace the 3rd '-' with 'T' to make it a valid date string
0;
case 'Date':
return new Date(withoutPrefix);
case 'Time':
case 'LTime':
const regex = /(\d+d)?(\d+h)?(\d+m(?!s))?(\d+s)?(\d+ms)?(\d+us)?(\d+ns)?/;
const matches = withoutPrefix.match(regex);
if (!matches)
return new Date(0);
[, days, hours, minutes, seconds, milliseconds, , ,] = matches;
const date = new Date(0);
if (days) {
date.setUTCDate(date.getUTCDate() + days.replace('d', ''));
}
if (hours) {
date.setUTCHours(date.getUTCHours() + hours.replace('h', ''));
}
if (minutes) {
date.setUTCMinutes(date.getUTCMinutes() + minutes.replace('m', ''));
}
if (seconds) {
date.setUTCSeconds(date.getUTCSeconds() + seconds.replace('s', ''));
}
if (milliseconds) {
date.setUTCMilliseconds(date.getUTCMilliseconds() + milliseconds.replace('ms', ''));
}
return date;
}
return new Date(0);
}
plcTextToJsDataType(plcDataType, data, ref) {
if (data) {
// return data without quotes
return data.slice(1, -1);
}
return '';
}
plcNumberToJsDataType(plcDataType, data, ref) {
if (data) {
return Number(data);
}
return 0;
}
/**
*
* @param plcDataType The DataType of the plc
* @param data the data that should be transformed if not set a empty binary-array will be returned
* @returns The fitting data-type for the PLC, based on the size.
*/
plcBinaryToJsDataType(plcDataType, data, ref) {
// Define the mapping from PLC data types to JavaScript TypedArray constructors
const dataTypeMapping = {
'Byte': Uint8Array,
'Word': Uint16Array,
'DWord': Uint32Array,
'LWord': BigInt64Array
};
// Determine the appropriate JavaScript TypedArray constructor based on the PLC data type
const jsDataType = dataTypeMapping[plcDataType] || Uint8Array;
if (!data) {
return 0;
}
let dataString = data;
let parsedData;
if (dataString.startsWith('16#')) {
parsedData = parseInt(dataString.slice(3), 16);
}
else if (dataString.startsWith('8#')) {
parsedData = parseInt(dataString.slice(2), 8);
}
else {
parsedData = parseInt(dataString, 2);
}
if (jsDataType === BigInt64Array) {
parsedData = BigInt(parsedData);
}
return parsedData;
}
async readInSubfolderXmlFiles(file_path, map) {
const files = fs.readdirSync(file_path);
for (const file of files) {
const fullPath = path.join(file_path, file);
if (fs.statSync(fullPath).isDirectory()) {
await this.readInSubfolderXmlFiles(fullPath, map);
}
else {
if (file.endsWith('.xml')) {
const xml = fs.readFileSync(fullPath, 'utf-8');
const parsed = await this.parser.parseStringPromise(xml);
map.set(file, parsed);
}
}
}
}
}
function runTypeScriptCode(tsCode, varName) {
const jsCode = compileTypeScript(tsCode);
return runJavaScript(jsCode, varName);
}
function compileTypeScript(tsCode) {
const result = ts.transpileModule(tsCode, { compilerOptions: { module: ts.ModuleKind.CommonJS } });
return result.outputText;
}
function runJavaScript(jsCode, varName) {
const context = {}; // Initialize the context
vm.createContext(context); // Create a new context
const script = new vm.Script(jsCode); // Create a script from the JavaScript code
script.runInContext(context); // Run the script in the context
return context[varName]; // Return the value of the specified variable
}