/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/triple-slash-reference */ /// import { camelize, capitalize, converters, dashify, humanify, normalizeDeviceSpecification, packInfo, parseServiceSpecificationMarkdownToJSON, snakify, TYPESCRIPT_STATIC_NAMESPACE, isNumericType, genFieldInfo, } from "./jdspec" import { packetsToRegisters } from "./jdutils" // eslint-disable-next-line no-var, @typescript-eslint/no-explicit-any declare var process: any // eslint-disable-next-line no-var, @typescript-eslint/no-explicit-any declare var require: any // eslint-disable-next-line @typescript-eslint/no-explicit-any let fs: any const serviceBuiltins = [ "bootloader", "logger", "control", "infrastructure", "proxy", "uniquebrain", "rolemanager", "bridge", "dashboard", "devicescriptcloud", "cloudadapter", "devicescriptcondition", "devicescriptdebugger", "power" ] function values(o: jdspec.SMap): T[] { const r: T[] = [] for (const k of Object.keys(o)) r.push(o[k]) return r } function toPxtJson(spec: jdspec.ServiceSpec) { const { camelName, notes } = spec return JSON.stringify( { name: `jacdac-${dashify(camelName)}`, version: "0.0.0", description: notes["short"], files: ["constants.ts", "client.g.ts"], testFiles: ["test.ts"], supportedTargets: ["microbit", "arcade", "maker", "calliopemini"], dependencies: { core: "*", jacdac: "github:microsoft/pxt-jacdac", }, }, null, 4 ) } function pick(...values: number[]) { return values?.find(x => x !== undefined) } // add as needed const reservedJsWords: { [index: string]: boolean } = { switch: true, } function tsify(name: string) { if (reservedJsWords[name]) { return name + "_" } return name } const Reading = 0x101 const Intensity = 0x1 const Value = 0x2 function isEnabledReg(reg: jdspec.PacketInfo) { return ( reg.identifier === Intensity && reg.name === "enabled" && reg.fields.length === 1 && reg.fields[0].type === "bool" ) } function toMakeCodeClient(spec: jdspec.ServiceSpec) { const { shortId, name, camelName, packets, tags } = spec const nsc = TYPESCRIPT_STATIC_NAMESPACE const registers = packetsToRegisters(packets) let baseType = "Client" let isSimpleSensorClient = false const ctorArgs = [`${nsc}.SRV_${snakify(camelName).toUpperCase()}`, `role`] const regs = registers.filter(r => !!r).filter(r => !r.restricted) const reading = regs.find(reg => reg.identifier === Reading) const enabledReg = regs.find(isEnabledReg) const events = packets.filter(pkt => !pkt.derived && pkt.kind === "event") const ninstances = tags?.indexOf("input") > -1 ? 2 : 1 // TODO: pipes support const commands = packets.filter( pkt => !pkt.derived && !pkt.restricted && pkt.kind === "command" && pkt.fields.every(f => f.type !== "pipe") ) // use sensor base class if reading present if (reading) { isSimpleSensorClient = reading.fields.length === 1 && isNumericType(reading.fields[0]) baseType = isSimpleSensorClient ? `SimpleSensorClient` : `SensorClient` ctorArgs.push( `jacdac.${capitalize(spec.camelName)}RegPack.${capitalize( camelize(reading.name) )}` ) } const { unit: readingUnit } = reading ? genFieldInfo(reading, reading.fields[0]) : { unit: undefined } const readingUnitString = readingUnit ? ` (${readingUnit})` : "" const className = `${capitalize(camelName)}Client` const group = capitalize(spec.group || name) const registerFlags = (pkt: jdspec.PacketInfo) => { const flags: string[] = [] if (pkt.optional) flags.push("Optional") if (pkt.kind === "const") flags.push("Const") return flags.length == 0 ? "" : `, ${flags .map(n => `jacdac.RegisterClientFlags.${n}`) .join(" | ")}` } const toMetaComments = (...lines: string[]) => lines .filter(l => !!l) .map(l => " //% " + l) .join("\n") let weight = 100 const eventEnumName = events.length ? `${nsc}.${capitalize(spec.camelName)}Event` : undefined return `namespace modules { /** * ${(spec.notes["short"] || "").split("\n").join("\n * ")} **/ //% fixedInstances blockGap=8 export class ${className} extends jacdac.${baseType} { ${regs .filter(reg => reg.identifier !== Reading && !reg.client) .map( reg => ` private readonly _${camelize(reg.name)} : jacdac.RegisterClient<[${ packInfo(spec, reg, { isStatic: true, useBooleans: true }).types }]>;` ) .join("")} constructor(role: string) { super(${ctorArgs.join(", ")}) ${regs .filter(reg => reg.identifier !== Reading && !reg.client) .map( reg => ` this._${camelize(reg.name)} = this.addRegister<[${ packInfo(spec, reg, { isStatic: true, useBooleans: true }).types }]>(${nsc}.${capitalize(spec.camelName)}Reg.${capitalize( camelize(reg.name) )}, jacdac.${capitalize(spec.camelName)}RegPack.${capitalize( camelize(reg.name) )}${registerFlags(reg)})` ) .join("")} } ${regs .map(reg => { const { types } = packInfo(spec, reg, { isStatic: true, useBooleans: true, }) const { fields, client } = reg const reading = reg.identifier === Reading const value = reg.identifier === Value const enabled = isEnabledReg(reg) const fieldName = `this._${reading ? "reading" : camelize(reg.name)}` const hasBlocks = !reg.lowLevel && (reg.identifier == Reading || reg.identifier == Intensity || reg.identifier == Value) return fields .map((field, fieldi) => { const { name, min, max, defl, valueScaler, valueUnscaler, unit, } = genFieldInfo(reg, field) const unitString = unit ? ` (${unit})` : "" return ` /** * ${(reg.description || "").split("\n").join("\n * ")} */ ${toMetaComments( "callInDebugger", `group="${group}"`, hasBlocks && `block="%${shortId} ${humanify(name)}${unitString}"`, hasBlocks && `blockId=jacdac_${shortId}_${reg.name}_${field.name}_get`, `weight=${weight--}` )} ${camelize(name)}(): ${types[fieldi]} {${ client ? ` // TODO: implement client register throw "client register not implement";` : reading && isSimpleSensorClient ? ` return ${valueScaler(`this.reading()`)}; ` : `${ reading ? ` this.setStreaming(true);` : ` this.start();` } const values = ${fieldName}.pauseUntilValues() as any[]; return ${valueScaler(`values[${fieldi}]`)};` } } ${ reg.kind === "rw" ? ` /** * ${(reg.description || "").split("\n").join("\n * ")} */ ${toMetaComments( `group="${group}"`, hasBlocks && `blockId=jacdac_${shortId}_${reg.name}_${field.name}_set`, hasBlocks && `block="${ enabled ? `set %${shortId} %value=toggleOnOff` : `set %${shortId} ${humanify(name)} to %value${unitString}` }"`, `weight=${weight--}`, min !== undefined && `value.min=${min}`, max !== undefined && `value.max=${max}`, defl !== undefined && `value.defl=${defl}` )} set${capitalize(camelize(name))}(value: ${types[fieldi]}) { this.start();${ enabledReg && value ? ` this.setEnabled(true);` : "" } const values = ${fieldName}.values as any[]; values[${fieldi}] = ${valueUnscaler("value")}; ${fieldName}.values = values as [${types}]; } ` : "" }` }) .join("") }) .join("")}${ isSimpleSensorClient ? ` /** * Run code when the ${humanify( reading.name )} changes by the given threshold value. */ ${toMetaComments( `group="${group}"`, `blockId=jacdac_${shortId}_on_${reading.name}_change`, `block="on %${shortId} ${humanify( reading.name )} changed by %threshold${readingUnitString}"`, `weight=${weight--}`, `threshold.min=0`, genFieldInfo(reading, reading.fields[0]).max !== undefined && `threshold.max=${genFieldInfo(reading, reading.fields[0]).max}`, `threshold.defl=${ reading.fields[0].unit === "/" ? "5" : /[ui]0\./.test(reading.fields[0].type) ? "0.1" : "1" }` )} on${capitalize( camelize(reading.name) )}ChangedBy(threshold: number, handler: () => void): void { this.onReadingChangedBy(${genFieldInfo( reading, reading.fields[0] ).valueUnscaler("threshold")}, handler); } ` : "" }${ eventEnumName ? ` /** * Register code to run when an event is raised */ ${toMetaComments( `group="${group}"`, `blockId=jacdac_on_${spec.shortId}_event`, `block="on %${shortId} %event"`, `weight=${weight--}` )} onEvent(ev: ${eventEnumName}, handler: () => void): void { this.registerEvent(ev, handler); } ` : "" }${events .map(event => { return ` /** * ${(event.description || "").split("\n").join("\n * ")} */ ${toMetaComments(`group="${group}"`, `weight=${weight--}`)} on${capitalize(camelize(event.name))}(handler: () => void): void { this.registerEvent(${eventEnumName}.${capitalize( camelize(event.name) )}, handler); }` }) .join("")} ${commands .map(command => { const { name, client } = command const { types } = packInfo(spec, command, { isStatic: true, useBooleans: true, }) const { fields } = command const fnames = fields.map(f => camelize(f.name)) const cmd = `${nsc}.${capitalize(spec.camelName)}Cmd.${capitalize( camelize(command.name) )}` const fmt = command.packFormat return ` /** * ${(command.description || "").split("\n").join("\n * ")} */ ${toMetaComments( `group="${group}"`, `blockId=jacdac_${shortId}_${command.name}_cmd`, `block="%${shortId} ${humanify(name)}${ !fnames?.length ? "" : fnames?.length == 1 ? ` $${fnames[0]}` : " " + fnames.map(fn => `|${fn} $${fn}`).join(" ") }"`, `weight=${weight--}` )} ${camelize(name)}(${fnames .map((fname, fieldi) => `${fname}: ${types[fieldi]}`) .join(", ")}): void { ${ client ? `// TODO: implement client command throw "client command not implemented"` : `this.start(); this.sendCommand(jacdac.JDPacket.${ types.length === 0 ? `onlyHeader(${cmd})` : `jdpacked(${cmd}, jacdac.${capitalize( spec.camelName )}CmdPack.${capitalize(camelize(name))}, [${fnames.join( ", " )}])` })` } } ` }) .join("")} } ${Array(ninstances) .fill(0) .map( (_, i) => ` //% fixedInstance whenUsed weight=${1 + i} block="${humanify( spec.camelName ).toLocaleLowerCase()}${i + 1}" export const ${tsify(spec.camelName)}${ i + 1 } = new ${className}("${humanify(spec.camelName)}${i + 1}");` ) .join("\n")} }` } function toPythonClient( spec: jdspec.ServiceSpec, options: { baseClient?: boolean } = {} ) { const { camelName, packets } = spec const { baseClient } = options const registers = packetsToRegisters(packets) const regs = registers .filter(r => !!r) .filter(r => !r.restricted && !r.client) .filter(r => !r.fields.some(f => f.startRepeats)) const reading = regs.find(reg => reg.identifier === Reading) const { pyTypes: readingTypes } = reading ? packInfo(spec, reading, { isStatic: true, useBooleans: true, }) : { pyTypes: undefined } const readingType = readingTypes ? readingTypes.length == 1 ? readingTypes[0] : `Tuple[${readingTypes.join(", ")}]` : undefined const missingReadingField = reading ? `missing_${snakify(reading.name)}_value` : undefined const baseType = reading ? "SensorClient" : "Client" const ctorArgs = [ `bus`, `JD_SERVICE_CLASS_${snakify(camelName).toUpperCase()}`, `JD_${snakify(camelName).toUpperCase()}_PACK_FORMATS`, `role`, reading?.preferredInterval && `preferred_interval = ${reading.preferredInterval}`, ].filter(a => !!a) const enabledReg = regs.find(isEnabledReg) const events = packets.filter(pkt => !pkt.derived && pkt.kind === "event") // TODO: pipes support const commands = packets.filter( pkt => !pkt.derived && !pkt.restricted && pkt.kind === "command" && pkt.fields.every(f => f.type !== "pipe") && !pkt.fields.some(f => f.startRepeats) ) const className = `${capitalize(camelName)}Client${ baseClient ? "Base" : "" }` const tuple = regs.some( reg => reg.fields.length > 1 || reg.fields[0].startRepeats ) return `# Autogenerated file. Do not edit. from jacdac.bus import Bus, ${reading ? "Sensor" : ""}Client${ events.length > 0 ? `, EventHandlerFn, UnsubscribeFn` : `` } from .constants import * ${regs.length > 0 ? `from typing import Optional${tuple ? ", Tuple" : ""}` : ``} class ${className}(${baseType}): """ ${(spec.notes["short"] || "").split("\n").join("\n * ")} Implements a client for the \`${ spec.name } \`_ service. """ def __init__(self, bus: Bus, role: str${ reading ? `, *, ${missingReadingField}: Optional[${readingType}] = None` : "" }) -> None: super().__init__(${ctorArgs.join(", ")}) ${ missingReadingField ? ` self.${missingReadingField} = ${missingReadingField}` : "" } ${regs .map(reg => { const { kind, name: rname } = reg const { pyTypes: types } = packInfo(spec, reg, { isStatic: true, useBooleans: true, }) const { fields, client } = reg const value = reg.identifier === Value const creading = reg === reading const regcst = `JD_${snakify(camelName).toUpperCase()}_REG_${snakify( reg.name ).toUpperCase()}` const fetchReg = `self.register(${regcst})` const single = fields.length === 1 const rtype = single ? types[0] : `Tuple[${types.join(", ")}]` const { scale } = genFieldInfo(reg, fields[0]) return ` @property def ${snakify(rname)}(self) -> Optional[${rtype}]: """ ${`${reg.optional ? "(Optional) " : ""}${ reg.description || "" }, ${fields.filter(f => f.unit).map(f => `${f.name}: ${f.unit}`)}` .split("\n") .join("\n ")} """${ client ? ` # TODO: implement client register raise RuntimeError("client register not implemented")` : `${reading === reg ? `\n self.refresh_reading()` : ``} return ${fetchReg}.${ single && rtype === "bool" ? "bool_" : single && scale ? "float_" : "" }value(${[ creading && `self.${missingReadingField}`, single && scale, ] .filter(v => !!v) .join(", ")})` } ${ kind === "rw" ? ` @${snakify(rname)}.setter def ${snakify(rname)}(self, value: ${rtype}) -> None:${ enabledReg && value ? ` self.enabled = True` : "" } ${fetchReg}.set_values(${single ? "" : "*"}value${ single && scale ? ` / ${scale}` : "" }) ` : "" }` }) .join("")}${events .map(event => { return ` def on_${snakify( event.name )}(self, handler: EventHandlerFn) -> UnsubscribeFn: """ ${(event.description || "").split("\n").join("\n ")} """ return self.on_event(JD_${snakify( camelName ).toUpperCase()}_EV_${snakify(event.name).toUpperCase()}, handler) ` }) .join("")} ${commands .map(command => { const { name: cname, client } = command const { pyTypes, types, names } = packInfo(spec, command, { isStatic: true, useBooleans: true, }) const fnames = names.map(f => snakify(f).toLowerCase()) const cmd = `JD_${snakify(spec.camelName)}_CMD_${snakify( cname )}`.toUpperCase() console.log("cmd", { cname, fnames, types, pyTypes, names }) return ` def ${snakify(cname)}(self, ${fnames .map((fname, fieldi) => `${fname}: ${pyTypes[fieldi]}`) .join(", ")}) -> None: """ ${(command.description || "").split("\n").join("\n ")} """ ${ client ? `# TODO: implement client command raise RuntimeError("client command not implemented")` : `self.send_cmd_packed(${cmd}, ${fnames.join(", ")})` } ` }) .join("")} ` } function toCSharpClient( spec: jdspec.ServiceSpec, options: { baseClient?: boolean } = {} ) { const { camelName, packets } = spec const { baseClient } = options const registers = packetsToRegisters(packets) const regs = registers .filter(r => !!r) .filter(r => !r.restricted && !r.client) .filter(r => !r.fields.some(f => f.startRepeats)) const reading = regs.find(reg => reg.identifier === Reading) const baseType = reading ? "SensorClient" : "Client" const ctorTypes = ["JDBus bus", "string name"].filter(a => !!a) const ctorArgs = [ `bus`, `name`, `ServiceClasses.${capitalize(camelName)}`, ].filter(a => !!a) const enabledReg = regs.find(isEnabledReg) const events = packets.filter(pkt => !pkt.derived && pkt.kind === "event") // TODO: pipes support const commands = packets.filter( pkt => !pkt.derived && !pkt.restricted && pkt.kind === "command" && pkt.fields.every(f => f.type !== "pipe") && !pkt.fields.some(f => f.startRepeats) ) const className = `${capitalize(camelName)}Client${ baseClient ? "Base" : "" }` return `/** Autogenerated file. Do not edit. */ using Jacdac; using System; namespace Jacdac.Clients { /// /// ${(spec.notes["short"] || "").split("\n").join("\n /// ")} /// Implements a client for the ${spec.name} service. /// /// public partial class ${className} : ${baseType} { public ${className}(${ctorTypes.join(", ")}) : base(${ctorArgs.join(", ")}) { } ${regs .map(reg => { const { kind, name: rname } = reg const { csTypes: types } = packInfo(spec, reg, { isStatic: true, useBooleans: true, }) const { fields, client } = reg const value = reg.identifier === Value const regcst = `(ushort)${capitalize(camelName)}Reg.${capitalize( camelize(reg.name) )}` const single = fields.length === 1 const rtype = single ? types[0] : `object[] /*(${types.join(", ")})*/` const fetchArgs = `${regcst}, ${capitalize( camelName )}RegPack.${capitalize(camelize(reg.name))}` const fetchReg = `(${rtype})this.GetRegisterValue${ rtype == "bool" ? "AsBool" : "" }${single ? "" : "s"}(${fetchArgs})` const setReg = `this.SetRegisterValue${ single ? "" : "s" }(${regcst}, ${capitalize(camelName)}RegPack.${capitalize( camelize(reg.name) )}, value)` return ` /// /// ${ reg.optional ? `Tries to read the ${rname} register value.` : `Reads the ${rname} register value.` } /// ${`${reg.description || ""}, ${fields .filter(f => f.unit) .map(f => `${f.name}: ${f.unit}`)}` .split("\n") .join("\n /// ")} /// ${ reg.optional ? `bool TryGet${capitalize(camelize(rname))}(out ${rtype} value) { object[] values; if (this.TryGetRegisterValues(${fetchArgs}, out values)) { value = (${rtype})values[0]; return true; } else { value = default(${rtype}); return false; } }${ kind === "rw" ? ` /// /// Sets the ${rname} value /// public void Set${capitalize(camelize(rname))}(${rtype} value) { ${setReg}; } ` : "" }` : `public ${rtype} ${capitalize(camelize(rname))} { get { ${ client ? `// TODO: implement client register throw NotSupportedException("client register not implemented");` : `return ${fetchReg};` } }${ kind === "rw" ? ` set { ${ enabledReg && value ? ` this.Enabled = true;` : "" } ${setReg}; } ` : "" } }` } ` }) .join("")}${events .map(event => { return ` /// /// ${(event.description || "").split("\n").join("\n /// ")} /// public event ClientEventHandler ${capitalize(camelize(event.name))} { add { this.AddEvent((ushort)${capitalize( camelName )}Event.${capitalize(camelize(event.name))}, value); } remove { this.RemoveEvent((ushort)${capitalize( camelName )}Event.${capitalize(camelize(event.name))}, value); } } ` }) .join("")} ${commands .map(command => { const { name: cname, client } = command const { csTypes: types, names } = packInfo(spec, command, { isStatic: true, useBooleans: true, }) const fnames = names.map(f => snakify(f).toLowerCase()) const cmd = `(ushort)${capitalize(spec.camelName)}Cmd.${capitalize( camelize(cname) )}` const pack = `${capitalize(spec.camelName)}CmdPack.${capitalize( camelize(cname) )}` return ` ${client ? "/* client command" : ""} /// /// ${(command.description || "").split("\n").join("\n /// ")} /// public void ${capitalize(camelize(cname))}(${fnames .map((fname, fieldi) => `${types[fieldi]} ${fname}`) .join(", ")}) { this.SendCmd${!fnames.length ? "" : "Packed"}(${cmd}${ fnames.length > 0 ? `, ${pack}, new object[] { ${fnames.join(", ")} }` : "" }); }${client ? "*/" : ""} ` }) .join("")} } }` } function toHex(n: number): string { if (n === undefined) return "" if (n < 0) return "-" + toHex(n) return "0x" + n.toString(16) } function packedSensorSpec(info: jdspec.ServiceSpec) { if (!info.extends || info.extends.indexOf("_sensor") < 0) return "" const reading = info.packets.find( pkt => pkt.kind === "ro" && pkt.identifierName === "reading" ) if (reading?.fields?.length !== 1) return "" const fld = reading.fields[0] const tp = fld.storage if ([1, 2, 4, 8].indexOf(Math.abs(tp)) < 0) return "" const fmt = tp < 0 ? `I${-tp * 8}` : `U${tp * 8}` const shift = fld.shift ?? 0 const mode = fld.type == "bool" || info.enums[fld.type] ? "DISCRETE" : "CONTINUOUS" const clsId = toHex(info.classIdentifier) return `JD_SPEC_PACK_SERVICE("${info.camelName}", ${clsId}, ${fmt}, ${shift}, ${mode})` } function processSpec(dn: string) { const path = require("path") console.log("processing directory " + dn + "...") const files: string[] = fs.readdirSync(dn) const includes: jdspec.SMap = {} files.sort() // ensure _system is first files.splice(files.indexOf("_system.md"), 1) files.unshift("_system.md") const outp = path.join(dn, "generated") mkdir(outp) for (const n of Object.keys(converters())) mkdir(path.join(outp, n)) // generate makecode file structure const mkcdir = path.join(outp, "makecode") mkdir(mkcdir) const pydir = path.join(outp, "python") mkdir(pydir) const csdir = path.join(outp, "cs") mkdir(csdir) const mkcdServices: jdspec.MakeCodeServiceInfo[] = [] const mkcTargetConfig: { packages: { approvedRepos: string[] } upgrades: Record } = { packages: { approvedRepos: ["microsoft/pxt-jacdac"], }, upgrades: {}, } const pxtJacdacDir = path.resolve( path.join(dn, "..", "..", "..", "pxt-jacdac") ) console.log(`pxt-jacdac: ${pxtJacdacDir}`) const pxtJacdacPxtJson = path.join(pxtJacdacDir, "pxt.json") let pxtJacdacVersion: string = undefined if (fs.existsSync(pxtJacdacPxtJson)) { const pxtJacdacjson: { version: string } = JSON.parse( fs.readFileSync(path.join(pxtJacdacDir, "pxt.json"), { encoding: "utf8", }) ) pxtJacdacVersion = pxtJacdacjson.version mkcTargetConfig.upgrades[ "microsoft/pxt-jacdac" ] = `min:v${pxtJacdacVersion}` } const jacdacPythonDir = path.resolve( path.join(dn, "..", "..", "..", "jacdac-python", "jacdac") ) console.log(`jacdac-python: ${jacdacPythonDir}`) const jacdacCsDir = path.resolve( path.join(dn, "..", "..", "..", "jacdac-dotnet", "Jacdac", "Clients") ) console.log(`jacdac-dotnet: ${jacdacCsDir}`) const fmtStats: { [index: string]: number } = {} const concats: jdspec.SMap = { ts: `/* eslint-disable @typescript-eslint/no-namespace */ ` } const markdowns: jdspec.ServiceMarkdownSpec[] = [] for (const fn of files) { if (!/\.md$/.test(fn) || fn[0] == ".") continue console.log(`process ${fn}`) const cont = readString(dn, fn) const json = parseServiceSpecificationMarkdownToJSON(cont, includes, fn) const key = fn.replace(/\.md$/, "") includes[key] = json markdowns.push({ classIdentifier: json.classIdentifier, shortId: json.shortId, source: cont, }) json.packets .map(pkt => pkt.packFormat) .filter(fmt => !!fmt) .forEach(fmt => (fmtStats[fmt] = (fmtStats[fmt] || 0) + 1)) reportErrors(json.errors, dn, fn) // check if there is a makecode project folder for this service const mkcdsrvdirname = dashify(json.camelName) const mkcdpxtjson = path.join(pxtJacdacDir, mkcdsrvdirname, "pxt.json") const hasMakeCodeProject = fs.existsSync(mkcdpxtjson) console.log(`check exists ${mkcdpxtjson}: ${hasMakeCodeProject}`) const pysrvdirname = snakify(json.camelName).toLowerCase() if (hasMakeCodeProject) { const pxtjson: { version: string supportedTargets?: string[] files: string[] } = JSON.parse(fs.readFileSync(mkcdpxtjson, { encoding: "utf8" })) const repo = `microsoft/pxt-jacdac/${mkcdsrvdirname}` mkcdServices.push({ service: json.shortId, client: { name: `jacdac-${mkcdsrvdirname}`, targets: pxtjson.supportedTargets, repo, qName: `modules.${capitalize(json.camelName)}Client`, default: `modules.${json.camelName}`, generated: pxtjson.files.indexOf("client.g.ts") > -1, }, }) mkcTargetConfig.packages.approvedRepos.push(repo) mkcTargetConfig.upgrades[repo] = `min:v${pxtJacdacVersion}` } const cnv = converters() for (const n of Object.keys(cnv)) { const convResult = cnv[n](json) const ext = n == "sts" ? "ts" : n == "c" ? "h" : n == "cs" ? "g.cs" : n let fnn = fn.slice(0, -3) if (n == "cs") { fnn = path.join( "Constants", capitalize(camelize(fnn)) + "Constants" ) mkdir(path.join(path.join(outp, n, "Constants"))) } const cfn = path.join(outp, n, fnn + "." + ext) fs.writeFileSync(cfn, convResult) console.log(`written ${cfn}`) if (!concats[n]) concats[n] = "" concats[n] += convResult const generateClient = !/^_/.test(json.shortId) && serviceBuiltins.indexOf(json.shortId) < 0 if (n === "sts") { const mkcdclient = generateClient && toMakeCodeClient(json) const srvdir = path.join(mkcdir, mkcdsrvdirname) mkdir(srvdir) fs.writeFileSync(path.join(srvdir, "constants.ts"), convResult) // generate project file and client template if (mkcdclient) { if (!hasMakeCodeProject) fs.writeFileSync( path.join(srvdir, "pxt.g.json"), toPxtJson(json) ) // only write client.g.ts if it already exists; otherwise use .gts to avoid confusing TS intellisense fs.writeFileSync( fs.existsSync(path.join(srvdir, "client.g.ts")) ? path.join(srvdir, "client.g.ts") : path.join(srvdir, "client.gts"), mkcdclient ) } } else if (n === "cs") { const baseClient = fs.existsSync( path.join( jacdacCsDir, `${capitalize(json.camelName)}Base.cs` ) ) const csclient = generateClient && toCSharpClient(json, { baseClient }) const srvdir = path.join(csdir, "Clients") mkdir(srvdir) //fs.writeFileSync(path.join(srvdir, "constants.cs"), convResult) if (csclient) fs.writeFileSync( path.join( srvdir, baseClient ? `${capitalize(json.camelName)}Base.g.cs` : `${capitalize(json.camelName)}Client.g.cs` ), csclient ) } else if (n === "py") { const baseClient = fs.existsSync( path.join(jacdacPythonDir, pysrvdirname, "client_base.py") ) const pyclient = generateClient && toPythonClient(json, { baseClient }) const srvdir = path.join(pydir, pysrvdirname) mkdir(srvdir) fs.writeFileSync(path.join(srvdir, "constants.py"), convResult) if (!pyclient) fs.writeFileSync( path.join(srvdir, "__init__.py"), `# Autogenerated file. from .constants import * ` ) else { fs.writeFileSync( path.join(srvdir, "__init__.py"), `# Autogenerated file. from .client import ${capitalize(json.camelName)}Client # type: ignore ` ) fs.writeFileSync( path.join( srvdir, baseClient ? "client_base.py" : "client.py" ), pyclient ) } } } } fs.writeFileSync( path.join(outp, "services-sources.json"), JSON.stringify(markdowns), "utf-8" ) fs.writeFileSync( path.join(outp, "services.json"), JSON.stringify(values(includes)), "utf-8" ) fs.writeFileSync(path.join(outp, "specconstants.ts"), concats["ts"]) fs.writeFileSync(path.join(outp, "specconstants.sts"), concats["sts"]) fs.writeFileSync(path.join(outp, "specconstants.cs"), concats["cs"]) if (fs.existsSync(pxtJacdacDir)) { // only available locally fs.writeFileSync( path.join(outp, "../makecode-extensions.json"), JSON.stringify(mkcdServices, null, 2) ) fs.writeFileSync( path.join(outp, "../makecode-targetconfig.json"), JSON.stringify(mkcTargetConfig, null, 2) ) } const specs = values(includes) specs.sort((a, b) => a.classIdentifier - b.classIdentifier) const packed = specs.map(packedSensorSpec).filter(s => !!s) const packedC = `#include "jd_spec_pack.h"\n\n` + `// Sorted by service class!\n` + `JD_SPEC_PACK_BEGIN\n` + packed.join("\n") + `\nJD_SPEC_PACK_END\n\n` + `JD_SPEC_PACK_NUM(${packed.length})\n` const packpath = path.join(outp, "c/jd_spec_pack.c") console.log(`wrote ${packpath}`) fs.writeFileSync(packpath, packedC) const fms = Object.keys(fmtStats).sort((l, r) => -fmtStats[l] + fmtStats[r]) console.log(fms.map(fmt => `${fmt}: ${fmtStats[fmt]}`)) } function readString(folder: string, file: string) { const path = require("path") const cont: string = fs.readFileSync(path.join(folder, file), "utf8") return cont } function processDevices(upperName: string) { const path = require("path") console.log("processing devices in directory " + upperName + "...") const allDevices: jdspec.DeviceSpec[] = [] const todo = [upperName] while (todo.length) { const dir = todo.pop() const files: string[] = fs.readdirSync(dir) files.sort() for (const fn of files) { const f = path.join(dir, fn) const stat = fs.statSync(f) if (stat.isDirectory()) todo.push(f) else if (/\.json/.test(f)) { console.log(` ${f}`) const dev = normalizeDeviceSpecification( JSON.parse(readString(dir, fn)) as jdspec.DeviceSpec ) fs.writeFileSync( path.join(dir, fn), JSON.stringify(dev, null, 2) ) allDevices.push(dev) } } } allDevices .filter(d => d.devices) .forEach(d => d.devices .filter(did => !allDevices.find(({ id }) => id === did)) .forEach(did => console.error(`${d.id}: device not found ${did}`) ) ) const statusScores: Record = { deprecated: 100, experimental: 50, rc: 10, stable: 0, } // push experimentals at the back allDevices.sort( (a, b) => statusScores[a.status || "experimental"] - statusScores[b.status || "experimental"] ) const ofn = path.join("../dist", "devices.json") console.log(`writing ${ofn}`) fs.writeFileSync(ofn, JSON.stringify(allDevices, null, 2)) } function reportErrors( errors: jdspec.Diagnostic[], folderName: string, fn: string ) { if (!errors) return const path = require("path") for (const e of errors) { const fn2 = e.file ? path.join(folderName, e.file) : fn console.error(`${fn2}(${e.line}): ${e.message}`) } process.exit(1) } function nodeMain() { fs = require("fs") const args: string[] = process.argv.slice(2) let deviceMode = false if (args[0] == "-d") { args.shift() deviceMode = true } if (args.length != 1) { console.error("usage: node spectool.js [-d] DIRECTORY") process.exit(1) } if (deviceMode) processDevices(args[0]) else processSpec(args[0]) } function mkdir(n: string) { try { fs.mkdirSync(n, "777") } catch (e) { if (e.code != "EEXIST") console.warn(e) } } if (typeof process != "undefined") nodeMain()