ini/src/ini.ts

1175 lines
31 KiB
TypeScript

import P from 'parsimmon';
import { Config as ConfigType, Constant, GroupMenu } from '@hyper-tuner/types';
import { ParserInterface } from './parserInterface';
export class INI implements ParserInterface {
space: P.Parser<any>;
expression: P.Parser<any>;
numbers: P.Parser<any>;
name: P.Parser<any>;
equal: P.Parser<any>;
quote: P.Parser<any>;
quotes: [P.Parser<any>, P.Parser<any>];
comma: P.Parser<any>;
size: P.Parser<any>;
delimiter: [P.Parser<any>, P.Parser<any>, P.Parser<any>];
notQuote: P.Parser<any>;
sqrBrackets: [P.Parser<any>, P.Parser<any>];
inQuotes: P.Parser<any>;
values: P.Parser<any>;
lines: string[];
currentPage?: number;
currentDialog?: string;
currentPanel?: string;
currentMenu?: string;
currentGroupMenu?: string;
currentCurve?: string;
currentTable?: string;
result: ConfigType;
constructor(buffer: ArrayBuffer) {
this.space = P.optWhitespace;
this.expression = P.regexp(/{.+?}|(([a-z])([A-Za-z\d]+))/);
this.numbers = P.regexp(/[0-9.E-]*/);
this.name = P.regexp(/[0-9a-z_\\-]*/i);
this.equal = P.string('=');
this.quote = P.string('"');
this.quotes = [this.quote, this.quote];
this.comma = P.string(',');
this.size = P.regexp(/U08|S08|U16|S16|U32|S32|S64|F32|ASCII/);
this.delimiter = [this.space, this.comma, this.space];
this.notQuote = P.regexp(/[^"]*/);
this.sqrBrackets = [P.string('['), P.string(']')];
this.inQuotes = this.notQuote.trim(this.space).wrap(...this.quotes);
this.values = P.regexp(/[^,;]*/).trim(this.space).sepBy(this.comma);
this.lines = new TextDecoder().decode(buffer).split('\n');
this.currentPage = undefined;
this.currentDialog = undefined;
this.currentPanel = undefined;
this.currentMenu = undefined;
this.currentCurve = undefined;
this.currentTable = undefined;
this.result = {
megaTune: {
signature: '',
MTversion: 0,
queryCommand: '',
versionInfo: '',
},
tunerStudio: {
iniSpecVersion: 0,
},
defines: {},
pcVariables: {},
constants: {
pages: [],
},
menus: {},
dialogs: {},
curves: {},
tables: {},
outputChannels: {},
datalog: {},
help: {},
};
}
parse(): this {
this.parseSections();
return this;
}
getResults(): ConfigType {
return this.result;
}
private parseSections() {
let section: string;
this.lines.forEach((raw) => {
const line = raw.trim();
// skip empty lines and lines with comments only
// skip #if for now
if (
line === '' ||
line.startsWith(';') ||
(line.startsWith('#') && !line.startsWith('#define'))
) {
return;
}
const result = P.seqObj<any>(
['section', P.letters.wrap(P.string('['), P.string(']'))],
P.all,
).parse(line);
// top level section found
if (result.status) {
section = result.value.section.trim();
return;
}
if (section) {
this.parseSectionLine(section, line);
}
});
}
private parseSectionLine(section: string, line: string) {
switch (section) {
case 'MegaTune': {
this.parseKeyValueFor('megaTune', line);
break;
}
case 'TunerStudio': {
this.parseKeyValueFor('tunerStudio', line);
break;
}
case 'PcVariables': {
this.parsePcVariables(line);
break;
}
case 'Constants': {
this.parseConstants(line);
break;
}
case 'Menu': {
this.parseMenu(line);
break;
}
case 'SettingContextHelp': {
this.parseKeyValueFor('help', line);
break;
}
case 'UserDefined': {
this.parseDialogs(line);
break;
}
case 'CurveEditor': {
this.parseCurves(line);
break;
}
case 'TableEditor': {
this.parseTables(line);
break;
}
case 'OutputChannels': {
this.parseOutputChannels(line);
break;
}
case 'Datalog': {
this.parseDatalog(line);
break;
}
default:
break;
}
}
private parseDatalog(line: string) {
const base: any = [
P.string('entry'),
this.space,
this.equal,
this.space,
['name', this.name],
...this.delimiter,
];
const type: any = [...this.delimiter, ['type', P.regexp(/float|int/)]];
const format: any = [...this.delimiter, ['format', this.notQuote.wrap(...this.quotes)]];
const noConditions = [
...base,
['label', this.notQuote.wrap(...this.quotes)],
...type,
...format,
];
const withConditions = [...noConditions, ...this.delimiter, ['condition', this.expression]];
const labelExpression = [...base, ['label', this.expression], ...type, ...format];
const labelExpressionWithCondition = [
...labelExpression,
...this.delimiter,
['condition', this.expression],
];
const result: any = P.seqObj(...labelExpressionWithCondition, P.all)
.or(P.seqObj(...withConditions, P.all))
.or(P.seqObj(...labelExpression, P.all))
.or(P.seqObj(...noConditions, P.all))
.tryParse(line);
this.result.datalog[result.name] = {
name: result.name,
label: INI.sanitize(result.label),
type: result.type,
format: INI.sanitize(result.format),
condition: result.condition ? INI.sanitize(result.condition) : '',
};
}
private parseOutputChannels(line: string) {
try {
const result = this.parseConstAndVar(line);
this.result.outputChannels[result.name] = {
type: result.type,
size: result.size,
offset: Number(result.offset),
units: INI.sanitize(result.units),
scale: INI.isNumber(result.scale) ? Number(result.scale) : INI.sanitize(result.scale),
transform: INI.isNumber(result.transform)
? Number(result.transform)
: INI.sanitize(result.transform),
};
return;
} catch (_) {
const base: any = [['name', this.name], this.space, this.equal, this.space];
// TODO: throttle = { tps }, "%"
// ochGetCommand = "r\$tsCanId\x30%2o%2c"
// ochBlockSize = 117
// coolant = { coolantRaw - 40 }
const result = P.seqObj<any>(...base, ['value', this.notQuote.wrap(...this.quotes)], P.all)
.or(P.seqObj<any>(...base, ['value', this.expression], P.all))
.or(P.seqObj<any>(...base, ['value', this.numbers], P.all))
.tryParse(line);
this.result.outputChannels[result.name] = {
value: INI.sanitize(result.value),
};
}
}
private parseTables(line: string) {
// table = veTable1Tbl, veTable1Map, "VE Table", 2
const tableResult = P.seqObj<any>(
P.string('table'),
this.space,
this.equal,
this.space,
['name', this.name],
...this.delimiter,
['map', this.name],
...this.delimiter,
['title', this.inQuotes],
...this.delimiter,
['page', P.digits],
P.all,
).parse(line);
if (tableResult.status) {
this.currentTable = tableResult.value.name;
this.result.tables[this.currentTable!] = {
map: tableResult.value.map,
title: INI.sanitize(tableResult.value.title),
page: Number(tableResult.value.page),
xBins: [],
yBins: [],
xyLabels: [],
zBins: [],
gridHeight: 0,
gridOrient: [],
upDownLabel: [],
};
return;
}
// topicHelp = "http://speeduino.com/wiki/index.php/Tuning"
const helpResult = P.seqObj<any>(
P.string('topicHelp'),
this.space,
this.equal,
this.space,
['help', this.inQuotes],
P.all,
).parse(line);
if (helpResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].help = INI.sanitize(helpResult.value.help);
return;
}
// xBins = rpmBins, rpm
// yBins = fuelLoadBins, fuelLoad
// xyLabels = "RPM", "Fuel Load: "
// zBins = veTable
// gridOrient = 250, 0, 340
// upDownLabel = "(RICHER)", "(LEANER)"
const parseBins = (name: string) =>
P.seqObj<any>(
P.string(name),
this.space,
this.equal,
this.space,
['values', this.values],
P.all,
).parse(line);
const xBinsResult = parseBins('xBins');
if (xBinsResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].xBins = xBinsResult.value.values.map(INI.sanitize);
return;
}
const yBinsResult = parseBins('yBins');
if (yBinsResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].yBins = yBinsResult.value.values.map(INI.sanitize);
return;
}
const yxLabelsResult = parseBins('xyLabels');
if (yxLabelsResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].xyLabels = yxLabelsResult.value.values.map(
INI.sanitize,
);
return;
}
const zBinsResult = parseBins('zBins');
if (zBinsResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].zBins = zBinsResult.value.values.map(INI.sanitize);
return;
}
// gridHeight = 2.0
const gridHeightResult = P.seqObj<any>(
P.string('gridHeight'),
this.space,
this.equal,
this.space,
['gridHeight', this.numbers],
P.all,
).parse(line);
if (gridHeightResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].gridHeight = Number(gridHeightResult.value.gridHeight);
return;
}
const gridOrientResult = parseBins('gridOrient');
if (gridOrientResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].gridOrient = gridOrientResult.value.values.map(Number);
return;
}
const upDownResult = parseBins('upDownLabel');
if (upDownResult.status) {
if (!this.currentTable) {
throw new Error('Table not set');
}
this.result.tables[this.currentTable].upDownLabel = upDownResult.value.values.map(
INI.sanitize,
);
}
}
private parseCurves(line: string) {
// curve = time_accel_tpsdot_curve, "TPS based AE"
const curveResult = P.seqObj<any>(
P.string('curve'),
this.space,
this.equal,
this.space,
['name', this.name],
...this.delimiter,
['title', this.inQuotes],
P.all,
).parse(line);
if (curveResult.status) {
this.currentCurve = curveResult.value.name;
this.result.curves[this.currentCurve!] = {
title: INI.sanitize(curveResult.value.title),
labels: [],
xAxis: [],
yAxis: [],
xBins: [],
yBins: [],
size: [],
};
return;
}
// columnLabel = "TPSdot", "Added"
const labelsResult = P.seqObj<any>(
P.string('columnLabel'),
this.space,
this.equal,
this.space,
['labels', this.values],
P.all,
).parse(line);
if (labelsResult.status) {
if (!this.currentCurve) {
throw new Error('Curve not set');
}
this.result.curves[this.currentCurve].labels = labelsResult.value.labels.map(INI.sanitize);
return;
}
// xAxis = 0, 1200, 6
// yAxis = 0, 1200, 6
// xBins = taeBins, TPSdot
// yBins = taeRates
const parseAxis = (name: string) =>
P.seqObj<any>(
P.string(name),
this.space,
this.equal,
this.space,
['values', this.values],
P.all,
).parse(line);
const xAxisResult = parseAxis('xAxis');
if (xAxisResult.status) {
if (!this.currentCurve) {
throw new Error('Curve not set');
}
this.result.curves[this.currentCurve].xAxis = xAxisResult.value.values.map((val: string) => {
return INI.isNumber(val) ? Number(val) : INI.sanitize(val);
});
return;
}
const yAxisResult = parseAxis('yAxis');
if (yAxisResult.status) {
if (!this.currentCurve) {
throw new Error('Curve not set');
}
this.result.curves[this.currentCurve].yAxis = yAxisResult.value.values.map((val: string) => {
return INI.isNumber(val) ? Number(val) : INI.sanitize(val);
});
return;
}
const xBinsResult = parseAxis('xBins');
if (xBinsResult.status) {
if (!this.currentCurve) {
throw new Error('Curve not set');
}
this.result.curves[this.currentCurve].xBins = xBinsResult.value.values.map((val: string) => {
return INI.isNumber(val) ? Number(val) : INI.sanitize(val);
});
return;
}
const yBinsResult = parseAxis('yBins');
if (yBinsResult.status) {
if (!this.currentCurve) {
throw new Error('Curve not set');
}
this.result.curves[this.currentCurve].yBins = yBinsResult.value.values.map((val: string) => {
return INI.isNumber(val) ? Number(val) : INI.sanitize(val);
});
return;
}
const size = parseAxis('size');
if (size.status) {
if (!this.currentCurve) {
throw new Error('Curve not set');
}
this.result.curves[this.currentCurve].size = size.value.values.map((val: string) => {
return INI.isNumber(val) ? Number(val) : INI.sanitize(val);
});
}
}
private parseDialogs(line: string) {
const dialogBase: any = [
P.string('dialog'),
this.space,
this.equal,
this.space,
['name', this.name],
...this.delimiter,
['title', this.inQuotes],
];
const dialogResult = P.seqObj<any>(
...dialogBase,
...this.delimiter,
['layout', this.name],
P.all,
)
.or(P.seqObj<any>(...dialogBase, P.all))
.parse(line);
if (dialogResult.status) {
this.currentDialog = dialogResult.value.name;
this.result.dialogs[this.currentDialog!] = {
title: INI.sanitize(dialogResult.value.title),
layout: dialogResult.value.layout,
panels: {},
fields: [],
};
return;
}
// panel = knock_window_angle_curve
const panelBase: any = [
P.string('panel'),
this.space,
this.equal,
this.space,
['name', this.name],
];
// panel = knock_window_angle_curve, West
const panelWithLayout = [...panelBase, ...this.delimiter, ['layout', this.name]];
// panel = flex_fuel_curve, { flexEnabled }
const panelWithCondition = [...panelBase, ...this.delimiter, ['condition', this.expression]];
const panelResult = P.seqObj<any>(
...panelWithLayout,
...this.delimiter,
['condition', this.expression],
P.all,
)
.or(P.seqObj<any>(...panelWithCondition, P.all))
.or(P.seqObj<any>(...panelWithLayout, P.all))
.or(P.seqObj<any>(...panelBase, P.all))
.parse(line);
if (panelResult.status) {
if (!this.currentDialog) {
throw new Error('Dialog not set');
}
this.currentPanel = panelResult.value.name;
this.result.dialogs[this.currentDialog!].panels[this.currentPanel!] = {
layout: panelResult.value.layout,
condition: panelResult.value.condition,
fields: [],
panels: {},
};
return;
}
// field = "Injector Layout"
const fieldBase: any = [
P.string('field'),
this.space,
this.equal,
this.space,
['title', this.notQuote.wrap(...this.quotes)],
];
// field = "Injector Layout", injLayout
const fieldWithName = [...fieldBase, ...this.delimiter, ['name', this.name]];
// field = "Low (E0) ", flexFreqLow, { flexEnabled }
const fieldWithCondition = [
...fieldWithName,
...this.delimiter,
['condition', this.expression],
];
// NOTE: this is probably a mistake, investigate that
// field = "AUX Input 0", caninput_sel0a, {}, { (!enable_secondarySerial && (!enable_intcan || (enable_intcan && intcan_available == 0))) }
const fieldWithDoubleCondition = [
...fieldWithName,
...this.delimiter,
P.regexp(/{.*?}/),
...this.delimiter,
['condition', this.expression],
];
// field = "!Warning: The board you have selected may not have enough channels for sequential fuel!", {}, {}, { injLayout == 3 && !sequentialFuelAvailable }
const fieldWithTripleCondition = [
...fieldBase,
...this.delimiter,
P.regexp(/{.*?}/),
...this.delimiter,
P.regexp(/{.*?}/),
...this.delimiter,
['condition', this.expression],
];
const fieldResult = P.seqObj<any>(...fieldWithTripleCondition, P.all)
.or(P.seqObj<any>(...fieldWithDoubleCondition, P.all))
.or(P.seqObj<any>(...fieldWithCondition, P.all))
.or(P.seqObj<any>(...fieldWithName, P.all))
.or(P.seqObj<any>(...fieldBase, P.all))
.parse(line);
if (fieldResult.status) {
if (!this.currentDialog) {
throw new Error('Dialog not set');
}
this.result.dialogs[this.currentDialog!].fields.push({
name: fieldResult.value.name || '_fieldText_',
title: INI.sanitize(fieldResult.value.title),
condition: fieldResult.value.condition,
});
return;
}
// topicHelp = "https://wiki.speeduino.com/en/configuration/Engine_Constants"
const helpResult = P.seqObj<any>(
P.string('topicHelp'),
this.space,
this.equal,
this.space,
['help', this.notQuote.wrap(...this.quotes)],
P.all,
).parse(line);
if (this.currentDialog) {
if (helpResult.status) {
this.result.dialogs[this.currentDialog!].help = INI.sanitize(helpResult.value.help);
}
}
// TODO: missing fields:
// - settingSelector
// - commandButton
// - displayOnlyField
}
private parseKeyValueFor(section: string, line: string) {
const { key, value } = this.parseKeyValue(line);
if (this.result[section][key]) {
// TODO: enable this for linting duplicates
return;
// throw new Error(`Key: ${key} for section: ${section} already exist`);
}
this.result[section][key] = value;
}
private parseKeyValue(line: string) {
const base: any = [['key', this.name], this.space, this.equal, this.space];
const result = P.seqObj<any>(...base, ['value', this.notQuote.wrap(...this.quotes)], P.all)
.or(P.seqObj<any>(...base, ['value', this.numbers], P.all))
.tryParse(line);
return {
key: result.key as string,
value: INI.isNumber(result.value) ? Number(result.value) : INI.sanitize(result.value),
};
}
private parseMenu(line: string) {
// skip root "menuDialog = main" for now
if (line.startsWith('menuDialog')) {
return;
}
const menuResult = P.seqObj<any>(
P.string('menu'),
this.space,
this.equal,
this.space,
['name', this.inQuotes],
P.all,
).parse(line);
if (menuResult.status) {
const title = INI.sanitize(menuResult.value.name).replace(/&/g, '');
const name = title.toLowerCase().replace(/([^\w]\w)/g, (g) => g[1].toUpperCase()); // camelCase
this.currentMenu = name;
this.result.menus[this.currentMenu] = {
title: INI.sanitize(title),
subMenus: {},
};
return;
}
if (this.currentMenu) {
// parse groupMenu
// groupMenu = "Engine Protection"
const groupMenuResult = P.seqObj<any>(
P.string('groupMenu'),
this.space,
this.equal,
this.space,
['title', this.notQuote.wrap(...this.quotes)],
P.all,
).parse(line);
if (groupMenuResult.status) {
const title = INI.sanitize(groupMenuResult.value.title);
const name = title.toLowerCase().replace(/([^\w]\w)/g, (g) => g[1].toUpperCase()); // camelCase
this.currentGroupMenu = name;
this.result.menus[this.currentMenu].subMenus[name] = {
type: 'groupMenu',
title: INI.sanitize(title),
groupChildMenus: {},
};
return;
}
// parse groupChildMenu
if (this.currentGroupMenu && line.startsWith('groupChildMenu')) {
// groupChildMenu = std_separator
const base: any = [
P.string('groupChildMenu'),
this.space,
this.equal,
this.space,
['name', this.name],
];
// groupChildMenu = engineProtection, "Common Engine Protection"
const withTitle: any = [
...base,
...this.delimiter,
['title', this.notQuote.wrap(...this.quotes)],
];
// groupChildMenu = revLimiterDialog, "Rev Limiters", { engineProtectType }
const full: any = [...withTitle, ...this.delimiter, ['condition', this.expression], P.all];
const groupChildMenuResult = P.seqObj<any>(...full, P.all)
.or(P.seqObj<any>(...withTitle, P.all))
.or(P.seqObj<any>(...base, P.all))
.tryParse(line);
(
this.result.menus[this.currentMenu].subMenus[this.currentGroupMenu] as GroupMenu
).groupChildMenus[groupChildMenuResult.name] = {
title: INI.sanitize(groupChildMenuResult.title),
condition: groupChildMenuResult.condition
? INI.sanitize(groupChildMenuResult.condition)
: '',
};
return;
}
// subMenu = std_separator
const base: any = [
P.string('subMenu'),
this.space,
this.equal,
this.space,
['name', this.name],
];
// subMenu = io_summary, "I/O Summary"
const withTitle: any = [
...base,
...this.delimiter,
['title', this.notQuote.wrap(...this.quotes)],
];
// subMenu = egoControl, "AFR/O2", 3
const withPage: any = [...withTitle, ...this.delimiter, ['page', P.digits]];
// subMenu = fuelTemp_curve, "Fuel Temp Correction", { flexEnabled }
const withCondition: any = [
...withTitle,
...this.delimiter,
['condition', this.expression],
P.all,
];
// subMenu = inj_trimad_B, "Sequential fuel trim (5-8)", 9, { nFuelChannels >= 5 }
const full: any = [...withPage, ...this.delimiter, ['condition', this.expression]];
const subMenuResult = P.seqObj<any>(...full, P.all)
.or(P.seqObj<any>(...withCondition, P.all))
.or(P.seqObj<any>(...withPage, P.all))
.or(P.seqObj<any>(...withTitle, P.all))
.or(P.seqObj<any>(...base, P.all))
.tryParse(line);
this.result.menus[this.currentMenu].subMenus[subMenuResult.name] = {
type: 'subMenu',
title: INI.sanitize(subMenuResult.title),
page: Number(subMenuResult.page || 0),
condition: subMenuResult.condition ? INI.sanitize(subMenuResult.condition) : '',
};
}
}
private parseDefines(line: string) {
const result = P.seqObj<any>(
P.string('#define'),
this.space,
['name', this.name],
this.space,
this.equal,
this.space,
['values', this.values],
P.all,
).tryParse(line);
this.result.defines[result.name] = result.values.map(INI.sanitize);
const resolved = this.result.defines[result.name].flatMap((val) => {
return val.startsWith('$') ? this.result.defines[val.slice(1)] : val;
});
this.result.defines[result.name] = resolved;
}
private parsePcVariables(line: string) {
if (line.startsWith('#define')) {
this.parseDefines(line);
return;
}
const result = this.parseConstAndVar(line, true);
let constant = {} as Constant;
switch (result.type) {
case 'scalar': {
constant = {
type: result.type,
size: result.size,
units: INI.sanitize(result.units),
scale: INI.numberOrExpression(result.scale),
transform: INI.numberOrExpression(result.transform),
min: INI.numberOrExpression(result.min),
max: INI.numberOrExpression(result.max),
digits: Number(result.digits),
};
break;
}
case 'array': {
constant = {
type: result.type,
size: result.size,
shape: INI.arrayShape(result.shape),
units: INI.sanitize(result.units),
scale: INI.numberOrExpression(result.scale),
transform: INI.numberOrExpression(result.transform),
min: INI.numberOrExpression(result.min),
max: INI.numberOrExpression(result.max),
digits: Number(result.digits),
};
break;
}
case 'bits': {
constant = {
type: result.type,
size: result.size,
address: result.address.split(':').map(Number),
values: this.resolveBitsValues(result.name, result.values || []),
};
break;
}
case 'string': {
constant = {
type: result.type,
size: result.size,
length: Number(result.length),
};
break;
}
default:
break;
}
this.result.pcVariables[result.name] = constant;
}
private parseConstants(line: string) {
if (line.startsWith('#define')) {
this.parseDefines(line);
return;
}
const page = P.seqObj<any>(
P.string('page'),
this.space,
this.equal,
this.space,
['page', P.digits],
P.all,
).parse(line);
if (page.status) {
this.currentPage = Number(page.value.page) - 1;
return;
}
if (INI.isNumber(this.currentPage)) {
const result = this.parseConstAndVar(line);
if (!this.result.constants.pages[this.currentPage!]) {
this.result.constants.pages[this.currentPage!] = {
number: this.currentPage! + 1,
size: 0,
data: {},
};
}
let constant = {} as Constant;
switch (result.type) {
case 'scalar': {
constant = {
type: result.type,
size: result.size,
offset: Number(result.offset),
units: INI.sanitize(result.units),
scale: INI.numberOrExpression(result.scale),
transform: INI.numberOrExpression(result.transform),
min: INI.numberOrExpression(result.min),
max: INI.numberOrExpression(result.max),
digits: Number(result.digits),
};
break;
}
case 'array': {
constant = {
type: result.type,
size: result.size,
offset: Number(result.offset),
shape: INI.arrayShape(result.shape),
units: INI.sanitize(result.units),
scale: INI.numberOrExpression(result.scale),
transform: INI.numberOrExpression(result.transform),
min: INI.numberOrExpression(result.min),
max: INI.numberOrExpression(result.max),
digits: Number(result.digits),
};
break;
}
case 'bits': {
constant = {
type: result.type,
size: result.size,
offset: Number(result.offset),
address: result.address.split(':').map(Number),
values: this.resolveBitsValues(result.name, result.values || []),
};
break;
}
default:
break;
}
if (this.result.constants.pages[this.currentPage!].data[result.name]) {
// TODO: if else
return;
}
this.result.constants.pages[this.currentPage!].data[result.name] = constant;
}
}
private resolveBitsValues(name: string, values: string[]) {
return values
.flatMap((val: string) => {
const resolve = () => {
const defineName = INI.sanitize(val.slice(1)); // name without $
const resolved = this.result.defines[defineName];
if (!resolved) {
throw new Error(`Unable to resolve bits values for ${name}`);
}
return resolved;
};
return val.startsWith('$') ? resolve() : INI.sanitize(val);
})
.filter((val) => val !== '');
}
private parseConstAndVar(line: string, asPcVariable = false) {
const address: any = [
[
'address',
P.regexp(/\d+:\d+/)
.trim(this.space)
.wrap(...this.sqrBrackets),
],
];
// first common (eg. name = scalar, U08, 3,)
const base: any = (type: string) => {
let list = [
['name', this.name],
this.space,
this.equal,
this.space,
['type', P.string(type)],
...this.delimiter,
['size', this.size],
];
// pcVariables don't have "offset"
if (!asPcVariable) {
list = [...list, ...[...this.delimiter, ['offset', P.digits]]];
}
return list;
};
const scalarShortRest: any = [
['units', P.alt(this.expression, this.inQuotes)],
...this.delimiter,
['scale', P.alt(this.expression, this.numbers)],
...this.delimiter,
['transform', P.alt(this.expression, this.numbers)],
];
const scalarRest: any = [
...scalarShortRest,
...this.delimiter,
['min', P.alt(this.expression, this.numbers)],
...this.delimiter,
['max', P.alt(this.expression, this.numbers)],
...this.delimiter,
['digits', P.alt(this.expression, P.digits)],
P.all,
];
// normal scalar
const scalar = P.seqObj<any>(...base('scalar'), ...this.delimiter, ...scalarRest);
// short version of scalar (e.g. 'divider')
const scalarShort = P.seqObj<any>(
...base('scalar'),
...this.delimiter,
...scalarShortRest,
P.all,
);
// normal version of array
const array = P.seqObj<any>(
...base('array'),
...this.delimiter,
[
'shape',
P.regexp(/\d+\s*(x\s*\d+)*/)
.trim(this.space)
.wrap(...this.sqrBrackets),
],
...this.delimiter,
...scalarRest,
);
// normal version of bits
const bits = P.seqObj<any>(
...base('bits'),
...this.delimiter,
...address,
...this.delimiter,
['values', this.values],
P.all,
);
// short version of bits
const bitsShort = P.seqObj<any>(...base('bits'), ...this.delimiter, ...address, P.all);
// string (in pcVariables)
const string = P.seqObj<any>(...base('string'), ...this.delimiter, ['length', P.digits], P.all);
// predefined constant continuousChannelValue (in pcVariables)
// TODO: investigate this
const continuousChannelValue = P.seqObj<any>(
['name', this.name],
this.space,
this.equal,
this.space,
['reference', this.name],
...this.delimiter,
['channel', this.name],
P.all,
);
return scalar
.or(scalarShort)
.or(array)
.or(bits)
.or(bitsShort)
.or(string)
.or(continuousChannelValue)
.tryParse(line);
}
private static numberOrExpression = (val: string | undefined | null) => {
return INI.isNumber(val || '0') ? Number(val || 0) : INI.sanitize(`${val}`);
};
private static sanitize = (val: any) => {
return val === undefined ? '' : `${val}`.replace(/"/g, '').replace(/\s+/g, ' ').trim();
};
private static isNumber = (val: any) => !Number.isNaN(Number(val));
private static arrayShape = (val: string) => {
const parts = INI.sanitize(val).split('x');
return {
columns: Number(parts[0]),
rows: parts[1] ? Number(parts[1]) : 0,
};
};
}