Skip to content

Commit 2391266

Browse files
authored
feat: add command for converting between different opc types to cli (#905)
* Add function for converting between opc types * Add function for writting cnfg to string * Fix mapping of dummy tags in opc conversion * Set default parameters for SopcProc * Add opc command for converting opc objects * Only convert proc when direction is correct * Add missing test file * Fix bugs with opc conversion * Fix tests
1 parent fedb4d3 commit 2391266

9 files changed

Lines changed: 1339 additions & 0 deletions

File tree

packages/septic/src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import { hideBin } from "yargs/helpers";
99
import { formatCommand } from "./format";
1010
import { lintCommand } from "./lint";
1111
import { compareCommand } from "./compare";
12+
import { opcCommand } from "./opc";
1213

1314
yargs(hideBin(process.argv))
1415
.scriptName("sca")
1516
.command(formatCommand)
1617
.command(lintCommand)
1718
.command(compareCommand)
19+
.command(opcCommand)
1820
.demandCommand(1, "You need to specify a command")
1921
.help()
2022
.version()

packages/septic/src/cli/opc.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Equinor ASA
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import yargs, { CommandModule } from "yargs";
7+
import { SepticCnfg } from "../cnfg";
8+
import { convertOPCObjects } from "../opc";
9+
import { createDocumentFromFile } from "../configProvider";
10+
import * as fs from "fs";
11+
import * as path from "path";
12+
13+
interface OpcOptions {
14+
config: string;
15+
output: string;
16+
direction: "sopc-to-ua" | "ua-to-sopc";
17+
simulator?: boolean;
18+
}
19+
20+
async function loadSepticConfig(filePath: string): Promise<SepticCnfg> {
21+
const document = await createDocumentFromFile(filePath);
22+
const cnfg = new SepticCnfg(document);
23+
cnfg.parse(undefined);
24+
await cnfg.updateObjectParents();
25+
return cnfg;
26+
}
27+
28+
async function handler(options: OpcOptions): Promise<void> {
29+
if (!fs.existsSync(options.config)) {
30+
console.error(`Error: Config file not found: ${options.config}`);
31+
process.exit(1);
32+
}
33+
34+
if (!options.config.endsWith(".cnfg")) {
35+
console.error(`Error: Config file must be a .cnfg file`);
36+
process.exit(1);
37+
}
38+
39+
console.log(`Loading config: ${options.config}`);
40+
const cnfg = await loadSepticConfig(options.config);
41+
42+
console.log(`Converting OPC objects (${options.direction})...`);
43+
const converted = convertOPCObjects(
44+
cnfg,
45+
options.direction,
46+
options.simulator,
47+
);
48+
49+
const outputPath = path.resolve(options.output);
50+
const outputDir = path.dirname(outputPath);
51+
52+
if (!fs.existsSync(outputDir)) {
53+
fs.mkdirSync(outputDir, { recursive: true });
54+
}
55+
56+
fs.writeFileSync(outputPath, converted.toString(), "utf-8");
57+
console.log(`Output written to: ${outputPath}`);
58+
}
59+
60+
export const opcCommand: CommandModule<object, OpcOptions> = {
61+
command: "opc <config> <output>",
62+
describe:
63+
"Convert OPC objects in a Septic config between Sopc and UA types",
64+
builder: (yargs) => {
65+
return yargs
66+
.positional("config", {
67+
type: "string",
68+
description: "Path to the input config file",
69+
demandOption: true,
70+
})
71+
.positional("output", {
72+
type: "string",
73+
description: "Path to the output config file",
74+
demandOption: true,
75+
})
76+
.option("direction", {
77+
alias: "d",
78+
type: "string",
79+
description: "Conversion direction",
80+
choices: ["sopc-to-ua", "ua-to-sopc"],
81+
demandOption: true,
82+
})
83+
.option("simulator", {
84+
alias: "s",
85+
type: "boolean",
86+
description: "Enable simulator mode",
87+
demandOption: false,
88+
})
89+
.example(
90+
"$0 opc input.cnfg output.cnfg --direction sopc-to-ua",
91+
"Convert Sopc objects to UA objects",
92+
)
93+
.example(
94+
"$0 opc input.cnfg output.cnfg --direction sopc-to-ua --simulator",
95+
"Convert Sopc objects to UA objects with simulator mode enabled",
96+
)
97+
.example(
98+
"$0 opc input.cnfg output.cnfg -d ua-to-sopc",
99+
"Convert UA objects to Sopc objects",
100+
) as unknown as yargs.Argv<OpcOptions>;
101+
},
102+
handler: (argv) => {
103+
handler(argv).catch((error) => {
104+
console.error("Unexpected error:", error);
105+
process.exit(1);
106+
});
107+
},
108+
};

packages/septic/src/cnfg.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export class SepticCnfg implements SepticContext, TextDocument {
115115
return Promise.resolve();
116116
}
117117

118+
public toString(): string {
119+
return this.objects.map((obj) => obj.toString()).join("\n");
120+
}
118121
public get uri(): string {
119122
return this.doc.uri;
120123
}

packages/septic/src/elements.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export enum SepticTokenType {
2424
eof = "eof",
2525
}
2626

27+
const indentsObjectDeclaration = 2;
28+
const startObjectName = 17;
29+
const spacesBetweenValues = 2;
30+
const spacesBetweenIntValues = 6;
31+
const indentsAttributeValuesStart = 17;
32+
const maxNumberAttrValuesPerLine = 5;
33+
const indentsAttributesDelimiter = 14;
34+
2735
export type SepticToken = IToken<SepticTokenType>;
2836

2937
export class SepticBase {
@@ -185,6 +193,28 @@ export class SepticObject extends SepticBase {
185193
}
186194
return { algExpr: expr, positionsMap: positionsMap };
187195
}
196+
197+
toString(): string {
198+
const identifierStr = this.identifier ? ` ${this.identifier.name}` : "";
199+
const objectTypeFormatted =
200+
" ".repeat(indentsObjectDeclaration) + this.type + ":";
201+
const indentsName = Math.max(
202+
startObjectName - objectTypeFormatted.length - 1,
203+
2,
204+
);
205+
const objectDeclartionFormatted =
206+
objectTypeFormatted + " ".repeat(indentsName) + identifierStr;
207+
return (
208+
objectDeclartionFormatted +
209+
"\n" +
210+
this.attributes
211+
.map((attr) => {
212+
return attr.toString();
213+
})
214+
.join("\n") +
215+
"\n"
216+
);
217+
}
188218
}
189219

