diff --git a/lib/src/aasStore.ts b/lib/src/aasStore.ts index b9c892a..c0e9c86 100644 --- a/lib/src/aasStore.ts +++ b/lib/src/aasStore.ts @@ -2,6 +2,8 @@ import { readdirSync, readFileSync } from "fs"; import { types, jsonization } from "@aas-core-works/aas-core3.0-typescript"; +const GLOBALLY_IDENTIFIABLES = [types.KeyTypes.GlobalReference, types.KeyTypes.AssetAdministrationShell, types.KeyTypes.ConceptDescription, types.KeyTypes.Identifiable, types.KeyTypes.Submodel] + export default class AASStore { private static instance: AASStore; private envs: Array = []; @@ -57,11 +59,8 @@ export default class AASStore { public findAASById(id: string): types.AssetAdministrationShell | null { for (const env of this.envs) { - if (env.assetAdministrationShells === null) continue; - - for (const aas of env.assetAdministrationShells) { - if (aas.id === id) return aas; - } + const aas = AASStore.findAASById(env, id); + if(aas) return aas; } return null; @@ -69,22 +68,78 @@ export default class AASStore { public findSMById(id: string): types.Submodel | null { for (const env of this.envs) { - if (env.submodels === null) continue; - - for (const sm of env.submodels) { - if (sm.id === id) return sm; - } + const sm = AASStore.findSMById(env, id); + if (sm) return sm; } return null; } - public findElement(submodelId: string, elementIdShort: string): types.Class | null { - const sm = this.findSMById(submodelId); - if (!sm) return null; + public findSMByIdShort(id: string): types.Submodel | null { + for (const env of this.envs) { + const sm = AASStore.findSMByIdShort(env, id); + if (sm) return sm; + } - for (const element of sm.descend()) { - if ((element as any).idShort === elementIdShort) return element; + return null; + } + + public resolveReference(ref: types.Reference): types.Class | null { + for (const env of this.envs) { + const e = AASStore.resolveReference(env, ref); + if (e !== null) return e; + } + + return null; + } + + public findElement(checkFunction: (element: types.Class) => boolean): types.Class | null { + for (const env of this.envs) { + const e = AASStore.findElement(env, checkFunction) + if (e !== null) return e; + } + + return null; + } + + public static findAASById(environment: types.Environment, id: string): types.AssetAdministrationShell | null { + if (environment.assetAdministrationShells === null) return null; + + return (environment.assetAdministrationShells.find(aas => aas.id === id)) ?? null; + } + + public static findSMById(environment: types.Environment, id: string): types.Submodel | null { + if (environment.submodels === null) return null; + + return (environment.submodels.find(sm => sm.id === id)) ?? null; + } + + public static findSMByIdShort(environment: types.Environment, id: string): types.Submodel | null { + if (environment.submodels === null) return null; + + return (environment.submodels.find(sm => sm.idShort === id)) ?? null; + } + + public static resolveReference(env: types.Environment, ref: types.Reference): types.Class | null { + if (ref.type === types.ReferenceTypes.ExternalReference) return null; // Not implemented + + let current: types.Class | null = null; + for (const key of ref.keys) { + if (current === null && !GLOBALLY_IDENTIFIABLES.includes(key.type)) break; + + if (current === null) { + current = AASStore.findElement(env, element => (element as any).id === key.value) + } else { + current = AASStore.findElement(current, element => (element as any).idShort === key.value) + } + } + + return current; + } + + public static findElement(environment: types.Class, checkFunction: (element: types.Class) => boolean): types.Class | null { + for (const element of environment.descend()) { + if (checkFunction(element)) return element; } return null; diff --git a/lib/src/example_modules/httpInterfaceServer.ts b/lib/src/example_modules/httpInterfaceServer.ts index feb6584..1c30712 100644 --- a/lib/src/example_modules/httpInterfaceServer.ts +++ b/lib/src/example_modules/httpInterfaceServer.ts @@ -49,4 +49,8 @@ export default class HTTPInterfaceServer extends AASInterfaceServer { protected findElementByRequest(request: Request) { } + + private createRoutes()/*: express.Router*/ { + // TODO + } } \ No newline at end of file diff --git a/lib/src/index.ts b/lib/src/index.ts index e7516b8..75dd4bc 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -1,23 +1,18 @@ -import { readFileSync } from "fs"; -import * as aasCore from "@aas-core-works/aas-core3.0-typescript"; +import AASStore from "./aasStore"; +import type { SubmodelElementCollection, SubmodelElementList, ReferenceElement, Reference } from "@aas-core-works/aas-core3.0-typescript/dist/types/types"; -const aasFile = readFileSync("../AID-AIMC-Full-Example-20230911T1608.json", {encoding: "utf-8"}); -const aasJSON = JSON.parse(aasFile); +const store = AASStore.getInstance(); +store.addAASByPath("../AID-AIMC-Full-Example-20230911T1608.json"); -const env = aasCore.jsonization.environmentFromJsonable(aasJSON); +const aid = store.findSMByIdShort("AssetInterfacesDescription"); +const aimc = store.findSMByIdShort("AssetInterfacesMappingConfiguration"); -if (env.error || !env.value || !env.value.assetAdministrationShells || !env.value.submodels) throw new Error("Invalid AAS file"); +if (!aid || !aimc) throw new Error("Invalid AAS file"); -const aas = env.value.assetAdministrationShells[0]; -const submodels = env.value.submodels; +const AIMCEndpointRef = (((aimc.submodelElements?.at(0) as SubmodelElementList) + .value?.at(0) as SubmodelElementCollection) + .value?.at(0) as ReferenceElement) + .value as Reference; -const aid = submodels[0].submodelElements -if (aid === null) throw new Error("Invalid AAS file"); +// console.log(store.resolveReference(AIMCEndpointRef)); -console.log(((aid[0] as aasCore.types.SubmodelElementList).value as aasCore.types.SubmodelElementCollection[])[0]) - -/* -for (const something of aas.descend()) { - console.log(something); - console.log("---------------------------------------"); -}*/ diff --git a/lib/src/multimessageBroker.ts b/lib/src/multimessageBroker.ts index 4ce3e56..7e9858f 100644 --- a/lib/src/multimessageBroker.ts +++ b/lib/src/multimessageBroker.ts @@ -9,7 +9,7 @@ class MultiMessageBroker { private serverInterfaces: Array = []; private serverInterfaceConfigs: Array = []; - private serverInstances: Array|null> = []; + private serverInstances: Array|null> = []; private connectorInterfaces: Array = []; private connectorConfigs: Array = []; diff --git a/lib/src/parser/AIDParser.ts b/lib/src/parser/AIDParser.ts new file mode 100644 index 0000000..4570ce6 --- /dev/null +++ b/lib/src/parser/AIDParser.ts @@ -0,0 +1,150 @@ +import * as aasCore from "@aas-core-works/aas-core3.0-typescript"; +import type { AssetInterfaceDescription, EndpointMetadata, InterfaceDescription, InterfaceMetadata, InterfaceAction, InterfaceEvent, InterfaceProperty, InterfacePropertyForm } from "types/aidConf"; +import { AvailableEndpoint, endpointAvailable } from "types/common"; +import AASStore from "aasStore"; + +export default class AIDParser { + + public static parse(env: aasCore.types.Environment): AssetInterfaceDescription | null { + const sm = AASStore.findSMByIdShort(env, "AssetInterfaceDescription"); + if (sm === null || sm.submodelElements === null) return null; + + const parsed = {} as AssetInterfaceDescription; + + sm.submodelElements.forEach(e => { + if (!aasCore.types.isSubmodelElementList(e) || !e.idShort || !e.value) return; + + const endpointProto = endpointAvailable(e.idShort); + if (!endpointProto) return; + + parsed[endpointProto] = e.value.map(element => { + if (!aasCore.types.isSubmodelElementCollection(element)) return null; + + return AIDParser.parseAIDEntry(element, endpointProto); + }).filter(e => e !== null) as InterfaceDescription[]; + }); + + return parsed; + } + + private static parseAIDEntry(entry: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceDescription | null { + const title = AASStore.findElement(entry, element => (element as any).idShort.toLocaleLowerCase() === "title"); + const titleString = title !== null && aasCore.types.isProperty(title) && title.value ? title.value : ""; + + const ep = AASStore.findElement(entry, element => (element as any).idShort.toLocaleLowerCase() === "endpointmetadata"); + const im = AASStore.findElement(entry, element => (element as any).idShort.toLocaleLowerCase() === "interfacemetadata"); + if (ep === null || !aasCore.types.isSubmodelElementCollection(ep) || !ep.value || im === null || !aasCore.types.isSubmodelElementCollection(im) || !im.value) return null; + + const parsedEp = AIDParser.parseEndpointMetaData(ep, endpointProtocol); + const parsedIm = AIDParser.parseInterfaceMetaData(im, endpointProtocol); + if (parsedEp === null || parsedIm === null) return null; + + return { + title: titleString, + EndPointMetadata: parsedEp, + InterfaceMetadata: parsedIm + } as InterfaceDescription; + + } + + public static parseEndpointMetaData(endpoint: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): EndpointMetadata | null { + if (!endpoint.value || endpoint.idShort?.toLocaleLowerCase() !== "endpointmetadata") return null; + + const parsed = {} as EndpointMetadata; + + for (const key of ["base", "contentType"]) { + const prop = AASStore.findElement(endpoint, element => (element as any).idShort.toLocaleLowerCase() === key.toLocaleLowerCase()); + if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) return null; // Mandatory + else parsed[key] = prop.value; + } + + // TODO + // Other values based on endpoint protocol + + return parsed; + } + + public static parseInterfaceMetaData(interfaceMeta: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceMetadata | null { + if (!interfaceMeta.value || interfaceMeta.idShort?.toLocaleLowerCase() !== "interfacemetadata") return null; + + const parsed = { + properties: [], + actions: [], + events: [] + } as InterfaceMetadata; + + const prop = AASStore.findElement(interfaceMeta, element => (element as any).idShort.toLocaleLowerCase() === "properties"); + if (prop === null || !aasCore.types.isSubmodelElementList(prop) || !prop.value) return null; // Mandatory + for (const element of prop.value) { + if (!aasCore.types.isSubmodelElementCollection(element)) continue; + const parsedProp = AIDParser.parseInterfaceMetadataProperty(element, endpointProtocol); + if (parsedProp === null) continue; + parsed.properties.push(parsedProp); + } + + const actions = AASStore.findElement(interfaceMeta, element => (element as any).idShort.toLocaleLowerCase() === "actions"); + if (actions === null || !aasCore.types.isSubmodelElementList(actions) || !actions.value) return null; // Mandatory + for (const element of actions.value) { + if (!aasCore.types.isSubmodelElementCollection(element)) continue; + const parsedAction = AIDParser.parseInterfaceMetadataProperty(element, endpointProtocol); + if (parsedAction === null) continue; + parsed.actions.push(parsedAction); + } + + return parsed as InterfaceMetadata; + } + + public static parseInterfaceMetadataProperty(prop: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceProperty | null { + if (!prop.value || prop.idShort?.toLocaleLowerCase() !== "properties") return null; + + const parsed = {} as InterfaceProperty; + + // This does not matter in any way for the interface + /* for (const key of ["title", "observeable", "type"]) { + const prop = AASStore.findElement(prop, element => (element as any).idShort.toLocaleLowerCase() === key.toLocaleLowerCase()); + if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) continue; // Not mandatory + else parsed[key] = prop.value; + } */ + + const forms = AASStore.findElement(prop, element => (element as any).idShort.toLocaleLowerCase() === "forms"); + if (forms === null || !aasCore.types.isSubmodelElementList(forms) || !forms.value) return null; // Mandatory + for (const element of forms.value) { + if (!aasCore.types.isSubmodelElementCollection(element)) continue; + const parsedForm = AIDParser.parseInterfaceMetadataPropertyForm(element, endpointProtocol); + if (parsedForm === null) continue; + parsed.forms.push(parsedForm); + } + + // TODO + // Other values based on endpoint protocol + + return parsed; + } + + private static parseInterfaceMetadataPropertyForm(form: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfacePropertyForm | null { + if (!form.value) return null; + + const parsed = {} as any; + + for (const key of ["href", "contentType"]) { + const prop = AASStore.findElement(form, element => (element as any).idShort.toLocaleLowerCase() === key.toLocaleLowerCase()); + if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) return null; // Mandatory + else parsed[key] = prop.value; + } + + // TODO + // Other values based on endpoint protocol + + return parsed as InterfacePropertyForm; + } + + public static parseInterfaceMetadataAction(action: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceAction | null { + // TODO + return null; + } + + public static parseInterfaceMetadataEvent(event: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceAction | null { + // TODO + return null; + } +} \ No newline at end of file diff --git a/lib/src/parser/AIMCMapper.ts b/lib/src/parser/AIMCMapper.ts new file mode 100644 index 0000000..2f3b4b9 --- /dev/null +++ b/lib/src/parser/AIMCMapper.ts @@ -0,0 +1,3 @@ +export class AIMCMapper { + +} \ No newline at end of file diff --git a/lib/src/types/aidConf.ts b/lib/src/types/aidConf.ts new file mode 100644 index 0000000..a7a61ea --- /dev/null +++ b/lib/src/types/aidConf.ts @@ -0,0 +1,86 @@ +import { AvailableEndpoint } from "./common"; + +export type AssetInterfaceDescription = { + [key in AvailableEndpoint]: InterfaceDescription[]; +}; + +export type InterfaceDescription = { + title: string; + //created?: Date; + //modified?: Date; + //support?: string; + //externalDescriptor: never; // We don't use these (atm) + EndPointMetadata: EndpointMetadata; + InterfaceMetadata: InterfaceMetadata; +} + +export type EndpointMetadata = { + base: string; // This is the connection base string + contentType: string; + //securityDefinition?: any; // Will currently not be used + [key: string]: any; // This is for the additional metadata +} + +export type InterfaceMetadata = { + properties: InterfaceProperty[]; + actions: InterfaceAction[]; + events: InterfaceEvent[]; +} + +export type InterfaceProperty = { + title?: string; + observable?: boolean; + forms: InterfacePropertyForm[]; + type?: string; + enum?: string[]; + const?: any; + default?: any; + //unit?: string; + minimum?: number; // TODO muss das gecapped werden? + maximum?: number; + minLength?: number; + maxLength?: number; + //items?: any[]; + minItems?: number; + maxItems?: number; +} + +export type InterfacePropertyForm = InterfacePropertyFormHTTP | InterfacePropertyFormMQTT | InterfacePropertyFormModbus; + +type InterfacePropertyFormBase = { + href: string; + contentType: string; +} + +type InterfacePropertyFormHTTP = InterfacePropertyFormBase & { + methodName: string; + headers?: Record; +} + +type InterfacePropertyFormModbus = InterfacePropertyFormBase & { + function: "readCoil" | + "readDeviceIdentification" | + "readDiscreteInput" | + "readHoldingRegisters" | + "readInputRegisters" | + "writeMultipleCoils" | + "writeMultipleHoldingRegisters" | + "writeSingleCoil" | + "writeSingleHoldingRegister"; + entity?: "Coil" | "DiscreteInput" | "HoldingRegister" | "InputRegister"; + zeroBasedAddressing?: boolean; +} + +type InterfacePropertyFormMQTT = InterfacePropertyFormBase & { + retain?: boolean; + controlPacket: "publish" | "subscribe" | "unsubscribe"; + qos?: 0 | 1 | 2; +} + +export type InterfaceAction = { + // TODO +} + +export type InterfaceEvent = { + // TODO +} \ No newline at end of file diff --git a/lib/src/types/aimcConf.ts b/lib/src/types/aimcConf.ts new file mode 100644 index 0000000..f081bff --- /dev/null +++ b/lib/src/types/aimcConf.ts @@ -0,0 +1,14 @@ +import type { types } from "@aas-core-works/aas-core3.0-typescript"; +import { AvailableEndpoint } from "./common"; + +export type AssetInterfaceMappingConfiguration = Record; + +type MappingConfEntry = { + EndpointMetaDataReference: types.ReferenceElement; + MappingConfiguration: MappingConfiguration; +} + +type MappingConfiguration = { + InterfaceMetaDataReference: types.ReferenceElement; + SourceSinkMappings: types.RelationshipElement[]; +} \ No newline at end of file diff --git a/lib/src/types/common.ts b/lib/src/types/common.ts new file mode 100644 index 0000000..1494d78 --- /dev/null +++ b/lib/src/types/common.ts @@ -0,0 +1,8 @@ +export const availableEndpoints = ["HTTP", "MQTT", "Modbus"] as const; +export type AvailableEndpoint = typeof availableEndpoints[number]; + +export function endpointAvailable(endpoint: string): AvailableEndpoint | null { + const idx = availableEndpoints.map(e => e.toLocaleLowerCase()).findIndex(e => e === endpoint.toLocaleLowerCase()); + if (idx === -1) return null; + return availableEndpoints[idx]; +} \ No newline at end of file