patch: Azure function to serve device model function (#245)

* try to build that function

* skip lib check

* fix proxy settings

* more scafoling

* npm install

* add model generation

* patch add sdmi support to CLI

* updated parsing code

* azure goo

* normalize parse

* updated path

* fix more issues

* another fix
This commit is contained in:
Peli de Halleux 2021-08-25 14:59:50 -07:00 коммит произвёл GitHub
Родитель 14d78e1c40
Коммит 046cf22581
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 444 добавлений и 11 удалений

1
.vscode/extensions.json поставляемый
Просмотреть файл

@ -1,5 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"ms-azuretools.vscode-azurefunctions",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"silvenon.mdx" "silvenon.mdx"

33
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -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": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/jacdac-cli.js",
"args": ["--sdmi=dtmi/jacdac/devices/x1473a263/x12fc91032-1.json"]
},
{
"type": "pwa-node",
"request": "launch",
"name": "cli usb",
"skipFiles": ["<node_internals>/**"],
"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"
}
]
}

7
.vscode/settings.json поставляемый
Просмотреть файл

@ -10,5 +10,10 @@
}, },
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features" "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",
} }

43
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -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"
}
}
]
}

Просмотреть файл

@ -0,0 +1,7 @@
*.js.map
*.ts
.git*
.vscode
local.settings.json
test
tsconfig.json

94
device-models-function/.gitignore поставляемый Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -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"
}

Просмотреть файл

@ -0,0 +1,16 @@
import { AzureFunction, Context } from "@azure/functions"
import { routeToDTDL } from "jacdac-ts"
const httpTrigger: AzureFunction = async function (
context: Context
): Promise<void> {
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

Просмотреть файл

@ -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)"
}
}

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -0,0 +1,5 @@
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
}
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "dist",
"rootDir": ".",
"sourceMap": true,
"strict": false,
"skipLibCheck": true
}
}

Просмотреть файл

@ -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==

@ -1 +1 @@
Subproject commit 794589fb18ccb016e163510026284e4925339b40 Subproject commit 92a61228187b225e4dcd5df125ae8d14be8dc870

Просмотреть файл

@ -19,7 +19,7 @@ import {
serviceSpecificationFromClassIdentifier, serviceSpecificationFromClassIdentifier,
serviceSpecifications, serviceSpecifications,
} from "../jdom/spec" } from "../jdom/spec"
import { uniqueMap } from "../jdom/utils" import { arrayConcatMany, uniqueMap } from "../jdom/utils"
import { import {
arraySchema, arraySchema,
DTDLContent, DTDLContent,
@ -31,10 +31,14 @@ import {
objectSchema, objectSchema,
} from "./dtdl" } 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 // 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}$ // ^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) { export function toDTMI(segments: (string | number)[], version?: number) {
return `dtmi:jacdac:${[...segments] return `dtmi:${DTDL_JACDAC_PATH}:${[...segments]
.map(seg => .map(seg =>
seg === undefined seg === undefined
? "???" ? "???"
@ -391,11 +395,11 @@ export function serviceSpecificationDTMI(
srv: jdspec.ServiceSpec, srv: jdspec.ServiceSpec,
customPath?: string customPath?: string
) { ) {
return toDTMI([customPath || "services", srv.classIdentifier]) return toDTMI([customPath || DTDL_SERVICES_PATH, srv.classIdentifier])
} }
export function deviceSpecificationDTMI(dev: jdspec.DeviceSpec) { 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) { export function DTMIToRoute(dtmi: string) {
@ -404,6 +408,75 @@ export function DTMIToRoute(dtmi: string) {
return route 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<string, (route: string) => 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( export function deviceSpecificationToDTDL(
dev: jdspec.DeviceSpec, dev: jdspec.DeviceSpec,
options?: DTDLGenerationOptions options?: DTDLGenerationOptions

Просмотреть файл

@ -11,6 +11,7 @@ import { createUSBTransport } from "../jdom/transport/usb"
import { createNodeUSBOptions } from "../jdom/transport/nodewebusb" import { createNodeUSBOptions } from "../jdom/transport/nodewebusb"
import { import {
deviceSpecificationToDTDL, deviceSpecificationToDTDL,
routeToDTDL,
serviceSpecificationsWithDTDL, serviceSpecificationsWithDTDL,
serviceSpecificationToDTDL, serviceSpecificationToDTDL,
} from "../azure-iot/dtdlspec" } from "../azure-iot/dtdlspec"
@ -27,6 +28,7 @@ interface OptionsType {
usb?: boolean usb?: boolean
packets?: boolean packets?: boolean
dtdl?: boolean dtdl?: boolean
sdmi?: string
devices?: string devices?: string
services?: string services?: string
rm?: boolean rm?: boolean
@ -37,12 +39,20 @@ const options: OptionsType = cli.parse({
usb: ["u", "listen to Jacdac over USB", true], usb: ["u", "listen to Jacdac over USB", true],
packets: ["p", "show/hide all packets", true], packets: ["p", "show/hide all packets", true],
dtdl: [false, "generate DTDL files", "file"], dtdl: [false, "generate DTDL files", "file"],
sdmi: [false, "generate dynamic DTDL files", "string"],
devices: ["d", "regular expression filter for devices", "string"], devices: ["d", "regular expression filter for devices", "string"],
services: [false, "regular expression filter for services", "string"], services: [false, "regular expression filter for services", "string"],
rm: [false, "delete files from output folder", true], rm: [false, "delete files from output folder", true],
parse: ["l", "parse logic analyzer log file", "string"], 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 // DTDL
if (options.dtdl) { if (options.dtdl) {
cli.info(`generating DTDL models`) cli.info(`generating DTDL models`)

Просмотреть файл

@ -1,2 +1,3 @@
export * from "./jdom/jacdac-jdom" export * from "./jdom/jacdac-jdom"
export * from "./servers/jacdac-servers" export * from "./servers/jacdac-servers"
export * from "./azure-iot/jacdac-azure-iot"

Просмотреть файл

@ -201,11 +201,7 @@ export function serviceSpecificationFromName(
export function serviceSpecificationFromClassIdentifier( export function serviceSpecificationFromClassIdentifier(
classIdentifier: number classIdentifier: number
): jdspec.ServiceSpec { ): jdspec.ServiceSpec {
if ( if (isNaN(classIdentifier))
classIdentifier === null ||
classIdentifier === undefined ||
isNaN(classIdentifier)
)
return undefined return undefined
return ( return (
_serviceSpecifications.find( _serviceSpecifications.find(