190220
export class SepticAttribute extends SepticBase {
@@ -201,6 +231,10 @@ export class SepticAttribute extends SepticBase {
201231
this.values.push(value);
202232
}
203233

234+
setValues(values: SepticAttributeValue[]) {
235+
this.values = values;
236+
}
237+
204238
updateEnd(): void {
205239
if (this.values.length) {
206240
this.end = this.values[this.values.length - 1]!.end;
@@ -261,6 +295,62 @@ export class SepticAttribute extends SepticBase {
261295
isKey(key: string) {
262296
return key === this.key;
263297
}
298+
299+
toString(): string {
300+
const indentsKey = Math.max(
301+
indentsAttributesDelimiter - this.key.length,
302+
0,
303+
);
304+
const attrDefFormatted = " ".repeat(indentsKey) + this.key + "= ";
305+
if (this.getType() === SepticValueTypes.stringList) {
306+
return (
307+
attrDefFormatted +
308+
formatStringList(this.values.map((val) => val.value))
309+
);
310+
} else if (this.getType() === SepticValueTypes.numericList) {
311+
return (
312+
attrDefFormatted +
313+
formatNumericList(this.values.map((val) => val.value))
314+
);
315+
} else if (this.getValues().length > 1) {
316+
return (
317+
attrDefFormatted +
318+
formatList(this.values.map((val) => val.value))
319+
);
320+
} else {
321+
return attrDefFormatted + (this.values[0]?.value || "");
322+
}
323+
}
324+
}
325+
326+
function formatStringList(values: string[]): string {
327+
let formatted = `${values.length - 1}`;
328+
values.slice(1).forEach((val, ind) => {
329+
if (ind % maxNumberAttrValuesPerLine === 0) {
330+
formatted += "\n" + " ".repeat(indentsAttributeValuesStart) + val;
331+
} else {
332+
formatted += " ".repeat(spacesBetweenValues) + val;
333+
}
334+
});
335+
return formatted;
336+
}
337+
338+
function formatNumericList(values: string[]): string {
339+
let formatted = `${values.length - 1}`;
340+
values.slice(1).forEach((val, ind) => {
341+
const spaces = Math.max(spacesBetweenIntValues - val.length, 1);
342+
formatted +=
343+
ind === 0 ? " ".repeat(spaces - 1) + val : " ".repeat(spaces) + val;
344+
});
345+
return formatted;
346+
}
347+
348+
function formatList(values: string[]): string {
349+
let formatted = `${values.length - 1}`;
350+
values.slice(1).forEach((val) => {
351+
formatted += " ".repeat(spacesBetweenValues) + val;
352+
});
353+
return formatted;
264354
}
265355

266356
export class SepticIdentifier extends SepticBase {

packages/septic/src/generator.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
SepticAttribute,
3+
SepticAttributeValue,
4+
SepticIdentifier,
5+
SepticObject,
6+
SepticTokenType,
7+
} from "./elements";
8+
import { SepticMetaInfoProvider, SepticObjectDoc } from "./metaInfoProvider";
9+
10+
export class SepticObjectGenerator {
11+
private metaInfoProvider: SepticMetaInfoProvider;
12+
constructor() {
13+
this.metaInfoProvider = SepticMetaInfoProvider.getInstance();
14+
}
15+
16+
public createObject(
17+
type: string,
18+
identifier: string,
19+
attributes: {
20+
key: string;
21+
type: SepticTokenType;
22+
values: string[];
23+
}[] = [],
24+
): SepticObject {
25+
const objectDoc = this.metaInfoProvider.getObjectDocumentation(type);
26+
if (!objectDoc) {
27+
throw new Error(`Unknown object type: ${type}`);
28+
}
29+
const obj = objectDocToObject(identifier, objectDoc);
30+
for (const attr of attributes) {
31+
const attribute = obj.getAttribute(attr.key);
32+
if (attribute) {
33+
attribute.setValues(createAttrValues(attr.values, attr.type));
34+
}
35+
}
36+
return obj;
37+
}
38+
}
39+
40+
function objectDocToObject(
41+
identifier: string,
42+
doc: SepticObjectDoc,
43+
): SepticObject {
44+
const obj = new SepticObject(doc.name, new SepticIdentifier(identifier));
45+
for (const attrDoc of doc.attributes) {
46+
if (attrDoc.noCnfg) {
47+
continue;
48+
}
49+
const attr = new SepticAttribute(attrDoc.name);
50+
const tokenType =
51+
attrDoc.dataType == "string"
52+
? SepticTokenType.string
53+
: SepticTokenType.numeric;
54+
const attrValues = createAttrValues(attrDoc.default, tokenType);
55+
attr.setValues(attrValues);
56+
obj.addAttribute(attr);
57+
}
58+
return obj;
59+
}
60+
61+
export function createAttrValues(
62+
values: string[],
63+
type: SepticTokenType,
64+
): SepticAttributeValue[] {
65+
const attrValues: SepticAttributeValue[] = [];
66+
if (values.length > 1) {
67+
attrValues.push(
68+
new SepticAttributeValue(
69+
`${values.length}`,
70+
SepticTokenType.numeric,
71+
),
72+
);
73+
}
74+
values.forEach((val) => {
75+
attrValues.push(new SepticAttributeValue(val, type));
76+
});
77+
return attrValues;
78+
}

0 commit comments

Comments
 (0)