// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import { NumberFormat } from "./buffer" import serviceSpecificationData from "../../jacdac-spec/dist/services.json" import { bufferEq, fromHex, toggleBit, toHex } from "./utils" import { SystemEvent, SystemReg, SensorReg, SRV_CONTROL, SRV_ROLE_MANAGER, SRV_SETTINGS, SRV_BOOTLOADER, SRV_LOGGER, SRV_INFRASTRUCTURE, SRV_PROTO_TEST, SRV_PROXY, SRV_UNIQUE_BRAIN, SRV_DASHBOARD, SRV_BRIDGE, SRV_DEVICE_SCRIPT_CONDITION, SRV_DEVICE_SCRIPT_MANAGER, SRV_DEVS_DBG, } from "./constants" // eslint-disable-next-line @typescript-eslint/no-explicit-any let _serviceSpecifications: jdspec.ServiceSpec[] = serviceSpecificationData as any let _serviceSpecificationMap: Record = undefined /** * Override built-in service specifications * @param specs * @category Specification */ export function loadServiceSpecifications( specifications: jdspec.ServiceSpec[], ): { added: jdspec.ServiceSpec[] errors: { message: string; spec: jdspec.ServiceSpec }[] changed: boolean } { const previous = _serviceSpecifications // combine builtin specs with new specs const builtins = (serviceSpecificationData || []) as any as jdspec.ServiceSpec[] const specs = builtins.slice(0) const added: jdspec.ServiceSpec[] = [] const errors: { message: string; spec: jdspec.ServiceSpec }[] = [] if (specifications?.length) { const serviceClasses = new Set( specs.map(s => s.classIdentifier), ) const shortIds = new Set(specs.map(s => s.shortId)) for (const spec of specifications) { if (serviceClasses.has(spec.classIdentifier)) { const existingSpec = specs.find( s => s.classIdentifier === spec.classIdentifier, ) if (JSON.stringify(existingSpec) === JSON.stringify(spec)) continue // inserting a duplicate, ignore errors.push({ message: `classIdentifier 0x${spec.classIdentifier.toString( 16, )} already in use`, spec, }) continue } if (shortIds.has(spec.shortId)) { errors.push({ message: "shortId already in use", spec, }) continue } specs.push(spec) added.push(spec) serviceClasses.add(spec.classIdentifier) shortIds.add(spec.shortId) } } const changed = JSON.stringify(previous) !== JSON.stringify(specs) if (changed) { _serviceSpecifications = specs _serviceSpecificationMap = undefined } return { added, errors, changed } } /** * Returns a map from service short ids to service specifications * @category Specification */ export function serviceMap(): Record { const m: Record = {} _serviceSpecifications.forEach(spec => (m[spec.shortId] = spec)) return m } /** * Returns the list of service specifications * @category Specification */ export function serviceSpecifications() { return _serviceSpecifications.slice(0) } /** * Checks if classIdentifier is compatible with requiredClassIdentifier * @category Specification */ export function isInstanceOf( classIdentifier: number, requiredClassIdentifier: number, ): boolean { // garbage data if (isNaN(classIdentifier)) return false // direct hit if (classIdentifier === requiredClassIdentifier) return true // lookup inheritance chain const classSpec = serviceSpecificationFromClassIdentifier(classIdentifier) return !!classSpec?.extends?.some(extend => { const extendSpec = serviceSpecificationFromName(extend) return ( !!extendSpec && isInstanceOf(extendSpec.classIdentifier, requiredClassIdentifier) ) }) } /** * Checks if the service supports the Jacdac infrastructure * @param spec * @returns * @category Specification */ export function isInfrastructure(spec: jdspec.ServiceSpec) { return ( spec && ([ SRV_CONTROL, SRV_ROLE_MANAGER, SRV_LOGGER, SRV_SETTINGS, SRV_BOOTLOADER, SRV_PROTO_TEST, SRV_INFRASTRUCTURE, SRV_PROXY, SRV_UNIQUE_BRAIN, SRV_DASHBOARD, SRV_BRIDGE, SRV_DEVICE_SCRIPT_CONDITION, SRV_DEVICE_SCRIPT_MANAGER, SRV_DEVS_DBG, ].indexOf(spec.classIdentifier) > -1 || spec.shortId[0] === "_") ) } /** * Looks up a service specification by name * @param shortId * @category Specification */ export function serviceSpecificationFromName( shortId: string, ): jdspec.ServiceSpec { if (!shortId) return undefined return _serviceSpecifications.find(s => s.shortId === shortId) } /** * Looks up a service specification by class * @param classIdentifier * @category Specification */ export function serviceSpecificationFromClassIdentifier( classIdentifier: number, ): jdspec.ServiceSpec { if (isNaN(classIdentifier)) return undefined // try lookup cache let srv = _serviceSpecificationMap?.[classIdentifier] if (srv) return srv // resolve srv = _serviceSpecifications.find( s => s.classIdentifier === classIdentifier, ) if (srv) { if (!_serviceSpecificationMap) _serviceSpecificationMap = {} _serviceSpecificationMap[classIdentifier] = srv } return srv } /** * Indicates if the specified service is a sensor * @param spec * @returns * @category Specification */ export function isSensor(spec: jdspec.ServiceSpec): boolean { return ( spec && spec.packets.some(pkt => isReading(pkt)) && spec.packets.some(pkt => pkt.identifier == SensorReg.StreamingSamples) ) } /** * Indicates if the specified service is an actuator * @param spec * @returns * @category Specification */ export function isActuator(spec: jdspec.ServiceSpec): boolean { return ( spec && spec.packets.some(pkt => pkt.identifier === SystemReg.Value) && spec.packets.some(pkt => pkt.identifier === SystemReg.Intensity) ) } /** * Indicates if the packet information is a register * @param spec * @returns * @category Specification */ export function isRegister(pkt: jdspec.PacketInfo) { return pkt && (pkt.kind == "const" || pkt.kind == "ro" || pkt.kind == "rw") } /** * Indicates if the packet information is a ``reading`` register * @param spec * @returns * @category Specification */ export function isReading(pkt: jdspec.PacketInfo) { return pkt && pkt.kind == "ro" && pkt.identifier == SystemReg.Reading } const ignoredRegister = [ SystemReg.StatusCode, SystemReg.InstanceName, SystemReg.StreamingInterval, SystemReg.StreamingPreferredInterval, SystemReg.StreamingSamples, SystemReg.ReadingError, SystemReg.ReadingResolution, SystemReg.MinReading, SystemReg.MaxReading, SystemReg.MinValue, SystemReg.MaxValue, SystemReg.MaxPower, ] /** * Indicates if the register is usable from a high-level programming environment. * @category Specification */ export function isHighLevelRegister(pkt: jdspec.PacketInfo) { return ( isRegister(pkt) && !pkt.lowLevel && !pkt.internal && ignoredRegister.indexOf(pkt.identifier) < 0 ) } const ignoredEvents = [SystemEvent.StatusCodeChanged] /** * Indicates if the event is usable from a high-level programming environment. * @category Specification */ export function isHighLevelEvent(pkt: jdspec.PacketInfo) { return ( isEvent(pkt) && !pkt.lowLevel && !pkt.internal && ignoredEvents.indexOf(pkt.identifier) < 0 ) } /** * Indicate if the register code is an auxilliary register to support streaming. * @param code * @returns * @category Specification */ export function isOptionalReadingRegisterCode(code: number) { const regs = [ SystemReg.MinReading, SystemReg.MaxReading, SystemReg.ReadingError, SystemReg.ReadingResolution, SystemReg.StreamingPreferredInterval, SystemReg.StreamingInterval, SystemReg.StreamingSamples, ] return regs.indexOf(code) > -1 } /** * Indicates if the packet info represents an ``intensity`` register * @category Specification */ export function isIntensity(pkt: jdspec.PacketInfo) { return pkt && pkt.kind == "rw" && pkt.identifier == SystemReg.Intensity } /** * Indicates if the packet info represents a ``value`` register * @category Specification */ export function isValue(pkt: jdspec.PacketInfo) { return pkt && pkt.kind == "rw" && pkt.identifier == SystemReg.Value } /** * Indicates if the packet info represents a ``intensity`` or a ``value`` register * @category Specification */ export function isValueOrIntensity(pkt: jdspec.PacketInfo) { return ( pkt && pkt.kind == "rw" && (pkt.identifier == SystemReg.Value || pkt.identifier == SystemReg.Intensity) ) } /** * Indicates if the packet info represents an ``const`` register * @category Specification */ export function isConstRegister(pkt: jdspec.PacketInfo) { return pkt?.kind === "const" } /** * Indicates if the packet info is not rw */ export function isReadOnlyRegister(pkt: jdspec.PacketInfo) { return pkt?.kind !== "rw" } /** * Indicates if the packet info represents an ``event`` * @category Specification */ export function isEvent(pkt: jdspec.PacketInfo) { return pkt.kind == "event" } /** * Indicates if the packet info represents a ``command`` * @category Specification */ export function isCommand(pkt: jdspec.PacketInfo) { return pkt.kind == "command" } /** * Indicates if the packet info represents a ``pipe_report`` * @category Specification */ export function isPipeReport(pkt: jdspec.PacketInfo) { return pkt.kind == "pipe_report" } /** * Indicates if the `report` packet is the report specication of the `cmd` command. * @category Specification */ export function isReportOf(cmd: jdspec.PacketInfo, report: jdspec.PacketInfo) { return ( report.secondary && report.kind == "report" && cmd.kind == "command" && cmd.name == report.name ) } /** * Indicates if the `report` packet is the *pipe* report specication of the `cmd` command. * @category Specification */ export function isPipeReportOf( cmd: jdspec.PacketInfo, pipeReport: jdspec.PacketInfo, ) { return ( pipeReport.kind == "pipe_report" && cmd.kind == "command" && cmd.pipeType && cmd.pipeType === pipeReport.pipeType ) } /** * @internal */ export function isIntegerType(tp: string) { return /^[ui]\d+(\.|$)/.test(tp) || tp == "pipe_port" || tp == "bool" } /** * @internal */ export function numberFormatFromStorageType(tp: jdspec.StorageType) { switch (tp) { case -1: return NumberFormat.Int8LE case 1: return NumberFormat.UInt8LE case -2: return NumberFormat.Int16LE case 2: return NumberFormat.UInt16LE case -4: return NumberFormat.Int32LE case 4: return NumberFormat.UInt32LE case -8: return NumberFormat.Int64LE case 8: return NumberFormat.UInt64LE case 0: return null default: return null } } /** * @internal */ export function numberFormatToStorageType(nf: NumberFormat) { switch (nf) { case NumberFormat.Int8LE: return -1 case NumberFormat.UInt8LE: return 1 case NumberFormat.Int16LE: return -2 case NumberFormat.UInt16LE: return 2 case NumberFormat.Int32LE: return -4 case NumberFormat.UInt32LE: return 4 case NumberFormat.Int64LE: return -8 case NumberFormat.UInt64LE: return 8 default: return null } } /** * @internal */ export function scaleIntToFloat(v: number, info: jdspec.PacketMember) { if (!info.shift) return v if (info.shift < 0) return v * (1 << -info.shift) else return v / (1 << info.shift) } /** * @internal */ export function scaleFloatToInt(v: number, info: jdspec.PacketMember) { if (!info.shift) return v if (info.shift < 0) return Math.round(v / (1 << -info.shift)) else return Math.round(v * (1 << info.shift)) } /** * @internal */ export function storageTypeRange(tp: jdspec.StorageType): [number, number] { if (tp == 0) throw new Error("no range for 0") if (tp < 0) { const v = Math.pow(2, -tp * 8 - 1) return [-v, v - 1] } else { const v = Math.pow(2, tp * 8) return [0, v - 1] } } /** * @internal */ export function clampToStorage(v: number, tp: jdspec.StorageType) { if (tp == null) return v // no clamping for floats const [min, max] = storageTypeRange(tp) if (isNaN(v)) return 0 if (v < min) return min if (v > max) return max return v } /** * @internal */ export function memberValueToString( value: any, info: jdspec.PacketMember, ): string { if (value === undefined || value === null) return "" switch (info.type) { case "bytes": return toHex(value) case "string": return value default: return "" + value } } /** * @internal */ export function tryParseMemberValue( text: string, info: jdspec.PacketMember, ): { value?: any; error?: string } { if (!text) return {} if (info.type === "string") return { value: text } else if (info.type === "pipe") return {} // not supported else if (info.type === "bytes") { try { return { value: fromHex(text) } } catch (e) { return { error: "invalid hexadecimal format", } } } else { const n = isIntegerType(info.type) ? parseInt(text) : parseFloat(text) if (isNaN(n)) return { error: "invalid format" } else return { value: n } } } /** * Parses a device identifier into a buffer, returns undefined if invalid * @param id * @returns * @category Specification */ export function parseDeviceId(id: string): Uint8Array { if (!id) return undefined id = id.replace(/\s/g, "") if (id.length != 16 || !/^[a-f0-9]+$/i.test(id)) return undefined return fromHex(id) } export function parseDualDeviceId(id: string) { const rid = parseDeviceId(id) toggleBit(rid, 0) return rid } export function dualDeviceId(id: string): string { return toHex(parseDualDeviceId(id)) } /** * Check if the left device identifier is a bootloader dual of the right identifier * @param left * @param right * @returns */ export function isDualDeviceId(left: string, right: string) { const lid = parseDeviceId(left) const rid = parseDualDeviceId(right) return bufferEq(lid, rid) }