/* eslint-disable @typescript-eslint/no-explicit-any */
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///
import { parseIntFloat } from "./jdutils"
export const DEVICE_IMAGE_WIDTH = 1024
export const DEVICE_IMAGE_HEIGHT = 768
// modified subset of SenML
export const unitDescription: jdspec.SMap = {
"°": "angle",
"°/s": "rotation rate",
"°/s2": "rotation acceleration",
m: "meter",
kg: "kilogram",
s: "second",
A: "ampere",
K: "kelvin",
cd: "candela",
mol: "mole",
Hz: "hertz",
rad: "radian",
sr: "steradian",
N: "newton",
Pa: "pascal",
J: "joule",
W: "watt",
C: "coulomb",
V: "volt",
F: "farad",
Ohm: "ohm",
S: "siemens",
Wb: "weber",
T: "tesla",
H: "henry",
"°C": "degrees Celsius",
lm: "lumen",
lx: "lux",
Bq: "becquerel",
Gy: "gray",
Sv: "sievert",
kat: "katal",
m2: "square meter (area)",
m3: "cubic meter (volume)",
"m/s": "meter per second (velocity)",
"m/s2": "meter per square second (acceleration)",
"m3/s": "cubic meter per second (flow rate)",
"W/m2": "watt per square meter (irradiance)",
"cd/m2": "candela per square meter (luminance)",
bit: "bit (information content)",
"bit/s": "bit per second (data rate)",
baud: "symbols per second",
lat: "degrees latitude",
lon: "degrees longitude",
pH: "pH value (acidity; logarithmic quantity)",
dB: "decibel (logarithmic quantity)",
dBW: "decibel relative to 1 W (power level)",
count: "1 (counter value)",
"/": "1 (ratio e.g., value of a switch)",
"%RH": "Percentage (Relative Humidity)",
"%EL": "Percentage (remaining battery energy level)",
EL: "seconds (remaining battery energy level)",
"1/s": "1 per second (event rate)",
"S/m": "Siemens per meter (conductivity)",
B: "Byte (information content)",
VA: "volt-ampere (Apparent Power)",
VAs: "volt-ampere second (Apparent Energy)",
var: "volt-ampere reactive (Reactive Power)",
vars: "volt-ampere-reactive second (Reactive Energy)",
"J/m": "joule per meter (Energy per distance)",
"kg/m3": "kilogram per cubic meter (mass density, mass concentration)",
"s/60°": "servo speed (time to travel 60°)",
"kg/cm": "torque",
hsv: "bit HSV color",
rgb: "RGB color",
rpm: "revolutions per minute",
uv: "UV index",
lux: "illuminance",
bpm: "beats per minute",
mcd: "micro candella",
px: "pixel",
AQI: "air quality index",
}
export const secondaryUnitConverters: jdspec.SMap<{
name: string
unit: senml.Unit | "#"
scale: number
offset: number
}> = {
ms: { name: "millisecond", unit: "s", scale: 1 / 1000, offset: 0 },
min: { name: "minute", unit: "s", scale: 60, offset: 0 },
h: { name: "hour", unit: "s", scale: 3600, offset: 0 },
MHz: { name: "megahertz", unit: "Hz", scale: 1000000, offset: 0 },
kW: { name: "kilowatt", unit: "W", scale: 1000, offset: 0 },
kVA: { name: "kilovolt-ampere", unit: "VA", scale: 1000, offset: 0 },
kvar: { name: "kilovar", unit: "var", scale: 1000, offset: 0 },
Ah: { name: "ampere-hour", unit: "C", scale: 3600, offset: 0 },
Wh: { name: "watt-hour", unit: "J", scale: 3600, offset: 0 },
kWh: { name: "kilowatt-hour", unit: "J", scale: 3600000, offset: 0 },
varh: { name: "var-hour", unit: "vars", scale: 3600, offset: 0 },
kvarh: { name: "kilovar-hour", unit: "vars", scale: 3600000, offset: 0 },
kVAh: {
name: "kilovolt-ampere-hour",
unit: "VAs",
scale: 3600000,
offset: 0,
},
"Wh/km": {
name: "watt-hour per kilometer",
unit: "J/m",
scale: 3.6,
offset: 0,
},
KB: { name: "kilobyte", unit: "B", scale: 1000, offset: 0 },
KiB: { name: "kibibyte", unit: "B", scale: 1024, offset: 0 },
GB: { name: "gigabyte", unit: "B", scale: 1.0e9, offset: 0 },
"Mbit/s": {
name: "megabit per second",
unit: "bit/s",
scale: 1000000,
offset: 0,
},
"B/s": { name: "byte per second", unit: "bit/s", scale: 8, offset: 0 },
"MB/s": {
name: "megabyte per second",
unit: "bit/s",
scale: 8000000,
offset: 0,
},
mV: { name: "millivolt", unit: "V", scale: 1 / 1000, offset: 0 },
mA: { name: "milliampere", unit: "A", scale: 1 / 1000, offset: 0 },
dBm: { name: "decibel (milliwatt)", unit: "dBW", scale: 1, offset: -30 },
"ug/m3": {
name: "microgram per cubic meter",
unit: "kg/m3",
scale: 1.0e-9,
offset: 0,
},
"mm/h": {
name: "millimeter per hour",
unit: "m/s",
scale: 1 / 3600000,
offset: 0,
},
"m/h": { name: "meter per hour", unit: "m/s", scale: 1 / 3600, offset: 0 },
"cm/s": {
name: "centimeter per seconds",
unit: "m/s",
scale: 1 / 100,
offset: 0,
},
ppm: { name: "parts per million", unit: "/", scale: 1.0e-6, offset: 0 },
ppb: { name: "parts per billion", unit: "/", scale: 1.0e-9, offset: 0 },
"/100": { name: "percent", unit: "/", scale: 1 / 100, offset: 0 },
"%": { name: "percent", unit: "/", scale: 1 / 100, offset: 0 },
"/1000": { name: "permille", unit: "/", scale: 1 / 1000, offset: 0 },
hPa: { name: "hectopascal", unit: "Pa", scale: 100, offset: 0 },
mm: { name: "millimeter", unit: "m", scale: 1 / 1000, offset: 0 },
cm: { name: "centimeter", unit: "m", scale: 1 / 100, offset: 0 },
km: { name: "kilometer", unit: "m", scale: 1000, offset: 0 },
"km/h": {
name: "kilometer per hour",
unit: "m/s",
scale: 1 / 3.6,
offset: 0,
},
"8ms": { name: "8 milliseconds", unit: "s", scale: 8 / 1000, offset: 0 },
nm: { name: "nanometer", unit: "m", scale: 1e-9, offset: 0 },
nT: { name: "nano Tesla", unit: "T", scale: 1e-9, offset: 0 },
// compat with previous Jacdac versions
frac: { name: "ratio", unit: "/", scale: 1, offset: 0 },
us: { name: "micro seconds", unit: "s", scale: 1e-6, offset: 0 },
mWh: { name: "micro watt-hour", unit: "J", scale: 3.6e-3, offset: 0 },
g: { name: "earth gravity", unit: "m/s2", scale: 9.80665, offset: 0 },
"#": { name: "count", unit: "#", scale: 1, offset: 0 },
AudHz: { name: "Audible Frequency", unit: "Hz", scale: 1, offset: 0 },
}
export const encodings: jdspec.SMap = {
json: "JSON",
bitset: "bitset",
}
export function resolveUnit(unit: string) {
if (!unit) return { name: "", scale: 1, offset: 1 } // indentifier
// seconary unit?
const su = secondaryUnitConverters[unit]
if (su) return su
const name = unitDescription[unit]
if (name) return { name, unit, scale: 1, offset: 0 }
return undefined
}
export function units(): { name: string; description: string }[] {
const r: { name: string; description: string }[] = []
Object.keys(unitDescription).forEach(k => {
r.push({ name: k, description: unitDescription[k] })
Object.keys(secondaryUnitConverters)
.filter(scd => secondaryUnitConverters[scd].unit === k)
.forEach(scd =>
r.push({
name: scd,
description: secondaryUnitConverters[scd].name,
}),
)
})
r.sort((l, r) => l.name.localeCompare(r.name))
return r
}
/* check ranges, see system.md
Registers `0x001-0x07f` - r/w common to all services
Registers `0x080-0x0ff` - r/w defined per-service
Registers `0x100-0x17f` - r/o common to all services
Registers `0x180-0x1ff` - r/o defined per-service
Registers `0x200-0xeff` - custom, defined per-service
Registers `0xf00-0xfff` - reserved for implementation, should not be seen on the wire
*/
const identifierRanges: { [index: string]: [number, number][] } = {
rw: [
[0x001, 0x07f],
[0x080, 0x0ff],
[0x200, 0xeff], // custom
[0xf00, 0xfff], // impl
],
ro: [
[0x100, 0x17f],
[0x180, 0x1ff],
[0x200, 0xeff], // custom
[0xf00, 0xfff], // impl
],
const: [
[0x100, 0x17f],
[0x180, 0x1ff],
[0x200, 0xeff], // custom
[0xf00, 0xfff], // impl
],
command: [
[0x000, 0x07f],
[0x080, 0xeff],
[0xf00, 0xfff],
],
report: [
[0x000, 0x07f],
[0x080, 0xeff],
[0xf00, 0xfff],
],
event: [
[0x00, 0x7f], // system
[0x80, 0xff],
],
}
export function parseServiceSpecificationMarkdownToJSON(
filecontent: string,
includes?: jdspec.SMap,
filename = "",
): jdspec.ServiceSpec {
filecontent = (filecontent || "").replace(/\r/g, "")
const info: jdspec.ServiceSpec = {
name: "",
status: "experimental",
shortId: filename.replace(/\.md$/, "").replace(/.*\//, ""),
camelName: "",
shortName: "",
extends: [],
notes: {},
classIdentifier: 0,
enums: {},
constants: {},
packets: [],
tags: [],
}
let backticksType: string = null
let enumInfo: jdspec.EnumInfo = null
let packetInfo: jdspec.PacketInfo = null
let pipePacket: jdspec.PacketInfo = null
let errors: jdspec.Diagnostic[] = []
let lineNo = 0
let noteId = "short"
let lastCmd: jdspec.PacketInfo
let packetsToDescribe: jdspec.PacketInfo[]
let nextModifier: "" | "segmented" | "multi-segmented" | "repeats" = ""
const systemInfo = includes?.["_system"]
const usedIds: jdspec.SMap = {}
for (const prev of values(includes || {})) {
if (prev.catalog && prev.classIdentifier)
usedIds[prev.classIdentifier + ""] = prev.name
}
try {
if (includes["_system"] && includes["_base"]) processInclude("_base")
for (const line of filecontent.split(/\n/)) {
lineNo++
processLine(line)
}
} catch (e) {
error("exception: " + e.message)
}
if (errors.length) info.errors = errors
for (const k of Object.keys(info.notes))
info.notes[k] = normalizeMD(info.notes[k])
for (const v of info.packets) v.description = normalizeMD(v.description)
if (!info.camelName)
info.camelName = camelize(
info.name
.replace(/\s+/g, " ")
.replace(/[ -](.)/g, (f, l) => l.toUpperCase())
.replace(/[^\w]+/g, "_"),
)
if (!info.shortName) info.shortName = info.camelName
if (info.camelName == "system") info.classIdentifier = 0x1fff_fff1
else if (info.camelName == "base") info.classIdentifier = 0x1fff_fff3
else if (info.camelName == "sensor") info.classIdentifier = 0x1fff_fff2
if (info.shortName != "control" && !info.classIdentifier)
error("identifier: not specified")
info.packets.forEach(pkt => (pkt.packFormat = packFormat(info, pkt)))
return info
function processLine(line: string) {
if (backticksType) {
if (line.trim() == "```") {
const prev = backticksType
backticksType = null
if (prev == "default") return
}
} else {
const m = /^```(.*)/.exec(line)
if (m) {
backticksType = m[1] || "default"
// if we just switched into code section, don't interpret this line and don't add to any description
if (backticksType == "default") return
}
}
const interpret =
backticksType == "default" ||
(backticksType == null && line.slice(0, 4) == " ")
if (!interpret) {
const m = /^(#+)\s*(.*)/.exec(line)
if (m) {
const [, hd, cont] = m
packetsToDescribe = null
const newNoteId = cont.trim().toLowerCase()
if (hd == "#" && !info.name) {
info.name = cont.trim()
line = ""
} else if (
newNoteId == "registers" ||
newNoteId == "commands" ||
newNoteId == "events" ||
newNoteId == "examples"
) {
noteId = newNoteId
line = ""
} else {
if (noteId == "short") noteId = "long"
// keep line
}
}
if (packetsToDescribe) {
for (const iface of packetsToDescribe)
iface.description += line + "\n"
} else {
if (line || info.notes[noteId]) {
if (!info.notes[noteId]) info.notes[noteId] = ""
info.notes[noteId] += line + "\n"
}
}
} else {
if (packetsToDescribe && packetsToDescribe[0].description)
packetsToDescribe = null
const expanded = line
.replace(/\/\/.*/, "")
.replace(/[?@:=,{};]/g, s => " " + s + " ")
.trim()
if (!expanded) return
const words = expanded.split(/\s+/)
if (/^[;,]/.test(words[words.length - 1])) words.pop()
let cmd = words[0]
// allow for `command = Foo.Bar` etc (ie. command is not a keyword there)
if (words[1] == ":" || words[1] == "=") cmd = ":"
switch (cmd) {
case "type":
case "enum":
case "flags":
startEnum(words)
break
case "define":
constant(words)
break
case "meta":
case "pipe":
case "report":
case "command":
case "const":
case "ro":
case "rw":
case "event":
case "client":
case "volatile":
case "lowlevel":
case "unique":
case "restricted":
case "packed":
startPacket(words)
break
case "}":
if (packetInfo) {
finishPacket()
} else if (enumInfo) {
enumInfo = null
} else {
error("nothing to end here")
}
break
default:
if (packetInfo) packetField(words)
else if (enumInfo) enumMember(words)
else metadataMember(words)
}
}
}
function finishPacket() {
const paderr = paddingError(packetInfo)
if (paderr) {
if (!packetInfo.packed)
error(
`${paderr} in ${packetInfo.kind} ${packetInfo.name}; you can also add 'packed' attribute`,
)
} else {
if (packetInfo.packed)
error(
`${packetInfo.kind} ${packetInfo.name} has unnecessary 'packed' attribute`,
)
}
let repeats = false
let hadzero = false
for (const p of packetInfo.fields) {
if (hadzero) {
error(
`field ${p.name} in ${packetInfo.kind} ${packetInfo.name} follows a variable-sized field`,
)
break
}
if (p.startRepeats) {
if (repeats)
error(
`repeats: can only be specified once; in ${packetInfo.kind} ${packetInfo.name}`,
)
repeats = true
}
if (p.storage == 0 && p.type != "string0") {
if (repeats) {
error(
`variable-sized field ${p.name} in ${packetInfo.kind} ${packetInfo.name} cannot repeat`,
)
break
}
hadzero = true
}
}
const pid = packetInfo.identifier
const ranges = identifierRanges[packetInfo.kind]
if (
packetInfo.name != "set_register" &&
packetInfo.name != "get_register" &&
ranges &&
!ranges.some(range => range[0] <= pid && pid <= range[1])
)
error(
`${packetInfo.name} identifier ${toHex(
pid,
)} out of range, expected in ${ranges
.map(range => `[${range.map(toHex).join(", ")}]`)
.join(", ")}`,
)
// additional checks for specific packets
if (
[
"reading_error",
"min_reading",
"max_reading",
"reading_resolution",
].indexOf(packetInfo.identifierName) > -1
) {
const regid = packetInfo.identifierName
if (packetInfo.fields.length > 1) error(`${regid} must be a number`)
const reading = info.packets.find(
pkt => pkt.kind === "ro" && pkt.identifierName === "reading",
)
if (!reading) error(`${regid} register without a reading register`)
else if (packetInfo.fields[0].unit !== reading.fields[0].unit)
error(
`${regid} unit (${packetInfo.fields[0].unit}) and reading unit (${reading.fields[0].unit}) must match`,
)
}
packetInfo = null
}
function normalizeMD(md: string) {
return md.replace(/^\s+/, "").replace(/\s+$/, "")
}
function checkBraces(words: string[]) {
if (enumInfo || packetInfo) error("already in braces")
if (words) {
if (words[2] != "{") error(`expecting: ${words[0]} NAME {`)
}
enumInfo = null
packetInfo = null
}
function forceSection(sectionId: string) {
if (noteId != sectionId) {
error(`this is only allowed in ## ${sectionId} not in ## ${noteId}`)
}
}
function generalKind(k: jdspec.PacketKind): jdspec.PacketKind {
switch (k) {
case "const":
case "ro":
case "rw":
return "rw"
default:
return k
}
}
function startPacket(words: string[]) {
checkBraces(null)
let client: boolean = undefined
let lowLevel: boolean = undefined
let restricted: boolean = undefined
let unique: boolean = undefined
let internal: boolean = undefined
let volatile: boolean = undefined
let packed: boolean = undefined
function processAttributes() {
while (words.length) {
if (words[0] === "restricted") {
restricted = true
} else if (words[0] === "client") {
client = true
} else if (words[0] === "lowlevel") {
lowLevel = true
} else if (words[0] === "unique") {
unique = true
} else if (words[0] === "internal") {
internal = true
} else if (words[0] === "volatile") {
volatile = true
} else if (words[0] === "packed") {
packed = true
} else {
break
}
words.shift()
}
}
processAttributes()
const kindSt = words.shift()
let kind: jdspec.PacketKind = "command"
if (kindSt == "meta") {
forceSection("commands")
let w2 = words.shift()
if (w2 == "pipe") w2 = words.shift()
if (w2 == "report" || w2 == "command")
kind = ("meta_pipe_" + w2) as any
else error("invalid token after meta")
} else if (kindSt == "pipe") {
forceSection("commands")
const w2 = words.shift()
if (w2 == "report" || w2 == "command") kind = ("pipe_" + w2) as any
else error("invalid token after pipe")
} else {
kind = kindSt as any
}
processAttributes()
if (unique && kind !== "command")
error("unique only applies to commands")
if (volatile && kind != "ro" && kind != "rw")
error("volatile can only modify ro or rw")
let name = words.shift()
const isReport = kind == "report"
if (isReport && lastCmd && !/^\w+$/.test(name)) {
words.unshift(name)
name = lastCmd.name
}
packetInfo = {
kind,
name: normalizeName(name),
identifier: undefined,
description: "",
fields: [],
internal,
client,
lowLevel,
unique,
volatile,
restricted,
packed,
}
if (isReport && lastCmd && name == lastCmd.name) {
packetInfo.secondary = true
lastCmd.hasReport = true
}
if (!packetsToDescribe) packetsToDescribe = []
packetsToDescribe.push(packetInfo)
if (words[0] == "?") {
words.shift()
packetInfo.optional = true
}
const prev = info.packets.filter(p => p.name == packetInfo.name)
if (prev.length == 0) {
// OK
} else if (
prev.length == 1 &&
prev[0].kind == "command" &&
packetInfo.kind == "report"
) {
// OK
} else {
error(`packet redefinition ${prev.map(p => p.name).join(", ")} `)
}
if (/pipe/.test(kind)) {
if (!pipePacket)
error(
"pipe definitions can only occur after the pipe-open packet",
)
else packetInfo.pipeType = pipePacket.pipeType
}
const atat = words.indexOf("@")
if (kind == "pipe_command" || kind == "pipe_report") {
// no identifier
packetInfo.identifier = 0
} else if (atat >= 0) {
const w = words[atat + 1]
let v = parseInt(w)
let isSet = true
if (isNaN(v)) {
v = 0
isSet = false
if (systemInfo) {
const systemPacket = systemInfo.packets.find(
p => p.name == w,
)
if (systemPacket) {
v = systemPacket.identifier
packetInfo.identifierName = w
if (systemPacket.kind != kind)
error(
`kind mismatch on ${w}: ${systemPacket.kind} vs ${kind}`,
)
else isSet = true
} else error(`${w} not found in _system`)
} else {
error(`${w} cannot be resolved, since _system is missing`)
}
}
// if we are accessing the reading or reading_error register, mark it volatile
if (kind === "ro" && (v === 0x101 || v === 0x106))
packetInfo.volatile = true
let isUser = false
let isSystem = false
let isHigh = 0x200 <= v && v <= 0xeff
switch (kind) {
case "const":
case "ro":
forceSection("registers")
isSystem = 0x100 <= v && v <= 0x17f
isUser = 0x180 <= v && v <= 0x1ff
break
case "rw":
forceSection("registers")
isSystem = 0x00 <= v && v <= 0x7f
isUser = 0x80 <= v && v <= 0xff
break
case "report":
case "command":
forceSection("commands")
isSystem = 0x00 <= v && v <= 0x7f
isUser = 0x80 <= v && v <= 0xff
isHigh = 0x100 <= v && v <= 0xeff
break
case "event":
forceSection("events")
isSystem = 0x00 <= v && v <= 0x7f
isUser = 0x80 <= v && v <= 0xff
break
}
if (isUser) {
// ok
} else if (isSystem) {
if (!packetInfo.identifierName)
warn(
`${kind} @ ${toHex(
v,
)} should be expressed with a name from _system.md`,
)
} else if (isHigh) {
if (!info.highCommands)
warn(
`${kind} @ ${toHex(
v,
)} is from the extended range but 'high: 1' missing`,
)
}
packetInfo.identifier = v
words.splice(atat, 2)
} else {
if (isReport && lastCmd) packetInfo.identifier = lastCmd.identifier
else error(`@ not found at ${packetInfo.name}`)
}
if (
info.packets.some(
p =>
generalKind(p.kind) == generalKind(packetInfo.kind) &&
(!/pipe/.test(p.kind) ||
p.pipeType == packetInfo.pipeType) &&
p.identifier == packetInfo.identifier,
)
) {
error("packet identifier already used")
}
info.packets.push(packetInfo)
if (kind == "command") lastCmd = packetInfo
else lastCmd = null
if (words[0] == "=" || words[0] == ":") {
words.unshift("_")
packetField(words)
finishPacket()
} else {
const last = words.shift()
if (last == "{") {
if (words[0] == "...") words.shift()
if (words[0] == "}") {
words.shift()
finishPacket()
}
if (words.length) error(`excessive tokens: ${words[0]}...`)
} else {
if (last === undefined && kind == "event") {
finishPacket()
} else {
error("expecting '{'")
}
}
}
}
function rangeCheck(tp: string, v: number) {
const [storage, type, typeShift] = normalizeStorageType(tp)
if (isNaN(v)) return v // error already reported
if (storage == 0) {
error(`numeric values like ${v} not allowed for ${tp}`)
return v
}
if (v < 0 && storage > 0) {
error(`negative values like ${v} not allowed for ${tp}`)
return v
}
if (Math.floor(v) != v && typeShift == 0) {
error(`only integer values allowed for ${tp}; got ${v}`)
return v
}
let bits = storage < 0 ? -storage * 8 - 1 : storage * 8
bits -= typeShift || 0
// don't use bitshift to allow for more than 32 bits
let max = 1
while (bits--) max *= 2
if (-v == max) {
// OK - min_int
} else if (max == 1 && v == 1) {
// we make an exception for u0.8 being set to 1
} else {
if (Math.abs(v) >= max) {
error(`value ${v} is out of range for ${tp}`)
return v
}
}
return v
}
function parseVal(words: string[]) {
const eq = words.shift()
if (eq != "=" && eq != ":") error("expecting '='")
const val = words.shift()
return parseIntCheck(val, true)
}
function constant(words: string[]) {
if (words.length != 3) {
error(`define syntax is "define name value" (${words.join(" ")}}`)
return
}
const name = words[1]
const svalue = words[2]
const hex = /^0x/.test(svalue)
const value = hex ? parseInt(svalue, 16) : parseInt(svalue)
if (isNaN(value)) {
error("invalid numeric value for constant")
return
}
info.constants[name] = { value, hex }
}
function packetField(words: string[]) {
if (
words.length == 2 &&
(words[0] == "repeats" ||
words[0] == "segmented" ||
words[0] == "multi-segmented")
) {
nextModifier = words[0]
return
}
const name = normalizeName(words.shift())
let defaultValue: number = undefined
let isOptional: boolean = undefined
let op = words.shift()
if (op == "?") {
isOptional = true
op = words.shift()
}
if (op == "=") {
defaultValue = parseIntCheck(words.shift(), true)
op = words.shift()
}
if (op != ":") error("expecting ':'")
const tp = words.shift()
const [storage, type, typeShift] = normalizeStorageType(tp)
const isFloat = typeShift === null || undefined
let tok = words.shift()
let unit: jdspec.Unit
let encoding: jdspec.Encoding
if (tok != "{") {
if (type === "string" || type === "bytes")
encoding = normalizeEncoding(tok)
else unit = normalizeUnit(tok)
tok = words.shift()
}
if (defaultValue !== undefined) rangeCheck(tp, defaultValue)
let shift = typeShift || undefined
if (unit == "/") {
// / units should be used with ui0. data
if (!/^(u0|i1)\.\d+$/.test(tp))
error(
`fraction unit must be used with u0.yyy or i1.yyy data types (got ${tp})`,
)
shift = Math.abs(storage) * 8
if (storage < 0) shift -= 1
}
const field: jdspec.PacketMember = {
name,
unit,
encoding,
shift,
isFloat,
type,
storage,
isSimpleType: canonicalType(storage) == type || undefined,
defaultValue,
isOptional,
multiSegmented: nextModifier == "multi-segmented" || undefined,
segmented:
nextModifier == "segmented" ||
nextModifier == "multi-segmented" ||
undefined,
startRepeats: nextModifier == "repeats" || undefined,
}
if (!unit) delete field.unit
if (!encoding) delete field.encoding
if (tok == "{") {
while (words.length) {
tok = words.shift()
if (tok == "}") break
tok = camelize(tok)
switch (tok) {
case "maxBytes": {
// eslint-disable-next-line @typescript-eslint/no-extra-semi,@typescript-eslint/no-explicit-any
;(field as any)[tok] = rangeCheck("u8", parseVal(words))
break
}
case "typicalMin":
case "typicalMax":
case "absoluteMin":
case "absoluteMax": {
// eslint-disable-next-line @typescript-eslint/no-extra-semi,@typescript-eslint/no-explicit-any
;(field as any)[tok] = rangeCheck(tp, parseVal(words))
break
}
case "preferredInterval": {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((packetInfo as any)[tok] !== undefined)
error(`field ${tok} already set`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(packetInfo as any)[tok] = rangeCheck(
"u32",
parseVal(words),
)
break
}
default:
error("unknown constraint: " + tok)
break
}
if (words[0] == ",") words.shift()
}
if (tok == "}") tok = null
}
if (tok) error(`excessive tokens at the end of member: ${tok}...`)
if (
field.typicalMin === undefined &&
field.typicalMax !== undefined &&
storage > 0
)
field.typicalMin = 0
if (
field.absoluteMin === undefined &&
field.absoluteMax !== undefined &&
storage > 0
)
field.absoluteMin = 0
if (!field.storage && field.maxBytes) field.storage = field.maxBytes
if (/pipe/.test(type)) {
packetInfo.pipeType = packetInfo.name
if (
pipePacket &&
pipePacket.name == packetInfo.name &&
packetInfo.kind == "report"
) {
// keep old pipePacket
} else {
pipePacket = packetInfo
}
}
if (
!field.isOptional &&
packetInfo.fields[packetInfo.fields.length - 1]?.isOptional
) {
error(`all fields after an optional field have to optional`)
}
packetInfo.fields.push(field)
nextModifier = undefined
}
function startEnum(words: string[]) {
checkBraces(null)
let isClosed = false
if (words[0] == "type") {
if (words[2] != ":") error("expecting: type NAME : TYPE")
if (!words[4]) isClosed = true
else if (words[4] == "{") {
if (words[5] == "}") isClosed = true
else if (words[5]) error("excessive syntax")
} else {
error("expecting {")
}
} else {
if (words[2] != ":" || words[4] != "{")
error("expecting: enum NAME : TYPE {")
}
enumInfo = {
name: normalizeName(words[1]),
storage: normalizeStorageType(words[3])[0],
isFlags: words[0] == "flags" || undefined,
isExtensible: words[0] == "type" || undefined,
members: {},
}
if (info.enums[enumInfo.name]) error("enum redefinition")
info.enums[enumInfo.name] = enumInfo
if (isClosed) enumInfo = null
}
function enumMember(words: string[]) {
if (words[1] != "=" || words.length != 3)
error(`expecting: FIELD_NAME = INTEGER`)
enumInfo.members[normalizeName(words[0])] = rangeCheck(
canonicalType(enumInfo.storage),
parseIntCheck(words[2]),
)
}
function parseIntCheck(w: string, allowFloat = false) {
try {
return parseIntFloat(info, w, allowFloat)
} catch (e) {
error(e.message)
return 0
}
}
function looksRandom(n: number) {
const s = n.toString(16)
const h = "0123456789abcdef"
for (let i = 0; i < h.length; ++i) {
const hh = h[i]
if (s.indexOf(hh + hh + hh) >= 0) return false
}
if (/f00d|dead|deaf|beef/.test(s)) return false
return true
}
function genRandom() {
for (;;) {
const m = (Math.random() * 0xfff_ffff) | 0x1000_0000
if (looksRandom(m)) return m
}
}
function metadataMember(words: string[]) {
if (
!(
(words[1] == "=" || words[1] == ":") &&
(words[0] == "tags" || words.length == 3)
)
)
error(`expecting: FIELD_NAME = VALUE or FIELD_NAME : VALUE`)
switch (words[0]) {
case "extends":
processInclude(words[2])
break
case "class":
case "identifier": {
info.classIdentifier = parseIntCheck(words[2])
if (info.name != "Control" && info.classIdentifier == 0)
info.classIdentifier = 1
const gen = `how about ${toHex(genRandom())}`
if (
!(
info.classIdentifier == 0 ||
(0x1000_0001 <= info.classIdentifier &&
info.classIdentifier <= 0x1fff_ff00) ||
(0x2000_0001 <= info.classIdentifier &&
info.classIdentifier <= 0x2ffff_ff00)
)
)
error(`class identifier out of range; ${gen}`)
if (!looksRandom(info.classIdentifier))
error(`class identifier doesn't look random; ${gen}`)
if (usedIds[info.classIdentifier + ""])
error(
`class identifier ${toHex(
info.classIdentifier,
)} already used in ${
usedIds[info.classIdentifier + ""]
}; ${gen}`,
)
break
}
case "camel":
info.camelName = words[2]
break
case "short":
info.shortName = words[2]
break
case "high":
info.highCommands = !!parseIntCheck(words[2])
break
case "status":
if (
["stable", "experimental", "deprecated", "rc"].indexOf(
words[2],
) > -1
)
info.status = words[2]
else error("unknown status")
break
case "group":
info.group = capitalize(words.slice(2).join(" "))
break
case "restricted":
if (words[2] == "true") info.restricted = true
else if (words[2] == "false") delete info.restricted
else error("restricted: should be true or false")
break
case "tags": {
const tags = words.slice(2).filter(w => w != "," && w != ";")
info.tags = info.tags.concat(tags)
break
}
default:
error("unknown metadata field: " + words[0])
break
}
}
function processInclude(name: string) {
if (name == "_system") return
const inner = includes[name]
if (!inner) return error("include file not found: " + name)
if (
info.packets.some(pkt => !pkt.derived) ||
values(info.enums).some(e => !e.derived)
)
error("extends: only allowed on top of the .md file")
if (inner.errors) errors = errors.concat(inner.errors)
const innerEnums = clone(inner.enums)
Object.keys(innerEnums)
.filter(k => !info.enums[k])
.forEach(k => {
const ie = innerEnums[k]
ie.derived = name
info.enums[k] = ie
})
const innerPackets = clone(
inner.packets.filter(
pkt =>
!info.packets.find(
ipkt =>
ipkt.kind === pkt.kind &&
ipkt.identifier === pkt.identifier,
),
),
)
innerPackets.forEach(pkt => (pkt.derived = name))
info.packets = [...info.packets, ...innerPackets]
if (inner.highCommands) info.highCommands = true
info.extends = inner.extends.concat([name])
}
function clone(v: T): T {
return JSON.parse(JSON.stringify(v))
}
function error(msg: string) {
if (!msg) msg = "syntax error"
if (errors.some(e => e.line == lineNo && e.message == msg)) return
errors.push({ file: filename, line: lineNo, message: msg })
}
function warn(msg: string) {
if (info.camelName == "system") return // no warnings in _system
if (errors.some(e => e.line == lineNo && e.message == msg)) return
errors.push({ file: filename, line: lineNo, message: msg })
}
function normalizeName(n: string) {
if (!/^\w+$/.test(n)) error("expecting name here")
if (n.length > 31) error(`name '${n}' too long`)
return n
}
function normalizeStorageType(
tp: string,
): [jdspec.StorageType, string, number] {
if (info.enums[tp]) return [info.enums[tp].storage, tp, 0]
if (!tp) error("expecting type here")
const tp2 = tp.replace(/_t$/, "").toLowerCase()
const m = /^([ui])(\d+)\.(\d+)$/.exec(tp2)
if (m) {
const a = parseIntCheck(m[2])
const b = parseIntCheck(m[3])
const len = a + b
if (!(len == 8 || len == 16 || len == 32 || len == 64))
error(`fixed point ${tp} can't be ${len} bits`)
if (a == 0 && m[1] == "i")
error(
`fixed point ${tp} can't be i0.X; has to be at least i1.X`,
)
return [(m[1] == "i" ? -1 : 1) * (len >> 3), tp2, b]
}
switch (tp2) {
case "bool":
return [1, tp2, 0]
case "i8":
case "u8":
case "i16":
case "u16":
case "i32":
case "u32":
case "i64":
case "u64": {
let sz = parseIntCheck(tp2.replace(/^./, "")) >> 3
if (tp2[0] == "i") sz = -sz
return [sz, tp2, 0]
}
case "f16":
return [2, tp2, null]
case "f32":
return [4, tp2, null]
case "f64":
return [8, tp2, null]
case "pipe":
return [12, tp2, 0]
case "pipe_port":
return [2, tp2, 0]
case "devid":
return [8, tp2, 0]
case "bytes":
case "string":
case "string0":
return [0, tp2, 0]
default: {
const m = /^u8\[(\d+)\]$/.exec(tp2)
if (m) return [parseIntCheck(m[1]), tp2, 0]
error("unknown type: " + tp + " " + tp2)
return [4, tp2, 0]
}
}
}
function normalizeEncoding(unit: string): jdspec.Encoding {
return (unit && encodings[unit.toLowerCase()]) || undefined
}
function normalizeUnit(unit: string): jdspec.Unit {
if (unit === undefined || unit === null) return undefined
if (unitDescription[unit] || secondaryUnitConverters[unit])
return unit as jdspec.Unit
error(`expecting unit, got '${unit}'`)
return undefined
}
function paddingError(iface: jdspec.PacketInfo): string {
let byteOffset = 0
for (const m of iface.fields) {
const sz = memberSize(m)
if (sz == 0) break // no checking after zero-sized element
const pad = m.type == "bytes" ? 1 : sz > 8 ? 8 : sz
if (!/^u8\[/.test(m.type) && byteOffset % pad != 0)
return `need padding of ${
pad - (byteOffset % pad)
} byte(s) before ${m.name}`
byteOffset += sz
}
return null
}
}
function values(o: jdspec.SMap): T[] {
const r: T[] = []
for (const k of Object.keys(o)) r.push(o[k])
return r
}
function toUpper(name: string) {
return name
?.replace(/([a-z])([A-Z])/g, (x, a, b) => a + "_" + b)
.toUpperCase()
}
function toLower(name: string) {
return name
?.replace(/([a-z])([A-Z])/g, (x, a, b) => a + "_" + b)
.toLowerCase()
}
function packed(iface: jdspec.PacketInfo) {
if (!iface.packed) return ""
else return " __attribute__((packed))"
}
export function cStorage(tp: jdspec.StorageType) {
if (tp == 0 || [1, 2, 4, 8].indexOf(Math.abs(tp)) < 0) return "bytes"
if (tp < 0) return `int${-tp * 8}_t`
else return `uint${tp * 8}_t`
}
function cSharpStorage(tp: jdspec.StorageType) {
if (tp == 0 || [1, 2, 4, 8].indexOf(Math.abs(tp)) < 0) return "bytes"
switch (tp) {
case -1:
return "sbyte"
case 1:
return "byte"
case -2:
return "short"
case 2:
return "ushort"
case -4:
return "int"
case 4:
return "uint"
}
return `unknown({${tp})`
}
function canonicalType(tp: jdspec.StorageType): string {
if (tp == 0) return "bytes"
if (tp < 0) return `i${-tp * 8}`
else return `u${tp * 8}`
}
function isRegister(k: jdspec.PacketKind) {
return k == "ro" || k == "rw" || k == "const"
}
function toHex(n: number): string {
if (n === undefined) return ""
if (n < 0) return "-" + toHex(n)
return "0x" + n.toString(16)
}
function unitPref(f: jdspec.PacketMember) {
if (!f.unit) return ""
else return prettyUnit(f.unit) + " "
}
function prettyUnit(u: jdspec.Unit): string {
switch (u) {
case "us":
return "μs"
case "C":
return "°C"
case "/":
return "ratio"
default:
return u
}
}
function toPython(info: jdspec.ServiceSpec, language: "py" | "cpy" | "mpy") {
const r = [`# Autogenerated constants for ${info.name} service`]
if (Object.keys(info.enums).length) r.push("from enum import IntEnum")
const docsLength = r.length
const desktop = language === "py"
const packFormats: Record = {}
if (desktop) r.push("from jacdac.constants import *")
let pref = "JD_" + toUpper(info.shortName) + "_"
if (info.shortId[0] == "_") pref = "JD_"
if (info.shortId[0] != "_")
r.push(
`JD_SERVICE_CLASS_${toUpper(info.shortName)} = const(${toHex(
info.classIdentifier,
)})`,
)
for (const cst in info.constants) {
const { value, hex } = info.constants[cst]
r.push(
`JD_${toUpper(cst)} = const(${
hex ? value.toString() : toHex(value)
})\n`,
)
}
if (Object.keys(info.enums).length) {
for (const en of values(info.enums).filter(en => !en.derived)) {
r.push("")
r.push("")
r.push(
`class ${upperCamel(info.shortName)}${upperCamel(
en.name,
)}(IntEnum):`,
)
for (const k of Object.keys(en.members))
r.push(` ${toUpper(k)} = const(${toHex(en.members[k])})`)
}
r.push("")
r.push("")
}
let useIdentifiers = false
for (const pkt of info.packets) {
if (pkt.derived) continue
if (
!pkt.secondary &&
pkt.kind != "pipe_command" &&
pkt.kind != "pipe_report"
) {
let inner = "CMD"
if (isRegister(pkt.kind)) inner = "REG"
else if (pkt.kind == "event") inner = "EV"
else if (
pkt.kind == "meta_pipe_command" ||
pkt.kind == "meta_pipe_report"
)
inner = "PIPE"
let val = toHex(pkt.identifier)
if (pkt.identifierName) {
// TODO find identifier and inline it
val = "JD_" + inner + "_" + toUpper(pkt.identifierName)
useIdentifiers = true
}
const name = pref + inner + "_" + toUpper(pkt.name)
if (name != val) {
r.push(`${name} = const(${val})`)
if (pkt.packFormat) packFormats[name] = pkt.packFormat
}
}
}
if (desktop && useIdentifiers)
r.splice(docsLength + 1, 0, "from jacdac.system.constants import *")
r.push(`${pref}PACK_FORMATS = {`)
r.push(
Object.keys(packFormats)
.map(k => ` ${k}: "${packFormats[k]}"`)
.join(",\n"),
)
r.push(`}`)
r.push("")
return r.join("\n")
}
function toH(info: jdspec.ServiceSpec) {
let r = "// Autogenerated C header file for " + info.name + "\n"
const hdDef = `_JACDAC_SPEC_${toUpper(info.camelName)}_H`
r += `#ifndef ${hdDef}\n`
r += `#define ${hdDef} 1\n`
let pref = "JD_" + toUpper(info.shortName) + "_"
if (info.shortId[0] == "_") pref = "JD_"
r += `\n#define JD_SERVICE_CLASS_${toUpper(info.shortName)} ${toHex(
info.classIdentifier,
)}\n`
for (const cst in info.constants) {
const { value, hex } = info.constants[cst]
r += `#define ${pref}${toUpper(cst)} ${
hex ? toHex(value) : value.toString()
}\n`
}
for (const en of values(info.enums).filter(en => !en.derived)) {
const enPref = pref + toUpper(en.name)
r += `\n// enum ${en.name} (${cStorage(en.storage)})\n`
for (const k of Object.keys(en.members))
r +=
"#define " +
enPref +
"_" +
toUpper(k) +
" " +
toHex(en.members[k]) +
"\n"
}
for (const pkt of info.packets) {
if (pkt.derived) continue
const cmt = addComment(pkt)
r += wrapComment("h", cmt.comment)
if (
!pkt.secondary &&
pkt.kind != "pipe_command" &&
pkt.kind != "pipe_report"
) {
let inner = "CMD"
if (isRegister(pkt.kind)) inner = "REG"
else if (pkt.kind == "event") inner = "EV"
else if (
pkt.kind == "meta_pipe_command" ||
pkt.kind == "meta_pipe_report"
)
inner = "PIPE"
let val = toHex(pkt.identifier)
if (pkt.identifierName)
val = "JD_" + inner + "_" + toUpper(pkt.identifierName)
const name = pref + inner + "_" + toUpper(pkt.name)
if (name != val) r += `#define ${name} ${val}\n`
}
const isMetaPipe =
pkt.kind == "meta_pipe_report" || pkt.kind == "meta_pipe_command"
if (cmt.needsStruct || isMetaPipe) {
let tname =
"jd_" + toLower(info.shortName) + "_" + toLower(pkt.name)
if (pkt.kind == "report") tname += "_report"
r += `typedef struct ${tname} {\n`
if (isMetaPipe) {
r += ` uint32_t identifier; // ${toHex(pkt.identifier)}\n`
}
let unaligned = ""
for (let i = 0; i < pkt.fields.length; ++i) {
const f = pkt.fields[i]
let def = ""
let cst = cStorage(f.storage)
const sz = memberSize(f)
if (f.type == "string" || f.type == "string0")
def = `char ${f.name}[${sz}]`
else if (cst == "bytes") def = `uint8_t ${f.name}[${sz}]`
else {
if (f.isFloat) cst = f.storage == 4 ? "float" : "double"
def = `${cst} ${f.name}`
}
// if it's the last field and it start repeats, treat it as an array
if (f.startRepeats && i == pkt.fields.length - 1) def += "[0]"
def += ";"
if (!f.isSimpleType && f.type != "devid")
def += " // " + unitPref(f) + f.type
else if (f.unit) def += " // " + prettyUnit(f.unit)
r += " " + unaligned + def + "\n"
if (f.type == "string0") unaligned = "// "
}
r += `}${packed(pkt)} ${tname}_t;\n\n`
}
}
r += "\n#endif\n"
return r
}
export function camelize(name: string) {
if (!name) return name
return (
name[0].toLowerCase() +
name
.slice(1)
.replace(/\s+/g, "_")
.replace(/_([a-z0-9])/gi, (_, l) => l.toUpperCase())
)
}
export function capitalize(name: string) {
if (!name) return name
return name[0].toUpperCase() + name.slice(1)
}
function upperCamel(name: string) {
name = camelize(name)
if (!name?.length) return name
return name[0].toUpperCase() + name.slice(1)
}
export function snakify(name: string) {
return name
?.replace(/([a-z])([A-Z])/g, (_, a, b) => a + "_" + b)
.replace(/\s+/g, "_")
}
export function dashify(name: string) {
if (!name) return name
return snakify(name.replace(/^_+/, ""))
.replace(/(_|\s)+/g, "-")
.toLowerCase()
}
export function humanify(name: string) {
return name
?.replace(/([a-z])([A-Z])/g, (_, a, b) => a + " " + b)
.replace(/(-|_)/g, " ")
}
export function addComment(pkt: jdspec.PacketInfo) {
let comment = ""
let typeInfo = ""
let needsStruct = false
if (pkt.fields.length == 0) {
if (pkt.kind != "event") typeInfo = "No args"
} else if (pkt.fields.length == 1 && !pkt.fields[0].startRepeats) {
const f0 = pkt.fields[0]
typeInfo = cStorage(f0.storage)
if (!f0.isSimpleType) typeInfo = f0.type + " (" + typeInfo + ")"
typeInfo = unitPref(f0) + typeInfo
if (f0.name != "_") typeInfo = f0.name + " " + typeInfo
} else {
needsStruct = true
}
if (pkt.fields.length == 1) {
if (isRegister(pkt.kind)) {
let info = ""
if (pkt.kind == "ro") info = "Read-only"
else if (pkt.kind == "const") info = "Constant"
else info = "Read-write"
if (typeInfo) typeInfo = info + " " + typeInfo
else typeInfo = info
} else if (typeInfo) {
typeInfo = "Argument: " + typeInfo
}
}
if (pkt.kind == "report" && pkt.secondary) {
comment += "Report: " + typeInfo + "\n"
} else {
if (pkt.description) {
let desc = pkt.description.replace(/\n\n[^]*/, "")
if (typeInfo) desc = typeInfo + ". " + desc
comment = desc + "\n" + comment
}
}
return {
comment,
needsStruct,
}
}
export function wrapComment(lang: string, comment: string) {
if (lang === "cs")
return (
"\n/// \n/// " +
comment.replace(/\n+$/, "").replace(/\n/g, "\n/// ") +
"\n/// \n"
)
else
return (
"\n/**\n * " +
comment.replace(/\n+$/, "").replace(/\n/g, "\n * ") +
"\n */\n"
)
}
export function wrapSnippet(code: string) {
if (!code) return code
return `
\`\`\`
${code.replace(/^\n+/, "").replace(/\n+$/, "")}
\`\`\`
`
}
export const TYPESCRIPT_STATIC_NAMESPACE = "jacdac"
function packFormatForField(
info: jdspec.ServiceSpec,
fld: jdspec.PacketMember,
isStatic?: boolean,
useBooleans?: boolean,
) {
const sz = memberSize(fld)
const szSuff = sz ? `[${sz}]` : ``
let tsType = "number"
let pyType = "float"
let csType = "float"
let fmt = ""
if (/^[fiu]\d+(\.\d+)?$/.test(fld.type) && 1 <= sz && sz <= 8) {
fmt = fld.type
if (/^[iu]\d+$/.test(fld.type)) {
pyType = "int"
csType = "int"
}
if (/^[u]\d+$/.test(fld.type)) {
csType = "uint"
}
} else if (/^u8\[\d*\]$/.exec(fld.type)) {
fmt = "b" + szSuff
} else if (info.enums[fld.type]) {
fmt = canonicalType(info.enums[fld.type].storage)
pyType =
tsType =
csType =
upperCamel(info.camelName) + upperCamel(fld.type)
if (isStatic) tsType = TYPESCRIPT_STATIC_NAMESPACE + "." + tsType
} else {
switch (fld.type) {
case "string":
fmt = "s" + szSuff
csType = tsType = "string"
pyType = "str"
break
case "bytes":
fmt = "b" + szSuff
break
case "string0":
fmt = "z"
csType = tsType = "string"
pyType = "str"
break
case "devid":
fmt = "b[8]"
break
case "pipe_port":
fmt = "u16"
break
case "pipe":
fmt = "b[12]"
break
case "bool":
// TODO native bool support
fmt = "u8"
if (useBooleans) {
tsType = "boolean"
csType = pyType = "bool"
}
break
default:
return null
}
}
if (tsType == "number" && fmt && fmt[0] == "b") {
tsType = "Buffer"
pyType = "bytes"
csType = "byte[]"
}
return { fmt, tsType, pyType, csType }
}
/**
* Generates the format to pack/unpack a data payload for this packet
* @param pkt
* TODO fix this
*/
export function packFormat(
sinfo: jdspec.ServiceSpec,
pkt: jdspec.PacketInfo,
useBooleans?: boolean,
): string {
if (!pkt.fields?.length) return undefined
const fmt: string[] = []
for (const fld of pkt.fields) {
if (fld.startRepeats) fmt.push("r:")
const ff = packFormatForField(sinfo, fld, false, useBooleans)
if (!ff) return undefined
fmt.push(ff.fmt)
}
return fmt.join(" ")
}
export function packInfo(
info: jdspec.ServiceSpec,
pkt: jdspec.PacketInfo,
options?: {
isStatic?: boolean
useBooleans?: boolean
useJDOM?: boolean
},
) {
const {
isStatic = false,
useBooleans = false,
useJDOM = false,
} = options || {}
const { kind } = pkt
const vars: string[] = []
const vartp: string[] = []
const vartppy: string[] = []
const vartpcs: string[] = []
let fmt = ""
let repeats: string[]
let reptp: string[]
for (let i = 0; i < pkt.fields.length; ++i) {
const fld = pkt.fields[i]
let isArray = ""
if (fld.startRepeats) {
if (i == pkt.fields.length - 1) {
isArray = "[]"
} else {
fmt += "r: "
repeats = []
reptp = []
vars.push("rest")
}
}
const varname = camelize(fld.name == "_" ? pkt.name : fld.name)
const f0 = packFormatForField(info, fld, isStatic, useBooleans)
if (!f0 || /(reserved|padding)/.test(fld.name)) {
if (!f0)
console.log(
`${pkt.name}/${fld.name} - can't get format for '${fld.type}'`,
)
fmt += `x[${memberSize(fld)}] `
} else {
fmt += f0.fmt + isArray + " "
let tp = f0.tsType
let tpy = f0.pyType
let tcs = f0.csType
if (tp == "Buffer" && !isStatic) {
tp = "Uint8Array"
tpy = "bytes"
tcs = "byte[]"
}
tp += isArray
if (isArray) tpy = "[" + tpy + "]"
if (repeats) {
repeats.push(varname)
reptp.push(tp)
} else {
vars.push(varname)
vartp.push(tp)
vartppy.push(tpy)
vartpcs.push(tcs)
}
}
}
fmt = fmt.replace(/ *$/, "")
if (reptp) vartp.push("([" + reptp.join(", ") + "])[]")
const pktName = camelize(pkt.name)
let buffers = ""
if (useJDOM) {
if (kind === "command") {
for (let i = 0; i < vars.length; ++i)
buffers += `const ${vars[i]}: ${vartp[i]} = ...\n`
buffers += `await service.sendCmdPackedAsync(${capitalize(
info.camelName,
)}Reg.${capitalize(pktName)}, [${vars.join(", ")}])\n`
} else if (isRegister(kind)) {
buffers +=
"// get (register to REPORT_UPDATE event to enable background refresh)\n"
buffers += `const ${pktName}Reg = service.register(${capitalize(
info.camelName,
)}Reg.${capitalize(pktName)})\n`
buffers += `const [${vars.join(", ")}] : [${vartp.join(
", ",
)}] = ${pktName}Reg.unpackedValue\n`
if (kind === "rw") {
buffers += "// set\n"
buffers += `await ${pktName}Reg.sendSetPackedAsync([${vars.join(
", ",
)}])\n`
}
} else if (kind === "event") {
buffers += `const ${pktName}Event = service.event(${capitalize(
info.camelName,
)}Event.${capitalize(pktName)})\n`
buffers += `${pktName}Event.on("change", () => {
// if you need to read the event values
// const values = ${pktName}Event.unpackedValue
})\n`
}
} else {
buffers += `const [${vars.join(", ")}] = jdunpack<[${vartp.join(
", ",
)}]>(buf, "${fmt}")\n`
}
if (repeats) buffers += `const [${repeats.join(", ")}] = rest[0]\n`
buffers = buffers.replace(/\n*$/, "")
return {
buffers,
names: vars,
types: vartp,
pyTypes: vartppy,
csTypes: vartpcs,
}
}
function memberSize(fld: jdspec.PacketMember) {
return Math.abs(fld.storage)
}
function toTypescript(info: jdspec.ServiceSpec, language: "ts" | "sts" | "cs") {
const ts = language === "ts"
const sts = language === "sts"
const csharp = language === "cs"
const useNamespace = sts || csharp
const indent = useNamespace ? " " : ""
const indent2 = indent + " "
const numberkw = csharp ? "uint " : ""
const hexkw = csharp ? "byte[]" : ""
const enumkw = csharp
? indent + "public enum"
: sts
? indent + "export const enum"
: "export enum"
const exportkw = csharp ? "public" : "export"
const enumsf = csharp ? "public const string " : "export const "
const cskw = csharp ? ";" : ""
let r = useNamespace
? `namespace ${
csharp
? capitalize(TYPESCRIPT_STATIC_NAMESPACE)
: TYPESCRIPT_STATIC_NAMESPACE
} {\n`
: ""
if (csharp) {
r += `${indent}public static partial class ServiceClasses\n${indent}{\n`
} else r += indent + "// Service " + info.name + " constants\n"
if (info.shortId[0] != "_") {
const name = csharp
? capitalize(info.camelName)
: `SRV_${snakify(info.camelName).toLocaleUpperCase()}`
r +=
indent +
(csharp ? indent : "") +
`${exportkw} const ${numberkw}${name} = ${toHex(
info.classIdentifier,
)}${cskw}\n`
}
const pref = upperCamel(info.camelName)
for (const cst in info.constants) {
const name = csharp
? capitalize(info.camelName)
: `CONST_${snakify(info.camelName).toLocaleUpperCase()}_`
const { value, hex } = info.constants[cst]
r +=
indent +
(csharp ? indent : "") +
`${exportkw} const ${hex ? hexkw : numberkw}${name}${
csharp ? capitalize(camelize(cst)) : toUpper(cst)
} = ${hex ? value.toString() : toHex(value)}${cskw}\n`
}
if (csharp) {
r += indent + `}\n`
}
for (const en of values(info.enums)) {
const enPref = pref + upperCamel(en.name)
r += `\n${
csharp && en.isFlags ? " [System.Flags]\n" : ""
}${enumkw} ${enPref}${
csharp ? `: ${cSharpStorage(en.storage)}` : ""
} { // ${cStorage(en.storage)}\n`
for (const k of Object.keys(en.members)) {
if (sts) r += indent2 + `//% block="${humanify(k).toLowerCase()}"\n`
r += indent2 + k + " = " + toHex(en.members[k]) + ",\n"
}
r += indent + "}\n\n"
}
const tsEnums: jdspec.SMap = {}
for (const pkt of info.packets) {
if (pkt.derived) continue
const cmt = addComment(pkt)
const pack = pkt.fields.length
? packInfo(info, pkt, {
isStatic: sts,
useBooleans: false,
}).buffers
: ""
let inner = "Cmd"
if (isRegister(pkt.kind)) inner = "Reg"
else if (pkt.kind == "event") inner = "Event"
else if (
pkt.kind == "meta_pipe_command" ||
pkt.kind == "meta_pipe_report"
)
inner = "PipeCmd"
else if (pkt.kind == "pipe_command" || pkt.kind == "pipe_report")
inner = "Pipe"
let text = ""
let meta = ""
if (pkt.secondary || inner == "Pipe") {
if (pack)
text = wrapComment(
language,
`${pkt.kind} ${upperCamel(pkt.name)}${
pkt.client ? "" : wrapSnippet(pack)
}`,
)
} else {
const val = toHex(pkt.identifier)
if (sts && pkt.kind === "event") {
meta = `//% block="${snakify(pkt.name).replace(/_/g, " ")}"\n`
}
text = `${
wrapComment(
language,
cmt.comment + (pkt.client ? "" : wrapSnippet(pack)),
) + meta
}${upperCamel(pkt.name)} = ${val},\n`
}
if (text) tsEnums[inner] = (tsEnums[inner] || "") + text
// don't emit const strings in makecode,
// they don't get dropped efficiently
if ((csharp || sts || ts) && pkt.packFormat) {
const packName = inner + "Pack"
tsEnums[packName] =
(tsEnums[packName] || "") +
`${wrapComment(
language,
`Pack format for '${pkt.name}' data.`,
)}${enumsf}${upperCamel(pkt.name)}${
pkt.secondary ? "Report" : ""
} = "${pkt.packFormat}"${csharp ? ";" : ""}\n`
}
}
for (const k of Object.keys(tsEnums)) {
if (k == "info") r += tsEnums[k].replace(/^/gm, indent) + "\n\n"
else {
const inner = tsEnums[k]
.replace(/^\n+/, "")
.replace(/\n$/, "")
.replace(/\n/g, "\n " + indent)
if (inner.indexOf("public const") > -1 || k.endsWith("Pack")) {
r += ` ${exportkw} ${csharp ? "static " : ""}${
csharp ? "class" : "namespace"
} ${pref}${k} {\n ${indent}${inner}\n${indent}}\n\n`
} else
r += `${enumkw} ${pref}${k} ${
csharp ? `: ushort ` : ""
}{\n ${indent}${inner}\n${indent}}\n\n`
}
}
if (useNamespace) r += "}\n"
return r.replace(/ *$/gm, "")
}
const jsKeywords: Record = {
switch: 1,
}
export function jsQuote(n: string) {
if (jsKeywords[n]) n += "_"
return n
}
export function generateDeviceSpecificationId(dev: jdspec.DeviceSpec) {
return (
escapeDeviceIdentifier(dev.company) +
"-" +
escapeDeviceNameIdentifier(dev.name) +
(dev.designIdentifier || "") +
(dev.version
? `v${dev.version
.toLowerCase()
.replace(/^v/, "")
.replace(/\./g, "")}`
: ""
).toLowerCase()
)
}
export function normalizeDeviceSpecification(dev: jdspec.DeviceSpec) {
const productIdentifiers = Array.from(
new Set([
...(dev.productIdentifiers || []),
...(dev.firmwares
?.map(fw => fw.productIdentifier)
.filter(pi => !!pi) || []),
]).values(),
)
// reorder fields
const clone: jdspec.DeviceSpec = {
id: generateDeviceSpecificationId(dev),
name: dev.name,
company: dev.company,
description: dev.description,
repo: dev.repo,
makeCodeRepo: dev.makeCodeRepo,
firmwareSource: dev.firmwareSource,
hardwareDesign: dev.hardwareDesign,
connector: dev.connector,
link: dev.link,
storeLink: dev.storeLink,
services: dev.services,
productIdentifiers: productIdentifiers,
transport: dev.transport,
tags: dev.tags,
firmwares: dev.firmwares,
version: dev.version ? dev.version.replace(/^v/, "") : undefined,
designIdentifier: dev.designIdentifier || undefined,
bootloader: dev.bootloader,
status: dev.status || (dev.storeLink ? "stable" : undefined),
devices: dev.devices,
relatedDevices: dev.relatedDevices,
requiredDevices: dev.requiredDevices,
shape: dev.shape,
order: dev.order,
}
// delete empty files
const anyClone: any = clone
for (const key of Object.keys(anyClone)) {
const v = anyClone[key]
if (v === undefined || v === "" || (Array.isArray(v) && !v.length))
delete anyClone[key]
}
return clone
}
export function escapeDeviceIdentifier(text: string) {
if (!text) text = ""
const escaped = text
.trim()
.toLowerCase()
.replace(/([^a-z0-9_-])+/gi, "-")
.replace(/\./g, "") // routing does not like dots
.replace(/^-+/, "")
.replace(/-+$/, "")
const id = snakify(escaped)
return id
}
export function escapeDeviceNameIdentifier(text: string) {
return escapeDeviceIdentifier(text).replace(/-/g, "")
}
export function converters(): jdspec.SMap<(s: jdspec.ServiceSpec) => string> {
return {
json: (j: jdspec.ServiceSpec) => JSON.stringify(j, null, 2),
c: toH,
ts: j => toTypescript(j, "ts"),
sts: j => toTypescript(j, "sts"),
cs: j => toTypescript(j, "cs"),
py: j => toPython(j, "py"),
/*
"cpp": toHPP,
*/
}
}
export function isNumericType(field: jdspec.PacketMember) {
const tp = field.type
return (
!field.startRepeats &&
/^[uif]\d+(\.\d+)?$/.test(tp) &&
tp != "pipe_port" &&
tp != "bool"
)
}
const Reading = 0x101
export function genFieldInfo(
reg: jdspec.PacketInfo,
field: jdspec.PacketMember,
) {
const isReading = reg.identifier === Reading
const name =
field.name === "_"
? reg.name
: isReading
? field.name
: `${reg.name}${capitalize(field.name)}`
const min = pick(
field.typicalMin,
field.absoluteMin,
field.unit === "/" || /^%/.test(field.unit)
? field.type[0] === "i"
? -100
: 0
: undefined,
field.type === "u8" || field.type === "u16" ? 0 : undefined,
)
const max = pick(
field.typicalMax,
field.absoluteMax,
field.unit === "/" || /^%/.test(field.unit) ? 100 : undefined,
field.type === "u8" ? 0xff : field.type === "u16" ? 0xffff : undefined,
)
const defl = field.defaultValue || (field.unit === "/" ? "100" : undefined)
const valueScaler: (s: string) => string =
field.unit === "/"
? s => `${s} * 100`
: field.type === "bool"
? s => `!!${s}`
: s => s
const valueUnscaler: (s: string) => string =
field.unit === "/"
? s => `${s} / 100`
: field.type === "bool"
? s => `${s} ? 1 : 0`
: s => s
const scale = field.unit === "/" ? 100 : undefined
let unit: string = field.unit === "/" ? "%" : field.unit
unit = unit?.replace("%", "\\\\%")
return { name, min, max, defl, scale, valueScaler, valueUnscaler, unit }
function pick(...values: number[]) {
return values?.find(x => x !== undefined)
}
}