import fs from "fs"; import path from "path"; import xmlFormatter from "xml-formatter"; import { ScpiCommand, ScpiCommandTreeNode, addCommandToTree } from "instrument/commands-tree"; import { IEnum, ISubsystem, IParameter, IResponse, getSdlSemanticTypeForParameter, getSdlParameterType, getSdlSemanticTypeForResponse, getSdlResponseType } from "instrument/scpi"; import { readTextFile } from "eez-studio-shared/util-electron"; import { CommandLineEnding } from "eez-studio-shared/extensions/extension"; //////////////////////////////////////////////////////////////////////////////// const MODULE_DOCS_FOLDER = "docs"; //////////////////////////////////////////////////////////////////////////////// export interface IInstrumentProperties { connection?: { ethernet?: { port: number; }; serial?: { baudRates: number[]; defaultBaudRate: number; defaultDataBits: 8 | 7 | 6 | 5; defaultStopBits: 1 | 2; defaultParity: "none" | "even" | "mark" | "odd" | "space"; defaultFlowControl: "none" | "xon/xoff" | "rts/cts"; }; usbtmc?: { idVendor: number | string | undefined; idProduct: number | string | undefined; }; webSimulator?: { src: string; width: number; height: number; }; }; channels?: { maxVoltage?: number; maxCurrent?: number; maxPower?: number; }[]; lists?: { maxPoints?: number; minDwell?: number; maxDwell?: number; dwellDigits?: number; voltageDigits?: number; currentDigits?: number; }; fileDownload?: { shortFileName?: boolean; startCommand?: string; fileSizeCommand?: string; sendChunkCommand?: string; finishCommand?: string; abortCommand?: string; chunkSize?: number; favoriteDestinationPaths?: { ext?: string; path: string; }[]; }; commandLineEnding?: CommandLineEnding; } export interface IdfProperties { buildConfiguration: string; extensionName: string; image: string; idn: string; idfName: string; idfShortName: string; idfFirmwareVersion: string; idfGuid: string; idfRevisionNumber: string; idfDescription: string; idfSupportedModels: string; idfRevisionComments: string; idfAuthor: string; sdlFriendlyName: string; //properties: IInstrumentProperties; useDashboardProjects: string[]; instrumentCommands?: { command: string; helpLink: string | undefined; }[]; } //////////////////////////////////////////////////////////////////////////////// async function buildPackageJson(idf: IdfProperties, properties: any) { // remove objID properties = JSON.parse( JSON.stringify( properties, (key: string | number, value: any) => { if (key === "objID") { return undefined; } return value; }, 2 ) ); properties.dashboards = []; for (const useDashboardProject of idf.useDashboardProjects) { const data = await readTextFile(useDashboardProject); const json = JSON.parse(data); properties.dashboards.push({ title: json.settings.general.title || path.basename(useDashboardProject), icon: json.settings.general.icon }); } properties.instrumentCommands = idf.instrumentCommands; return JSON.stringify( { id: idf.idfGuid, name: idf.extensionName, description: idf.idfDescription, displayName: idf.idfName || idf.extensionName, version: idf.idfRevisionNumber, author: idf.idfAuthor, "eez-studio": properties }, undefined, 2 ); } function quoteAttr(str: string) { return (str || "") .replace(/&/g, "&") .replace(/'/g, "'") .replace(/"/g, """) .replace(//g, ">") .replace(/\r\n/g, " ") .replace(/[\r\n]/g, " "); } function buildIdf(idf: IdfProperties) { let extensionName = quoteAttr(idf.extensionName); let name = quoteAttr(idf.idfName); let shortName = quoteAttr(idf.idfShortName); let firmwareVersion = quoteAttr(idf.idfFirmwareVersion); let guid = quoteAttr(idf.idfGuid); let revisionNumber = quoteAttr(idf.idfRevisionNumber); let description = quoteAttr(idf.idfDescription); let supportedModels = quoteAttr(idf.idfSupportedModels); let revisionComments = quoteAttr(idf.idfRevisionComments); let author = quoteAttr(idf.idfAuthor); return ` `; } function buildEnumDefinitions(enums: IEnum[]) { return enums .map( enumeration => ` ${enumeration.members .map( member => `` ) .join("")} ` ) .join(""); } function buildParameters(parameters: IParameter[]) { if (parameters.length === 0) { return ""; } return ` ${parameters .map( parameter => ` ${parameter.type .map(parameterType => getSdlParameterType(parameterType) ) .join("")} ` ) .join("")} `; } function buildResponse(response: IResponse) { return ` ${response.type .map(responseType => getSdlResponseType(responseType)) .join("")} `; } function buildCommand(command: ScpiCommand) { const description = quoteAttr(command.description || ""); let helpLinks = ""; let commandSyntaxes = ""; let querySyntaxes = ""; if (command.commandSyntax) { if (command.commandSyntax.url) { helpLinks += ``; } let sendsBackDataBlockAttr; if (command.commandSyntax.sendsBackDataBlock) { sendsBackDataBlockAttr = ' sendsBackDataBlock="1"'; } else { sendsBackDataBlockAttr = ""; } commandSyntaxes = `${buildParameters( command.commandSyntax.parameters )}`; } if (command.querySyntax) { if (command.querySyntax.url) { if (helpLinks) { helpLinks += "\n"; } helpLinks += ``; } querySyntaxes = ` ${buildParameters(command.querySyntax.parameters)} ${buildResponse( command.querySyntax.response )} `; } return `${description}${helpLinks}${commandSyntaxes}${querySyntaxes}`; } function buildCommonCommand(command: ScpiCommand) { const name = quoteAttr(command.name); const commmand = buildCommand(command); return `${commmand}`; } function filterSubsystemCommands(idf: IdfProperties, subsystem: ISubsystem) { return subsystem.commands.filter( command => !command.usedIn || command.usedIn.indexOf(idf.buildConfiguration) !== -1 ); } function buildCommonCommands(idf: IdfProperties, subsystems: ISubsystem[]) { let commands = new Map(); subsystems.forEach(subsystem => { filterSubsystemCommands(idf, subsystem).forEach(subsystemCommand => { if (subsystemCommand.name.startsWith("*")) { let name = subsystemCommand.name.slice(1); let query = false; if (name.endsWith("?")) { name = name.slice(0, -1); query = true; } let command = commands.get(name); if (!command) { command = { name: name, description: subsystemCommand.description }; commands.set(name, command); } let url = subsystemCommand.helpLink; if (query) { command.querySyntax = { name: "*" + name + "?", url: url, parameters: subsystemCommand.parameters, response: subsystemCommand.response }; } else { command.commandSyntax = { name: "*" + name, url: url, parameters: subsystemCommand.parameters, sendsBackDataBlock: subsystemCommand.sendsBackDataBlock }; } } }); }); let commonCommands = ""; commands.forEach((command, name) => { commonCommands += buildCommonCommand(command); }); return commonCommands; } function getAliases(mnemonic: string) { let i; for (i = 0; i < mnemonic.length; i++) { if (mnemonic[i] != mnemonic[i].toUpperCase()) { break; } } return mnemonic.slice(0, i); } function buildNode(node: ScpiCommandTreeNode) { let result = ""; if (node.command) { result += `${buildCommand( node.command )}`; } if (node.nodes) { let nodes = node.nodes as ScpiCommandTreeNode[]; let tagName = !node.mnemonic ? "RootNode" : "Node"; nodes.forEach(node => { let aliases = quoteAttr(getAliases(node.mnemonic)); let mnemonic = quoteAttr(node.mnemonic); let content = buildNode(node); result += `<${tagName} aliases="${aliases}"${ node.optional ? ' default="true"' : "" }${ node.numericSuffix ? ` numericSuffix="${node.numericSuffix}"` : "" } mnemonic="${mnemonic}">${content}`; }); } return result; } function buildSubsystemCommands(idf: IdfProperties, subsystems: ISubsystem[]) { let tree: ScpiCommandTreeNode = { mnemonic: "" }; subsystems.forEach(subsystem => { filterSubsystemCommands(idf, subsystem).forEach(subsystemCommand => { if (subsystemCommand.name[0] !== "*") { addCommandToTree(subsystemCommand, tree); } }); }); return buildNode(tree); } function buildSdl( idf: IdfProperties, enums: IEnum[], subsystems: ISubsystem[] ) { const friendlyName = quoteAttr(idf.sdlFriendlyName); const enumDefinitions = buildEnumDefinitions(enums); const commonCommands = buildCommonCommands(idf, subsystems); const subsystemCommands = buildSubsystemCommands(idf, subsystems); return ` ${enumDefinitions} ${commonCommands} ${subsystemCommands} `; } export async function buildInstrumentExtension( idf: IdfProperties, subsystems: ISubsystem[], enums: IEnum[], moduleFilePath: string, imageFilePath: string | undefined, commandsDocFolderPath: string | undefined, projectFilePath: string, properties: any, isScpiInstrument: boolean ) { const archiver = await import("archiver"); return new Promise(async (resolve, reject) => { let extensionName = idf.extensionName; var output = fs.createWriteStream(moduleFilePath); var archive = archiver.default("zip", { zlib: { level: 9 } }); output.on("close", function () { resolve(); }); archive.on("warning", function (err: any) { reject(err); }); archive.on("error", function (err: any) { reject(err); }); archive.pipe(output); let webSimulatorFiles; const webSimulator = properties?.properties?.connection?.webSimulator; if (webSimulator) { webSimulatorFiles = webSimulator.files; delete webSimulator.files; } const packageJson = await buildPackageJson(idf, properties); archive.append(packageJson, { name: "package.json" }); if (isScpiInstrument) { const idfStr = buildIdf(idf); archive.append(xmlFormatter(idfStr), { name: extensionName + ".idf" }); } if (isScpiInstrument) { const sdl = buildSdl(idf, enums, subsystems); archive.append(xmlFormatter(sdl), { name: extensionName + ".sdl" }); } if (imageFilePath) { if (imageFilePath.startsWith("data:image")) { let i = imageFilePath.indexOf(","); archive.append( Buffer.from(imageFilePath.slice(i + 1), "base64"), { name: "image.png" } ); } else { archive.file(imageFilePath, { name: "image.png" }); } } if (commandsDocFolderPath) { archive.glob( "**/*", { cwd: commandsDocFolderPath, ignore: [".*"] }, { prefix: MODULE_DOCS_FOLDER } ); } if (webSimulatorFiles) { for (const file of webSimulatorFiles) { const srcFilePath = projectFilePath + "/" + file[0]; archive.file(srcFilePath, { name: file[1] }); } } for (let i = 0; i < idf.useDashboardProjects.length; i++) { archive.file(idf.useDashboardProjects[i], { name: `d${i}.eez-project` }); } archive.finalize(); }); }