diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 13eb05ab1..1b194c937 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "ms-azuretools.vscode-azurefunctions", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "silvenon.mdx" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..fa1f35f35 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "pwa-node", + "name": "cli sdmi", + "request": "launch", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/jacdac-cli.js", + "args": ["--sdmi=dtmi/jacdac/devices/x1473a263/x12fc91032-1.json"] + }, + { + "type": "pwa-node", + "request": "launch", + "name": "cli usb", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/jacdac-cli.js", + "args": ["--usb=true"], + "outFiles": ["${workspaceFolder}/dist/*.js"] + }, + { + "name": "Attach to Node Functions", + "type": "node", + "request": "attach", + "port": 9229, + "preLaunchTask": "func: host start" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 3ded4eed0..8a65aa030 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,10 @@ }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" - } + }, + "azureFunctions.deploySubpath": "device-models-function", + "azureFunctions.postDeployTask": "npm install (functions)", + "azureFunctions.projectLanguage": "TypeScript", + "azureFunctions.projectRuntime": "~3", + "debug.internalConsoleOptions": "neverOpen", } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..e0c763b16 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,43 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "command": "host start", + "problemMatcher": "$func-node-watch", + "isBackground": true, + "dependsOn": "npm build (functions)", + "options": { + "cwd": "${workspaceFolder}/device-models-function" + } + }, + { + "type": "shell", + "label": "npm build (functions)", + "command": "npm run build", + "dependsOn": "npm install (functions)", + "problemMatcher": "$tsc", + "options": { + "cwd": "${workspaceFolder}/device-models-function" + } + }, + { + "type": "shell", + "label": "npm install (functions)", + "command": "yarn install --frozen-lockfile", + "options": { + "cwd": "${workspaceFolder}/device-models-function" + } + }, + { + "type": "shell", + "label": "npm prune (functions)", + "command": "npm prune --production", + "dependsOn": "npm build (functions)", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/device-models-function" + } + } + ] +} \ No newline at end of file diff --git a/device-models-function/.funcignore b/device-models-function/.funcignore new file mode 100644 index 000000000..517922249 --- /dev/null +++ b/device-models-function/.funcignore @@ -0,0 +1,7 @@ +*.js.map +*.ts +.git* +.vscode +local.settings.json +test +tsconfig.json \ No newline at end of file diff --git a/device-models-function/.gitignore b/device-models-function/.gitignore new file mode 100644 index 000000000..772851c9e --- /dev/null +++ b/device-models-function/.gitignore @@ -0,0 +1,94 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TypeScript output +dist +out + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json \ No newline at end of file diff --git a/device-models-function/dtmi/function.json b/device-models-function/dtmi/function.json new file mode 100644 index 000000000..0ef0d8109 --- /dev/null +++ b/device-models-function/dtmi/function.json @@ -0,0 +1,21 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "{*dtmi}", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/dtmi/index.js" +} diff --git a/device-models-function/dtmi/index.ts b/device-models-function/dtmi/index.ts new file mode 100644 index 000000000..8f74977d0 --- /dev/null +++ b/device-models-function/dtmi/index.ts @@ -0,0 +1,16 @@ +import { AzureFunction, Context } from "@azure/functions" +import { routeToDTDL } from "jacdac-ts" + +const httpTrigger: AzureFunction = async function ( + context: Context +): Promise { + const { bindingData } = context + const dtmi = bindingData.dtmi as string + const dtdl = routeToDTDL(dtmi) + context.res = { + status: dtdl ? 200 : 404, + body: dtdl ? JSON.stringify(dtdl) : undefined, + } +} + +export default httpTrigger diff --git a/device-models-function/host.json b/device-models-function/host.json new file mode 100644 index 000000000..b1756179f --- /dev/null +++ b/device-models-function/host.json @@ -0,0 +1,20 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "http": { + "routePrefix": "dtmi/jacdac" + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[2.*, 3.0.0)" + } +} diff --git a/device-models-function/package.json b/device-models-function/package.json new file mode 100644 index 000000000..f88e557d0 --- /dev/null +++ b/device-models-function/package.json @@ -0,0 +1,19 @@ +{ + "name": "device-models-function", + "version": "1.0.0", + "description": "", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "prestart": "npm run build", + "start": "func start", + "test": "echo \"No tests yet...\"" + }, + "dependencies": { + "jacdac-ts": "latest" + }, + "devDependencies": { + "@azure/functions": "^1.2.3", + "typescript": "^4.3.5" + } +} diff --git a/device-models-function/proxies.json b/device-models-function/proxies.json new file mode 100644 index 000000000..36dfb2c89 --- /dev/null +++ b/device-models-function/proxies.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json.schemastore.org/proxies", + "proxies": { + } +} diff --git a/device-models-function/tsconfig.json b/device-models-function/tsconfig.json new file mode 100644 index 000000000..e7462b19e --- /dev/null +++ b/device-models-function/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": ".", + "sourceMap": true, + "strict": false, + "skipLibCheck": true + } +} diff --git a/device-models-function/yarn.lock b/device-models-function/yarn.lock new file mode 100644 index 000000000..95962b907 --- /dev/null +++ b/device-models-function/yarn.lock @@ -0,0 +1,78 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@azure/functions@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@azure/functions/-/functions-1.2.3.tgz#65765837e7319eedffbf8a971cb2f78d4e043d54" + integrity sha512-dZITbYPNg6ay6ngcCOjRUh1wDhlFITS0zIkqplyH5KfKEAVPooaoaye5mUFnR+WP9WdGRjlNXyl/y2tgWKHcRg== + +"@types/node@^16.7.1": + version "16.7.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.1.tgz#c6b9198178da504dfca1fd0be9b2e1002f1586f0" + integrity sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A== + +"@types/w3c-web-serial@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/w3c-web-serial/-/w3c-web-serial-1.0.2.tgz#8bf21f90b40dda6d2e2e6b188417b6bd66525d03" + integrity sha512-Ftx4BtLxgAnel7V7GbHylCYjSq827A+jeEE3SnTS7huCGUN0pSwUn+CchTCT9TkZj9w+NVMUq4Bk2R0GvUNmAQ== + +"@types/w3c-web-usb@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz#90284d17f35de981670c85d29053ae8b88fa5543" + integrity sha512-dYolx2XWesl1TMu+1BjtjU6eC6c2zZ2VDKhjU4f/mtR3+UBfMW6h1tPCQt7leY5Y8JBg0Fe/mMnoDMkPPNX9sw== + +"@types/web-bluetooth@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.11.tgz#2dc7ae7a809b70723e064b58245103028d5ea616" + integrity sha512-2CF3Kk2Rcvg/c2QzO7mXUhY7eL9CC3aKzrF+dNWNmp7Q8bmlvjmUM1nFPMSngawdJ+CcIdu8eJlQRytBgAZR9w== + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + +jacdac-ts@latest: + version "1.14.19" + resolved "https://registry.yarnpkg.com/jacdac-ts/-/jacdac-ts-1.14.19.tgz#34a593e6a09526bbb0ca0182b0b2f736684caef4" + integrity sha512-TUqQYkAyHpACceTQ2sNUe6z45LgDleo/zHt8KDK1hPd+lzL1+1BsGFSkOScUO/UFdRfsyG+4YhJq8HzvXV3peQ== + dependencies: + "@types/node" "^16.7.1" + "@types/w3c-web-serial" "^1.0.2" + "@types/w3c-web-usb" "^1.0.5" + "@types/web-bluetooth" "^0.0.11" + fs-extra "^10.0.0" + regenerator-runtime "^0.13.9" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +regenerator-runtime@^0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +typescript@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== diff --git a/jacdac-spec b/jacdac-spec index 794589fb1..92a612281 160000 --- a/jacdac-spec +++ b/jacdac-spec @@ -1 +1 @@ -Subproject commit 794589fb18ccb016e163510026284e4925339b40 +Subproject commit 92a61228187b225e4dcd5df125ae8d14be8dc870 diff --git a/src/azure-iot/dtdlspec.ts b/src/azure-iot/dtdlspec.ts index a7c6125a6..98f2c4630 100644 --- a/src/azure-iot/dtdlspec.ts +++ b/src/azure-iot/dtdlspec.ts @@ -19,7 +19,7 @@ import { serviceSpecificationFromClassIdentifier, serviceSpecifications, } from "../jdom/spec" -import { uniqueMap } from "../jdom/utils" +import { arrayConcatMany, uniqueMap } from "../jdom/utils" import { arraySchema, DTDLContent, @@ -31,10 +31,14 @@ import { objectSchema, } from "./dtdl" +export const DTDL_JACDAC_PATH = "jacdac" +export const DTDL_SERVICES_PATH = "services" +export const DTDL_DEVICES_PATH = "devices" + // https://github.com/Azure/digital-twin-model-identifier // ^dtmi:(?:_+[A-Za-z0-9]|[A-Za-z])(?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::(?:_+[A-Za-z0-9]|[A-Za-z])(?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$ export function toDTMI(segments: (string | number)[], version?: number) { - return `dtmi:jacdac:${[...segments] + return `dtmi:${DTDL_JACDAC_PATH}:${[...segments] .map(seg => seg === undefined ? "???" @@ -391,11 +395,11 @@ export function serviceSpecificationDTMI( srv: jdspec.ServiceSpec, customPath?: string ) { - return toDTMI([customPath || "services", srv.classIdentifier]) + return toDTMI([customPath || DTDL_SERVICES_PATH, srv.classIdentifier]) } export function deviceSpecificationDTMI(dev: jdspec.DeviceSpec) { - return toDTMI(["devices", dev.id.replace(/-/g, ":")]) + return toDTMI([DTDL_DEVICES_PATH, dev.id.replace(/-/g, ":")]) } export function DTMIToRoute(dtmi: string) { @@ -404,6 +408,75 @@ export function DTMIToRoute(dtmi: string) { return route } +function parseRoute(route: string, normalize?: boolean) { + const [, path, version] = /(.*)-(\d+)\.json$/.exec(route) + const parts = path.split("/") + if (normalize) + while (parts[0] === "dtmi" || parts[0] === DTDL_JACDAC_PATH) + parts.shift() + return { version, parts } +} + +export function routeToDTMI(route: string) { + const { parts, version } = parseRoute(route) + if (parts[0] !== "dtmi") parts.unshift("dtmi") + if (parts[1] !== DTDL_JACDAC_PATH) parts.splice(1, 0, DTDL_JACDAC_PATH) + return `${parts.join(":")}-${version}` +} + +export function serviceRouteToDTDL(route: string) { + const { parts } = parseRoute(route, true) + if (parts[0] !== DTDL_SERVICES_PATH) throw Error("invalid route") + const serviceClass = parseInt("0" + parts[1], 16) + const specification = serviceSpecificationFromClassIdentifier(serviceClass) + const dtdl = serviceSpecificationToDTDL(specification) + return dtdl +} + +export function encodedDeviceRouteToDTDL(route: string) { + const { parts } = parseRoute(route, true) + if (parts[0] !== DTDL_DEVICES_PATH) throw Error("invalid route") + const services = parts.slice(1).map(part => { + const m = /^x(\w{8,8})(\d*)$/.exec(part) + return { + service: serviceSpecificationFromClassIdentifier( + parseInt(m[1], 16) + ), + occurance: m[2] ? parseInt(m[2]) : 1, + } + }) + const dtdl: DTDLInterface = { + "@type": "Interface", + "@id": routeToDTMI(route), + displayName: route, + contents: arrayConcatMany( + services.map(({ occurance, service }) => + Array(occurance) + .fill(0) + .map((_, i) => + serviceSpecificationToComponent( + service, + `${service.shortName}${i}` + ) + ) + ) + ), + "@context": DTDL_CONTEXT, + } + return dtdl +} + +const routes: Record DTDLContent> = { + services: serviceRouteToDTDL, + devices: encodedDeviceRouteToDTDL, +} +export function routeToDTDL(route: string) { + const { parts } = parseRoute(route, true) + const path = parts[0] + const handler = routes[path] + return handler?.(route) +} + export function deviceSpecificationToDTDL( dev: jdspec.DeviceSpec, options?: DTDLGenerationOptions diff --git a/src/cli/jacdac-cli.ts b/src/cli/jacdac-cli.ts index 7042fe070..cca8236e9 100644 --- a/src/cli/jacdac-cli.ts +++ b/src/cli/jacdac-cli.ts @@ -11,6 +11,7 @@ import { createUSBTransport } from "../jdom/transport/usb" import { createNodeUSBOptions } from "../jdom/transport/nodewebusb" import { deviceSpecificationToDTDL, + routeToDTDL, serviceSpecificationsWithDTDL, serviceSpecificationToDTDL, } from "../azure-iot/dtdlspec" @@ -27,6 +28,7 @@ interface OptionsType { usb?: boolean packets?: boolean dtdl?: boolean + sdmi?: string devices?: string services?: string rm?: boolean @@ -37,12 +39,20 @@ const options: OptionsType = cli.parse({ usb: ["u", "listen to Jacdac over USB", true], packets: ["p", "show/hide all packets", true], dtdl: [false, "generate DTDL files", "file"], + sdmi: [false, "generate dynamic DTDL files", "string"], devices: ["d", "regular expression filter for devices", "string"], services: [false, "regular expression filter for services", "string"], rm: [false, "delete files from output folder", true], parse: ["l", "parse logic analyzer log file", "string"], }) +// SDMI +if (options.sdmi) { + console.log(`sdmi: generate DTDL for ${options.sdmi}`) + const dtdl = routeToDTDL(options.sdmi) + console.log(dtdl) +} + // DTDL if (options.dtdl) { cli.info(`generating DTDL models`) diff --git a/src/jacdac.ts b/src/jacdac.ts index 927dc1c8a..25a348eee 100644 --- a/src/jacdac.ts +++ b/src/jacdac.ts @@ -1,2 +1,3 @@ export * from "./jdom/jacdac-jdom" export * from "./servers/jacdac-servers" +export * from "./azure-iot/jacdac-azure-iot" \ No newline at end of file diff --git a/src/jdom/spec.ts b/src/jdom/spec.ts index 012014edc..69a05fbb6 100644 --- a/src/jdom/spec.ts +++ b/src/jdom/spec.ts @@ -201,11 +201,7 @@ export function serviceSpecificationFromName( export function serviceSpecificationFromClassIdentifier( classIdentifier: number ): jdspec.ServiceSpec { - if ( - classIdentifier === null || - classIdentifier === undefined || - isNaN(classIdentifier) - ) + if (isNaN(classIdentifier)) return undefined return ( _serviceSpecifications.find(