diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..f0e4e47 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[env] +DEFAULT_INTENT_BROKER_URL = "http://0.0.0.0:4243" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a54d98e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.pb filter=lfs diff=lfs merge=lfs -text +intent_brokering/dogmode/applications/local-object-detection/models/ssd_mobilenet_v2_coco.pb filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml new file mode 100644 index 0000000..f7d7ac3 --- /dev/null +++ b/.github/workflows/devskim.yml @@ -0,0 +1,37 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: DevSkim + +on: + workflow_dispatch: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '17 19 * * 4' + +jobs: + lint: + name: DevSkim + runs-on: ubuntu-20.04 + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run DevSkim scanner + uses: microsoft/DevSkim-Action@v1 + with: + ignore-globs: "*.json" + + - name: Upload DevSkim scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: devskim-results.sarif diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml new file mode 100644 index 0000000..1989138 --- /dev/null +++ b/.github/workflows/dotnet-ci.yml @@ -0,0 +1,33 @@ +name: .NET CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths: + - 'intent_brokering/dogmode/applications/dog-mode-ui/**' + - '.github/workflows/dotnet-ci.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.403 + global-json-file: intent_brokering/dogmode/applications/dog-mode-ui/global.json + + - run: dotnet --info + + - run: dotnet build intent_brokering/dogmode/applications/dog-mode-ui diff --git a/.github/workflows/editorconfig-audit-ci.yml b/.github/workflows/editorconfig-audit-ci.yml new file mode 100644 index 0000000..7154e1c --- /dev/null +++ b/.github/workflows/editorconfig-audit-ci.yml @@ -0,0 +1,27 @@ +name: EditorConfig audit + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + editorconfig-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - run: npm install eclint + - run: | + # Unfortunately, if the "".editorconfig" is present then "eclint" + # checks for full conformance either irrespective or on top of the + # check options you given the command-line. By removing the file, only + # the given options are checked. + git ls-files | grep -iF .editorconfig | xargs -t -n 1 rm + env node_modules/.bin/eclint check --insert_final_newline --trim_trailing_whitespace $(git ls-files | grep -viF .editorconfig | grep -viE "\.pb$") diff --git a/.github/workflows/markdown-ci.yml b/.github/workflows/markdown-ci.yml index 4aea16f..52c3bfb 100644 --- a/.github/workflows/markdown-ci.yml +++ b/.github/workflows/markdown-ci.yml @@ -8,6 +8,8 @@ on: - 'docs/**' - '**.md' - '**.markdown' + - 'tools/.markdownlint.jsonc' + - 'tools/.markdownlinkcheck.json' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -30,4 +32,4 @@ jobs: - uses: actions/checkout@v2 - run: | npm install markdown-link-check - find . -type d \( -name node_modules -o -name .github \) -prune -o -type f -name '*.md' -print0 | xargs -0 -n1 node_modules/.bin/markdown-link-check --config ./tools/.markdownlinkcheck.jsonc --quiet + find . -type d \( -name node_modules -o -name .github \) -prune -o -type f -name '*.md' -print0 | xargs -0 -n1 node_modules/.bin/markdown-link-check --config ./tools/.markdownlinkcheck.json --quiet diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..fc20e4f --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,84 @@ +name: Rust CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths: + - 'intent_brokering/dogmode/common/**' + - 'intent_brokering/dogmode/applications/**' + - 'Cargo.lock' + - 'Cargo.toml' + - 'rust-toolchain.toml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + CARGO_TERM_COLOR: always + +jobs: + static_code_analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install protobuf-compiler + run: sudo apt-get install -y protobuf-compiler + - name: Install stable toolchain + run: | + rustup show + rustup component add rustfmt clippy + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1 + - run: cargo check --workspace --locked + - run: cargo clippy --all-targets --all-features --workspace --no-deps -- -D warnings + - run: cargo fmt --all -- --check + - name: Run doctest only + # we run doctests here as cargo tarpaulin (our test runner) + # requires nightly toolchain to do so + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --doc + - name: Run cargo doc + # This step is required to detect possible errors in docs that are not doctests. + uses: actions-rs/cargo@v1 + with: + command: doc + args: --workspace --no-deps # Warnings are treated as errors due to our .cargo/config file. + + + build_and_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install protobuf-compiler + run: sudo apt-get install -y protobuf-compiler + - name: Install stable toolchain + run: rustup show + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1 + - name: Run cargo-tarpaulin + uses: actions-rs/tarpaulin@v0.1 + with: + version: '0.21.0' + args: '--workspace --ignore-tests --skip-clean --exclude-files spikes/* --exclude-files examples/* --exclude-files tests/*' + + buf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: bufbuild/buf-setup-action@v1 + with: + version: '1.8.0' + - uses: bufbuild/buf-lint-action@v1 diff --git a/.github/workflows/security-audit.yaml b/.github/workflows/security-audit.yaml new file mode 100644 index 0000000..e47ed8f --- /dev/null +++ b/.github/workflows/security-audit.yaml @@ -0,0 +1,23 @@ +name: Security Audit +on: + pull_request: + branches: + - main + paths: + - "**/Cargo.toml" + - "**/Cargo.lock" + - ".github/workflows/security-audit.yaml" + schedule: + - cron: "0 0 * * *" # once a day at midnight UTC + # NB: that cron trigger on GH actions runs only on the default branch + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + security_audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: cargo audit -D warnings diff --git a/.gitignore b/.gitignore index 356b850..413225e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ bld/ **/*.rs.bk # Stops pushes of local vscode files. -/.vscode/* \ No newline at end of file +/.vscode/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..938ef57 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/chariott"] + path = external/chariott + url = https://github.com/eclipse-chariott/chariott diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bc39d66 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3376 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.3", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.3.0", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.2.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.1.1", + "async-executor", + "async-io 2.3.1", + "async-lock 3.3.0", + "blocking", + "futures-lite 2.2.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.2.0", + "parking", + "polling 3.3.2", + "rustix 0.38.30", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-task" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.1.1", + "async-lock 3.3.0", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.2.0", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cloud-object-detection-app" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64", + "examples-common", + "futures", + "intent_brokering_common", + "intent_brokering_proto", + "lazy_static", + "reqwest", + "serde", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tonic-build", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "futures", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.4.10", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.70+curl-8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0333d8849afe78a4c8102a429a446bfdd055832af071945520e835ae2d841e" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.48.0", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dog-mode-logic-app" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "examples-common", + "futures", + "intent_brokering_common", + "intent_brokering_proto", + "prost", + "prost-types", + "serde", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "ess" +version = "0.1.0" +dependencies = [ + "criterion", + "futures", + "intent_brokering_common", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "examples-common" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "ess", + "futures", + "intent_brokering_common", + "intent_brokering_proto", + "keyvalue", + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "exr" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279d3efcc55e19917fff7ab3ddd6c14afb6a90881a0078465196fe2f99d08c56" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "intent_brokering_common" +version = "0.1.0" +dependencies = [ + "async-trait", + "ess", + "intent_brokering_proto", + "regex", + "serde", + "tempfile", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tracing", + "uuid", +] + +[[package]] +name = "intent_brokering_proto" +version = "0.1.0" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-build", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyvalue" +version = "0.1.0" + +[[package]] +name = "kv-app" +version = "0.1.0" +dependencies = [ + "async-trait", + "ess", + "examples-common", + "intent_brokering_common", + "intent_brokering_proto", + "keyvalue", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libz-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "local-object-detection-app" +version = "0.1.0" +dependencies = [ + "async-trait", + "examples-common", + "image", + "intent_brokering_common", + "intent_brokering_proto", + "lazy_static", + "ndarray", + "serde", + "serde_json", + "tensorflow", + "tokio", + "tokio-util", + "tonic", + "tonic-build", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mock-vas" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "async-trait", + "env_logger", + "ess", + "examples-common", + "futures", + "intent_brokering_common", + "intent_brokering_proto", + "lazy_static", + "regex", + "test-log", + "tokio", + "tokio-stream", + "tokio-test", + "tokio-util", + "tonic", + "tonic-build", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "openssl" +version = "0.10.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.2.1", +] + +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.30", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.48", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +dependencies = [ + "bytes", + "heck", + "itertools 0.11.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.48", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simulated-camera-app" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "async-stream", + "async-trait", + "ess", + "examples-common", + "futures", + "intent_brokering_common", + "intent_brokering_proto", + "lazy_static", + "regex", + "serde", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + +[[package]] +name = "tensorflow" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a5aa784c60e18bcca7f79fde117bd7209a30247d3242e9127a7d9496e66272" +dependencies = [ + "byteorder", + "crc", + "half", + "libc", + "num-complex", + "protobuf", + "rustversion", + "tensorflow-internal-macros", + "tensorflow-sys", +] + +[[package]] +name = "tensorflow-internal-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4975c975b6d9c05a7cbf007ebc8e01e92e7e0907be9efcc74074857fc92ceb54" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tensorflow-sys" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24fa07ba31c6802a42d8fcb4ed2c400091d1a3da4531c1dea2c162b4b650505c" +dependencies = [ + "curl", + "flate2", + "libc", + "pkg-config", + "semver", + "tar", + "zip", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-log" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6159ab4116165c99fc88cce31f99fa2c9dbe08d3691cb38da02fc3b45f357d2b" +dependencies = [ + "env_logger", + "test-log-macros", +] + +[[package]] +name = "test-log-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba277e77219e9eea169e8508942db1bf5d8a41ff2db9b20aab5a5aadc9fa25d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b3cbabd3ae862100094ae433e1def582cf86451b4e9bf83aa7ac1d8a7d719" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "web-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.30", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.13", + "rustix 0.38.30", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..11216cb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +[workspace] +members = [ + "intent_brokering/dogmode/common", + "intent_brokering/dogmode/applications/cloud-object-detection", + "intent_brokering/dogmode/applications/dog-mode-logic", + "intent_brokering/dogmode/applications/kv-app", + "intent_brokering/dogmode/applications/local-object-detection", + "intent_brokering/dogmode/applications/mock-vas", + "intent_brokering/dogmode/applications/simulated-camera", +] + +[workspace.dependencies] +anyhow = "1.0" +async-trait = "0.1" +intent_brokering_common = { path = "external/chariott/intent_brokering/common" } +intent_brokering_proto = { path = "external/chariott/intent_brokering/proto.rs" } +futures = { version = "0.3" } +lazy_static = "1.4.0" +parking_lot = "0.12.1" +prost = "0.12" +prost-types = "0.12" +regex = "1.10" +serde = "1.0.195" +serde_json = "1.0.111" +tokio = { version = "1.35", features = ["macros"] } +tokio-util = "0.7" +tokio-stream = { version = "0.1", features = ["net"] } +tonic = "0.10" +tonic-build = "0.10" +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.7.0", features = ["v4"] } +url = "2.5" +test-case = "2.2.2" + +[profile.release] +strip = true +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" diff --git a/README.md b/README.md index 4654662..0536b23 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ - [Introduction](#introduction) - [Examples](#examples) - [Dog mode](#dog-mode) +- [Getting Started](#getting-started) ## Introduction @@ -12,11 +13,87 @@ components like [Eclipse Agemo](https://github.com/eclipse-chariott/Agemo), [Eclipse Freyja](https://github.com/eclipse-ibeji/freyja). It also serves as a place to incubate early ideas for an eventual blueprint. +## Getting Started + +### Prerequisites + +This guide uses `apt` as the package manager in the examples. You may need to substitute your own +package manager in place of `apt` when going through these steps. + +1. Install gcc: + + ```shell + sudo apt install gcc + ``` + + > **NOTE**: Rust needs gcc's linker. + +1. Install git and git-lfs + + ```shell + sudo apt install -y git git-lfs + git lfs install + ``` + +1. Install [rust](https://rustup.rs/#), using the default installation, for example: + + ```shell + sudo apt install curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + + You will need to restart your shell to refresh the environment variables. + + > **NOTE**: The rust toolchain version is managed by the rust-toolchain.toml file, so once you + install rustup there is no need to manually install a toolchain or set a default. + +1. Install OpenSSL: + + ```shell + sudo apt install pkg-config + sudo apt install libssl-dev + ``` + +1. Install Protobuf Compiler: + + ```shell + sudo apt install -y protobuf-compiler + ``` + + > **NOTE**: The protobuf compiler is needed for building the project. + +### Cloning the Repo + +The repo has a submodule [chariott](https://github.com/eclipse-chariott/chariott) that provides +proto files for Chariott Intent Brokering integration. To ensure that these files are included, +please use the following command when cloning the repo: + +```shell +git clone --recurse-submodules https://github.com/eclipse-chariott/chariott-example-applications +``` + +### Building + +Run the following to build everything in the workspace once you have installed the prerequisites: + +```shell +cargo build --workspace +``` + +### Running the Tests + +After successfully building the service, you can run all of the unit tests. To do this go to the +enlistment's root directory and run: + +```shell +cargo test +``` + ## Examples ### Dog mode -The [dog mode](./dogmode/README.md) allows a car owner to keep their dog safe while they are away +The [dog mode](./intent_brokering/dogmode/README.md) allows a car owner to keep their dog safe while they are away from the car. If the ambient temperature is high, multiple different applications will interact with each other to ensure that the temperature inside the car is at a safe level for the dog. This example currently uses Eclipse Chariott and is in design and development to integrate with other diff --git a/buf.work.yaml b/buf.work.yaml new file mode 100644 index 0000000..9c2d370 --- /dev/null +++ b/buf.work.yaml @@ -0,0 +1,4 @@ +version: v1 +directories: + - external/chariott/intent_brokering/proto + - intent_brokering/dogmode/applications/proto diff --git a/dogmode/README.md b/dogmode/README.md deleted file mode 100644 index 2032284..0000000 --- a/dogmode/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Dogmode Example - -- [Introduction](#introduction) - -## Introduction - -The dog mode example will be migrated to this repository component-by-component. The description -below represents the overall example when it is finished. - -The dog mode allows a car owner to keep their dog safe while they are away from the car. If the -ambient temperature is high, multiple different applications will interact with each other to -ensure that the temperature inside the car is at a safe level for the dog. This works as follows: -first, the dog mode logic application detects whether a dog is present, either by automatically -connecting a camera with object detection, or through user interaction in the UI application. If a -dog is detected, it will monitor various vehicle hardware properties through the Vehicle -Abstraction Service (VAS). Based on certain conditions, actions are taken. For example, if the -battery is low, the owner is notified to return to the car immediately. If the temperature rises, -the air conditioning is turned on. diff --git a/external/chariott b/external/chariott new file mode 160000 index 0000000..021ec1d --- /dev/null +++ b/external/chariott @@ -0,0 +1 @@ +Subproject commit 021ec1d09da5d40ece3de911dcb0a86d4c5f9d55 diff --git a/intent_brokering/dogmode/README.md b/intent_brokering/dogmode/README.md new file mode 100644 index 0000000..a986557 --- /dev/null +++ b/intent_brokering/dogmode/README.md @@ -0,0 +1,442 @@ +# Example applications + +This directory contains a number of examples showcasing Chariott Intent Brokering. The example +applications are not intended for production use, but illustrate how to approach solving certain +problems when interacting with Chariott Intent Brokering. All example binaries contribute to a demo +scenario that we call _dog mode_. + +## Dog mode + +The dog mode allows a car owner to keep their dog safe while they are away from +the car. If the ambient temperature is high, different applications will +interact with each other to ensure that the temperature inside the car is at a +safe level for the dog. This works as follows: first, the dog mode logic +application (`intent_brokering/dogmode/applications/dog-mode-logic`) detects whether a dog is +present, either by automatically connecting a [camera][simulated camera] with +[object detection][local object detection], or through user interaction in the +[UI application][ui]. If a dog is detected, it will monitor various vehicle +hardware properties through the [mocked Vehicle Abstraction Service (VAS)][vas]. +Based on certain conditions, actions are taken. For example, if the battery is +low, the owner is notified to return to the car immediately. If the temperature +rises, the air conditioning is turned on. + +## Setup + +You will need the .NET SDK and ASP.NET Core Runtime version 6. As of the writing of this, +installing the .NET SDK on Ubuntu installs the SDK, runtime, and ASP.NET Core runtime. + +If you do not have these already, follow the instructions +[here](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-2004#add-the-microsoft-package-repository), +but replace the current version of the SDK with version 6 (dotnet-sdk-6.0). + +Once the update is done, run: + +```bash +dotnet --info +``` + +to ensure the installation was successful. At the end of the output message, you should see +something like the following. Ensure that they are major version 6, and that you have both the +SDK and ASP.NET Core runtime. + +```bash +.NET SDKs installed: + 6.0.412 [/usr/share/dotnet/sdk] + +.NET runtimes installed: + Microsoft.AspNetCore.App 6.0.20 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App] + Microsoft.NETCore.App 6.0.20 [/usr/share/dotnet/shared/Microsoft.NETCore.App] +``` + +## Running the example applications + +### Prerequisites + +In order to run an example applications with Chariott Intent Brokering, you must first build and +run the Intent Brokering service. Follow the instructions +[here](https://github.com/eclipse-chariott/chariott/tree/main/intent_brokering#getting-started) to +do this, and once you have the Intent Brokering service running, follow either +[automatic dog mode](#automatic-dog-mode), [manual dog mode](#manual-dog-mode), or start any of the +applications individually. + +### Automatic dog mode + +You can follow a simulation of the dog mode scenario by executing + +```bash +./intent_brokering/dogmode/applications/run_demo.sh --cognitive_endpoint "" --cognitive_key "" +``` + +from the repository root directory. + +> Note: The arguments for [Cognitive Services][cognitive services] can be left +> out and/or do not need to be valid to run the demo script. If erroneous +> arguments are detected, a [non-cloud based application][local object +> detection] is used for object detection. + +The script will run the following components: + +1. [Mock VAS (including simulated vehicle state changes)][vas] +1. [Key-Value Store][kv app] +1. [Simulated Camera Application][simulated camera] +1. [Local Object Detection Application][local object detection] +1. [Cloud Object Detection Application][cloud object detection] +1. Dog Mode Logic Application from `applications/dog-mode-logic` +1. [Dog Mode UI Application][ui] + +Once you have run the script, you can inspect the application logs that are written to +`target/logs`. This is helpful for troubleshooting if you experience unexpected behavior. + +After the UI is being served, you can access it under . +By default, the UI will also display the stream from the camera used to detect +whether a dog is present. You can access the camera stream at +. + +Note that the dog symbol in the UI will turn green once a dog is detected. This +happens without much delay if you configured [Azure Cognitive Services +correctly][cloud object detection], but it will take longer if the logic uses +the fallback local object detection. + +The simulation will oscillate the temperature between a lower and upper bound. +If the temperature is above a threshold and a dog is detected, you can see the +air conditioning being turned on. If the temperature falls, or the dog has left +the car, the air conditioning will be turned off. + +### Manual dog mode + +While the abovementioned flow runs continuously when you use the `run_demo.sh` +script, you can have more control over the applications by running (a subset of) +the components manually. Note the following behavior: + +- If the camera app is not running, you can turn the dog mode on or off via the + button in the UI. +- The mock VAS, which is responsible for simulating the car hardware, can be + driven via standard input to make changes to the cabin temperature, battery + level, etc. +- If you do not start, or misconfigure, the cloud object detection application, + the Intent Broker will automatically broker detection requests to the local detection + application. + +Refer to the documentation of each application to learn more about how to use +it. + +[local object detection]: ./applications/local-object-detection/README.md +[cloud object detection]: ./applications/cloud-object-detection/README.md +[simulated camera]: ./applications/simulated-camera/README.md +[ui]: ./applications/dog-mode-ui/README.md +[vas]: ./applications/mock-vas/README.md +[cognitive services]: https://docs.microsoft.com/en-us/azure/cognitive-services/what-are-cognitive-services +[kv app]: ./applications/kv-app/README.md + +## Flows + +### Provider registration + +To register a provider with the Intent Registry, the provider needs to implement the +`intent_brokering.provider.v1` protobuf interface. In addition it needs to register +itself using the `Announce` and `Register` method of the `intent_brokering.runtime.v1` interface. +These interfaces can be found +[here](https://github.com/eclipse-chariott/chariott/tree/main/intent_brokering/proto). + +This diagram shows the interaction between the provider and the Intent Brokering service during +the registration process: + +```mermaid +sequenceDiagram + autonumber + participant P as Provider + participant C as Intent Brokering service + + P ->> C: AnnounceRequest + C ->> P: AnnounceResponse: ANNOUNCED + P ->> C: RegisterRequest + loop Continious Announce + P ->> C: AnnounceRequest + C ->> P: AnnounceResponse: NOT_CHANGED + end +``` + +1. Provider starts up and announces itself to the Intent Brokering service. +2. The Intent Brokering service responds with `ANNOUNCED`. +3. Provider sends a `RegisterRequest` with all service details. +4. Provider continously sends an announce heartbeat to the Intent Brokering service. If the Intent Brokering service + crashed between two announcements, it will respond with `ANNOUNCED`, in which + case the provider should reregister using the `RegisterRequest`. + +See the [Simple Provider Application][simple-provider] for a self-contained example for how to +implement the above pattern. + +[simple-provider]: https://github.com/eclipse-chariott/chariott/blob/main/intent_brokering/examples/applications/simple-provider/README.md + +### Vehicle integration + +This scenario illustrates how you can integrate the vehicle hardware when using the Intent +Brokering service. We inspect the Vehicle Digital Twin for the presence of a property and +discover how to connect to it directly. + +Depends on: [provider registration](#provider-registration). + +#### Participants + +- DML - Dog mode logic application +- VAS - Vehicle abstraction service +- The Intent Brokering service - Application programming model + +```mermaid +sequenceDiagram + autonumber + participant D as DogMode + participant C as Intent Brokering service + participant V as VAS + + D ->> C: 'inspect' + Note over D,C: sdv.vdt, cabin.hvac.* + C ->> V: 'inspect' + Note over C,V: cabin.hvac.* + V ->> C: fulfillment + C ->> D: fulfillment + D ->> C: 'discover' + Note over D,C: sdv.vdt + C ->> V: 'discover' + V ->> C: service list + C ->> D: service list +``` + +1. `Inspect` is sent to the Intent Broker to the `sdv.vdt` namespace with a query for + `cabin.hvac.*`. +2. The `inspect` is forwarded the VAS that has registered for the `sdv.vdt` + namespace. +3. The fulfillment is routed through the Intent Broker. +4. The Intent Broker routes the fulfillment back to the application - the result is used + to make decisions about hardware availability and functionality that can be + enabled in the application. +5. The application sends a `discover` intent to the `sdv.vdt` namespace to find + an endpoint to communicate with the provider directly. +6. The Intent Broker forwards the `discover` intent to the VAS. +7. The VAS generates the `discover` response containing information about + endpoints that are exposed for direct consumption and returns it to the Intent Broker. +8. The Intent Registry returns the list to the application - the application then uses the + information to connect directly to the intent provider. + +### Streaming + +We establish a stream that contains property changes from the Vehicle Digital +Twin. This pattern can be used to connect to any streaming provider. + +Depends on: [provider registration](#provider-registration), [vehicle +integration](#vehicle-integration). + +```mermaid +sequenceDiagram + autonumber + participant D as DogMode + participant C as Intent Brokering service + participant V as VAS + + D ->> C: 'discover' + note over D,C: sdv.vdt + C ->> V: 'discover' + V ->> C: service list + C ->> D: service list + D ->> V: open channel + V ->> D: channel:x + D ->> C: 'subscribe' + note over D,C: sdv.vdt, channel:x + C ->> V: 'subscribe' + V ->> C: acknowledgement + C ->> D: acknowledgement + loop telemetry events + V ->> D: temperature + end +``` + +1. The application sends a `discover` request to the Intent Broker for the `sdv.vdt` + namespace. +2. The Intent Broker forwards the request to VAS, which was registered for the namespace. +3. VAS generates a service list including the streaming endpoint for event + notifications. +4. The service list is returned to the application. +5. The application uses the information from the service list to connect + directly to the streaming endpoint and open a gRPC streaming channel. +6. The endpoint responds with a channel id. +7. The application sends a `subscribe` intent to the `sdv.vdt` namespace + including the channel id that was established. +8. The `subscription` request is forwarded to the VAS including the target + channel id. +9. An acknowledgement is returned to the Intent Broker. +10. An acknowledgement is returned to the application. +11. The application receives update events for the subscribed telemetry. + +### System Inspect + +We inspect the Intent Registry to gather insights about the currently +supported intents and namespaces. This can be used to dynamically take +decisions, based on functionality currently present in the car. + +Depends on: [provider registration](#provider-registration). + +```mermaid +sequenceDiagram + autonumber + participant D as DogMode + participant C as Intent Brokering service + participant CA as camera + participant O as object detection + + CA ->> C: register + O ->> C: register + D ->> C: inspect + note over D,C: query: ** + C ->> D: Result + note over D,C: sdv.camera, sdv.objectdetection + D ->> D: conditionally change behavior +``` + +1. The camera application registers with the Intent Brokering service. +2. The object detection application registers with the Intent Brokering service. +3. The application sends an `inspect` intent to the Intent Broker for the + `system.registry` namespace. +4. The Intent Brokering service sends registration information back to the application. +5. Application conditionally adapts its functionality based on availability of + certain features. + +### App-to-App communication + +We communicate from the Dog Mode UI to the Dog Mode Logic Application through the Intent Brokering +service and a provider application. The UI will write state updates, for which the Dog Mode will +be notified via a stream of value changes. + +Depends on: [provider registration](#provider-registration), +[streaming](#streaming). + +```mermaid +sequenceDiagram + autonumber + participant D as DogMode + participant DUI as DogModeUI + participant C as Intent Brokering service + participant KV as KV-Store + + D --> KV: Establish streaming + note over D,KV: Subscriptions: sample.dogmode + DUI ->> C: write + note over DUI,C: sdv.kv, sample.dogmode:on + C ->> KV: write + KV ->> D: notification + note over KV,D: sample.dogmode:on +``` + +1. DogMode [establishes streaming](#streaming) for the key `sample.dogmode` with + the KV-Store. +2. The DogMode UI is sending a `write` intent to the Intent Broker for the + `sample.dogmode` key in the `sdv.kv` namespace. +3. The Intent Broker forwards the request to the KV-Store. +4. The KV-Store updates the value and checks its subscriptions. When it finds a + matching subscription, a notification is sent to the target channel. + +### Automatic dog detection + +We show how to combine the abovementioned patterns to combine a stream of images +to detect whether a dog is present in the car or not. Based on the presence or +absence of a dog, we change state in the Key-Value store, which can be observed +by any application that needs to take action based on the current state. + +Depends on: [provider registration](#provider-registration), +[streaming](#streaming), [app-to-app communication](#app-to-app-communication). + +```mermaid +sequenceDiagram + autonumber + participant D as DogMode + participant DUI as DogModeUI + participant C as Intent Brokering service + participant CA as camera + participant O as object detection + participant KV as KV-Store + + D --> KV: Establish streaming + note over D,KV: Subscriptions: sample.dogmode + CA ->> D: frame + D ->> C: invoke + note over D,C: sdv.objectdetection, blob[jpeg] + C ->> O: invoke + O ->> C: result + C ->> D: result + D ->> C: write + note over D,C: sdv.kv, dogmode=true + C ->> KV: write + KV ->> DUI: notification + note over KV,DUI: dogmode=true + DUI ->> DUI: update state + D ->> D: start dog mode logic +``` + +1. DogMode [establishes streaming](#streaming) for frames from the camera. +2. The Camera is sending frames to a subscribed consumer (DogMode). +3. The DogMode is invoking the `detect` command for the + `sdv.objectdetection` namespace passing in the frame from the camera. +4. The Intent Broker forwards the request to the matching provider. +5. The result is sent back to the Intent Broker. +6. The result is sent to the application. +7. After detecting a dog in the car, the application issues a `write` intent to + the Intent Broker for the `sdv.kv` namespace to update `dogmode=true`. +8. The Intent Broker forwards the request to the right provider. +9. If there was a subscription setup from the UI, the UI is notified about the + change of the `dogmode` state. +10. The state is updated in the UI and rendered. +11. The dog mode logic is executed. + +### Multi Provider + +Based on the scenario for [automatic dog detection](#automatic-dog-detection), +we show how to use the Intent Brokering service to dynamically switch over from a cloud-based +provider for object detection to a local provider for object detection. + +Depends on: [provider registration](#provider-registration), +[streaming](#streaming), [app-to-app communication](#app-to-app-communication), +[automatic dog detection](#automatic-dog-detection). + +```mermaid +sequenceDiagram + autonumber + participant D as DogMode + participant C as Intent Brokering service + participant CA as Camera + participant OBJ_L as Detection local + participant OBJ_C as Detection cloud + + OBJ_L ->> C: register + OBJ_C ->> C: register + D --> CA: Establish streaming + note over D,KV: Subscriptions: camera frames + loop + CA ->> D: Frames + end + D ->> C: invoke + note over D,C: sdv.objectdetection, frame + C ->>C: Connectivity? + alt Connected + C->>OBJ_C: invoke + OBJ_C ->> C: result + else is No connection + C->>OBJ_L: invoke + OBJ_L ->> C: result + end + C ->> D: result + D ->> D: adjusting logic +``` + +1. The local detection provider registers with the Intent Broker. +2. The cloud detection provider registers with the Intent Broker. +3. DogMode [establishes streaming](#streaming) for frames from the camera. +4. The camera starts sending frames over the channel. +5. The DogMode is invoking the `detect` command for the + `sdv.objectdetection` namespace passing in the frame from the camera. +6. The Intent Broker finds 2 matching provider, one for cloud. It validates + connectivity/bandwidth. +7. In case of connectivity, the request is sent to the cloud. +8. The response from the cloud is sent back to the Intent Broker. +9. Alternatively or for fallback, the local provider is invoked. +10. Local detection is performed and returned to the Intent Broker. +11. The Intent Broker returns a unified result from a single provider to the application. +12. Depending on the result, the execution is adjusted. diff --git a/intent_brokering/dogmode/applications/Dockerfile.base b/intent_brokering/dogmode/applications/Dockerfile.base new file mode 100644 index 0000000..d740b9d --- /dev/null +++ b/intent_brokering/dogmode/applications/Dockerfile.base @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +ARG RUST_VERSION=1.70 +FROM docker.io/library/rust:${RUST_VERSION} AS builder + +# Dockerfile for building Eclipse Chariott Intent Brokering base image +# +# This Dockerfile builds the packages from chariott workspace with +# statically linked dependencies (using musl) for x86_64 architecture + +# Examples user id +ARG SAMPLE_UID=10001 + +# User name +ENV USER_NAME=sample + +RUN apt update && apt upgrade -y +RUN apt install -y cmake protobuf-compiler musl-tools + +# compile openssl for musl +# from https://qiita.com/liubin/items/6c94f0b61f746c08b74c, +# but keeping references in case above side goes down +# https://github.com/sfackler/rust-openssl/issues/183 +# https://github.com/getsentry/rust-musl-cross +# https://github.com/rust-lang/cargo/issues/713#issuecomment-59597433 +# https://github.com/getsentry/rust-musl-cross/blob/master/Dockerfile + +RUN ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/x86_64-linux-musl/asm \ + && ln -s /usr/include/asm-generic /usr/include/x86_64-linux-musl/asm-generic \ + && ln -s /usr/include/linux /usr/include/x86_64-linux-musl/linux + +WORKDIR /musl +RUN wget https://www.openssl.org/source/openssl-1.1.1l.tar.gz \ + && tar -xzf openssl-1.1.1l.tar.gz \ + && cd openssl-1.1.1l \ + && CC="musl-gcc -fPIE -pie" ./Configure no-shared no-async linux-x86_64 --prefix=/musl --openssldir=/musl/ssl \ + && make depend \ + && make \ + && make install + +ENV PKG_CONFIG_ALLOW_CROSS 1 +ENV OPENSSL_STATIC true +ENV OPENSSL_DIR /musl + +WORKDIR /sdv + +COPY ./rust-toolchain.toml . + +RUN rustup target add x86_64-unknown-linux-musl + +COPY ./ . + +RUN cargo build --release --target=x86_64-unknown-linux-musl --workspace + +# unprivileged identity to run Chariott as +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${SAMPLE_UID}" \ + ${USER_NAME} diff --git a/intent_brokering/dogmode/applications/Dockerfile.cloud-object-detection b/intent_brokering/dogmode/applications/Dockerfile.cloud-object-detection new file mode 100644 index 0000000..a8a85c9 --- /dev/null +++ b/intent_brokering/dogmode/applications/Dockerfile.cloud-object-detection @@ -0,0 +1,24 @@ +##################################################################################################### +## Final image +#################################################################################################### +FROM intent_brokering_examples:base as base +FROM alpine:latest +ARG APP_NAME +ENV USER_NAME=sample + +# Import Chariott user and group from base. +COPY --from=base /etc/passwd /etc/passwd +COPY --from=base /etc/group /etc/group + +WORKDIR /sdv +RUN apk update && apk add ca-certificates curl +RUN curl -sSL https://curl.se/ca/cacert.pem > /usr/local/share/ca-certificates/local-ca.crt +RUN update-ca-certificates + +# Copy our build +COPY --from=base /sdv/target/x86_64-unknown-linux-musl/release/${APP_NAME} /sdv/${APP_NAME} + +# Use the unprivileged chariott user during execution. +USER ${USER_NAME}:${USER_NAME} +ENV APP_NAME=${APP_NAME} +CMD ["sh", "-c", "./${APP_NAME}"] diff --git a/intent_brokering/dogmode/applications/Dockerfile.generic b/intent_brokering/dogmode/applications/Dockerfile.generic new file mode 100644 index 0000000..6aacc02 --- /dev/null +++ b/intent_brokering/dogmode/applications/Dockerfile.generic @@ -0,0 +1,21 @@ +##################################################################################################### +## Final image +#################################################################################################### +FROM intent_brokering_examples:base as base +FROM alpine:latest +ARG APP_NAME +ENV USER_NAME=sample + +# Import Chariott user and group from base. +COPY --from=base /etc/passwd /etc/passwd +COPY --from=base /etc/group /etc/group + +WORKDIR /sdv + +# Copy our build +COPY --from=base /sdv/target/x86_64-unknown-linux-musl/release/${APP_NAME} /sdv/${APP_NAME} + +# Use the unprivileged sample user during execution. +USER ${USER_NAME}:${USER_NAME} +ENV APP_NAME=${APP_NAME} +CMD ["sh", "-c", "./${APP_NAME}"] diff --git a/intent_brokering/dogmode/applications/Dockerfile.kv-app.ci b/intent_brokering/dogmode/applications/Dockerfile.kv-app.ci new file mode 100644 index 0000000..9bd044d --- /dev/null +++ b/intent_brokering/dogmode/applications/Dockerfile.kv-app.ci @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +ARG RUST_VERSION=1.70 +FROM docker.io/library/rust:${RUST_VERSION} AS builder + +# Dockerfile for building the kv-app container +# +# This Dockerfile utilizes a two step build process. It builds kv-app with +# statically linked dependencies (using musl vs. glibc to accomplish this) for a +# specific architecture such that we can utilize a scratch container without +# further dependencies for our final container, minimizing container size. + +# Examples user id +ARG SAMPLE_UID=10001 + +# User name +ARG USER_NAME=sample + +RUN apt update && apt upgrade -y +RUN apt install -y cmake protobuf-compiler musl-tools + +WORKDIR /sdv + +COPY ./ . + +RUN rustup target add x86_64-unknown-linux-musl + +RUN cargo build --release --target=x86_64-unknown-linux-musl --package kv-app + +# unprivileged identity to run kv-app as +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${SAMPLE_UID}" \ + ${USER_NAME} + +##################################################################################################### +## Final image +#################################################################################################### +FROM alpine:latest +ARG APP_NAME +ARG USER_NAME +# Import kv-app user and group from builder. +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group + +WORKDIR /sdv + +# Copy our build +COPY --from=builder /sdv/target/x86_64-unknown-linux-musl/release/kv-app /sdv/kv-app + +# Use the unprivileged kv-app user during execution. +USER ${USER_NAME}:${USER_NAME} +CMD ["sh", "-c", "./kv-app"] diff --git a/intent_brokering/dogmode/applications/Dockerfile.mock-vas b/intent_brokering/dogmode/applications/Dockerfile.mock-vas new file mode 100644 index 0000000..a9e8907 --- /dev/null +++ b/intent_brokering/dogmode/applications/Dockerfile.mock-vas @@ -0,0 +1,23 @@ +##################################################################################################### +## Final image +#################################################################################################### +FROM intent_brokering_examples:base as base +FROM alpine:latest +ARG APP_NAME +ENV USER_NAME=sample + +# Import Chariott user and group from base. +COPY --from=base /etc/passwd /etc/passwd +COPY --from=base /etc/group /etc/group + +WORKDIR /sdv +RUN apk add bash + +# Copy our build +COPY --from=base /sdv/target/x86_64-unknown-linux-musl/release/${APP_NAME} /sdv/${APP_NAME} +COPY ./intent_brokering/dogmode/applications/dog-mode-ui/mock_provider_dog_mode_demo.sh /sdv/ + +# Use the unprivileged chariott user during execution. +USER ${USER_NAME}:${USER_NAME} +ENV APP_NAME=${APP_NAME} +CMD ["sh", "-c", "./mock_provider_dog_mode_demo.sh | ./${APP_NAME}"] diff --git a/intent_brokering/dogmode/applications/Dockerfile.simulated-camera b/intent_brokering/dogmode/applications/Dockerfile.simulated-camera new file mode 100644 index 0000000..2c3169c --- /dev/null +++ b/intent_brokering/dogmode/applications/Dockerfile.simulated-camera @@ -0,0 +1,23 @@ +##################################################################################################### +## Final image +#################################################################################################### +FROM intent_brokering_examples:base as base +FROM alpine:latest +ARG APP_NAME +ENV USER_NAME=sample + +# Import Chariott user and group from base. +COPY --from=base /etc/passwd /etc/passwd +COPY --from=base /etc/group /etc/group + +WORKDIR /sdv +RUN mkdir -p /sdv/images + +# Copy our build +COPY --from=base /sdv/target/x86_64-unknown-linux-musl/release/${APP_NAME} /sdv/${APP_NAME} +COPY ./intent_brokering/dogmode/applications/simulated-camera/images /sdv/images/ + +# Use the unprivileged chariott user during execution. +USER ${USER_NAME}:${USER_NAME} +ENV APP_NAME=${APP_NAME} +CMD ["sh", "-c", "./${APP_NAME}"] diff --git a/intent_brokering/dogmode/applications/build-all-containers.sh b/intent_brokering/dogmode/applications/build-all-containers.sh new file mode 100755 index 0000000..4ee56b6 --- /dev/null +++ b/intent_brokering/dogmode/applications/build-all-containers.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +set -e + +function concat_image_registry() { + if [ -z "$IMAGE_REGISTRY" ]; then + echo "$1" + else + echo "$IMAGE_REGISTRY/$1" + fi +} + +if [ -z "$IMAGE_TAG" ]; then + export IMAGE_TAG="1" +fi + +# Build base image for all example applications +docker build --tag "intent_brokering_examples:base" --file ./intent_brokering/dogmode/applications/Dockerfile.base . + +# Build Intent Brokering service +# docker build --tag "$(concat_image_registry intent_brokering:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.generic --build-arg APP_NAME=intent_brokering . + +# Build Examples +docker build --tag "$(concat_image_registry cloud-object-detection-app:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.cloud-object-detection --build-arg APP_NAME=cloud-object-detection-app . +docker build --tag "$(concat_image_registry dog-mode-logic-app:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.generic --build-arg APP_NAME=dog-mode-logic-app . +docker build --tag "$(concat_image_registry kv-app:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.generic --build-arg APP_NAME=kv-app . +# Local object detection build is not executed as it is currently not working due to missing tensorflow libraries in the image +# docker build --tag "$(concat_image_registry local-object-detection-app:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile --build-arg APP_NAME=local-object-detection-app . +docker build --tag "$(concat_image_registry mock-vas:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.mock-vas --build-arg APP_NAME=mock-vas . +docker build --tag "$(concat_image_registry simulated-camera-app:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.simulated-camera --build-arg APP_NAME=simulated-camera-app . +docker build --tag "$(concat_image_registry lt-provider-app:"$IMAGE_TAG")" --file ./intent_brokering/dogmode/applications/Dockerfile.generic --build-arg APP_NAME=lt-provider-app . diff --git a/intent_brokering/dogmode/applications/cloud-object-detection/Cargo.toml b/intent_brokering/dogmode/applications/cloud-object-detection/Cargo.toml new file mode 100644 index 0000000..c54052b --- /dev/null +++ b/intent_brokering/dogmode/applications/cloud-object-detection/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "cloud-object-detection-app" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +async-trait = { workspace = true } +base64 = "0.21.7" +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +examples-common = { path = "../../common/" } +futures = { workspace = true } +lazy_static = { workspace = true } +reqwest = { version = "0.11.23", features = ["json"] } +serde = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-stream = { workspace = true } +tokio-util = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } + +[dev-dependencies] +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/intent_brokering/dogmode/applications/cloud-object-detection/README.md b/intent_brokering/dogmode/applications/cloud-object-detection/README.md new file mode 100644 index 0000000..1ae00e6 --- /dev/null +++ b/intent_brokering/dogmode/applications/cloud-object-detection/README.md @@ -0,0 +1,42 @@ +# Cloud object detection application + +This code sample shows you an implementation of object detection based on Azure +Cognitive Services. + +To run the application: + +1. Start the Intent Brokering runtime by executing `cargo run -p intent_brokering` +2. Start the cloud object detection by executing `cargo run` from the + `intent_brokering/dogmode/applications/cloud-object-detection` directory while specifying + `COGNITIVE_ENDPOINT` (i.e. `myendpoint.cognitiveservices.azure.com`) and + `COGNITIVE_KEY` environment variables. +3. In the root directory create a `detect_image.json` file with the following + message structure: + + ```json + { + "intent": { + "invoke": { + "command": "detect", + "args": [ + { + "any": { + "@type": "examples.detection.v1.DetectRequest", + "blob": { + "media_type": "image/jpg", + "bytes": "base64 encoding of the image" + } + } + } + ] + } + }, + "namespace": "sdv.detection" + } + ``` + +4. Execute detection with `grpcurl -v -plaintext -import-path proto/ \ + -import-path intent_brokering/dogmode/applications/proto -use-reflection -proto \ + intent_brokering/dogmode/applications/proto/examples/detection/v1/detection.proto -d @ \ + localhost:4243 intent_brokering.runtime.v1.IntentBrokeringService/Fulfill < \ + detect_image.json` diff --git a/intent_brokering/dogmode/applications/cloud-object-detection/src/detection.rs b/intent_brokering/dogmode/applications/cloud-object-detection/src/detection.rs new file mode 100644 index 0000000..117ccbe --- /dev/null +++ b/intent_brokering/dogmode/applications/cloud-object-detection/src/detection.rs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use examples_common::examples::detection::{DetectRequest, DetectResponse, DetectionObject}; +use intent_brokering_common::error::{Error, ResultExt}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::{env, mem}; + +pub struct DetectionLogic { + http_client: Client, + cognitive_endpoint: String, + cognitive_key: String, +} + +impl DetectionLogic { + pub fn new() -> Self { + let http_client = Client::new(); + + let cognitive_endpoint = match env::var("COGNITIVE_ENDPOINT") { + Ok(e) => e.replace("https://", ""), + Err(_) => panic!("Missing COGNITIVE_ENDPOINT environment variable"), + }; + let cognitive_key = match env::var("COGNITIVE_KEY") { + Ok(e) => e, + Err(_) => panic!("Missing COGNITIVE_KEY environment variable"), + }; + + Self { + http_client, + cognitive_endpoint, + cognitive_key, + } + } + + pub async fn detect_cloud(&self, body: DetectRequest) -> Result { + let response = self + .http_client + .post(format!( + "https://{}/vision/v3.2/detect?model-version=2021-04-01", + self.cognitive_endpoint + )) + .header("Ocp-Apim-Subscription-Key", self.cognitive_key.to_owned()) + .header("Content-Type", "application/octet-stream") + .body(Vec::::from(body)) + .send() + .await + .and_then(|r| r.error_for_status()) + .map_err_with("Request to Cognitive Services failed.")?; + + let deserialized_response = response + .json::() + .await + .map_err_with("Deserialization failed")?; + + Ok(DetectResponse::new( + deserialized_response + .objects + .iter() + .flat_map(|o| o.ascendants_and_self()) + .map(|o| DetectionObject::new(o.object.clone(), o.confidence)) + .collect(), + )) + } +} + +impl Default for DetectionLogic { + fn default() -> Self { + Self::new() + } +} + +#[derive(Serialize, Deserialize)] +pub struct DetectionResponse { + objects: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Object { + object: String, + confidence: f64, + parent: Option>, +} + +impl Object { + pub fn ascendants_and_self(&self) -> ObjectAscendantsAndSelfIterator<'_> { + ObjectAscendantsAndSelfIterator { next: Some(self) } + } +} + +pub struct ObjectAscendantsAndSelfIterator<'a> { + next: Option<&'a Object>, +} + +impl<'a> Iterator for ObjectAscendantsAndSelfIterator<'a> { + type Item = &'a Object; + + fn next(&mut self) -> Option { + let mut next = self + .next + .and_then(|n| n.parent.as_ref()) + .map(|p| p.as_ref()); + mem::swap(&mut self.next, &mut next); + next + } +} diff --git a/intent_brokering/dogmode/applications/cloud-object-detection/src/intent_provider.rs b/intent_brokering/dogmode/applications/cloud-object-detection/src/intent_provider.rs new file mode 100644 index 0000000..602e612 --- /dev/null +++ b/intent_brokering/dogmode/applications/cloud-object-detection/src/intent_provider.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use async_trait::async_trait; +use intent_brokering_proto::{ + common::{FulfillmentEnum, FulfillmentMessage, IntentEnum}, + provider::{provider_service_server::ProviderService, FulfillRequest, FulfillResponse}, +}; +use tonic::{Request, Response, Status}; +use tracing::error; + +use crate::detection::DetectionLogic; + +use examples_common::{ + examples::detection::DetectRequest, + intent_brokering::inspection::{fulfill, Entry}, +}; + +pub struct IntentProvider { + internal_logic: DetectionLogic, +} + +impl IntentProvider { + pub fn new() -> Self { + let internal_logic = DetectionLogic::new(); + Self { internal_logic } + } +} + +lazy_static::lazy_static! { + static ref INSPECT_FULFILLMENT_SCHEMA: Vec = vec![ + Entry::new("detect", [ + ("member_type", "command"), + ("request", "examples.detection.v1.DetectRequest"), + ("response", "examples.detection.v1.DetectResponse"), + ]) + ]; +} + +#[async_trait] +impl ProviderService for IntentProvider { + async fn fulfill( + &self, + request: Request, + ) -> Result, Status> { + let response = match request + .into_inner() + .intent + .and_then(|i| i.intent) + .ok_or_else(|| Status::invalid_argument("Intent must be specified"))? + { + IntentEnum::Inspect(inspect) => fulfill(inspect.query, &*INSPECT_FULFILLMENT_SCHEMA), + IntentEnum::Invoke(intent) => { + let arg = DetectRequest::try_from(intent) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + + let result = self.internal_logic.detect_cloud(arg).await.map_err(|e| { + error!("Error when running detection: '{e:?}'."); + Status::unknown(format!("Error when invoking function: '{}'", e)) + })?; + + FulfillmentEnum::Invoke(result.into()) + } + _ => Err(Status::not_found(""))?, + }; + + Ok(Response::new(FulfillResponse { + fulfillment: Some(FulfillmentMessage { + fulfillment: Some(response), + }), + })) + } +} diff --git a/intent_brokering/dogmode/applications/cloud-object-detection/src/main.rs b/intent_brokering/dogmode/applications/cloud-object-detection/src/main.rs new file mode 100644 index 0000000..bf96eb0 --- /dev/null +++ b/intent_brokering/dogmode/applications/cloud-object-detection/src/main.rs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod detection; +mod intent_provider; + +use examples_common::intent_brokering; +use intent_brokering_common::error::Error; +use intent_brokering_common::shutdown::RouterExt as _; +use intent_brokering_proto::{ + provider::provider_service_server::ProviderServiceServer, + runtime::{intent_registration::Intent, intent_service_registration::ExecutionLocality}, +}; +use tonic::transport::Server; + +use crate::intent_provider::IntentProvider; + +intent_brokering::provider::main!(wain); + +async fn wain() -> Result<(), Error> { + let (url, socket_address) = intent_brokering::provider::register( + "sdv.cloud-detection", + "0.0.1", + "sdv.detection", + [Intent::Inspect, Intent::Invoke], + "CLOUD_DETECTION_URL", + "http://0.0.0.0:50063", // DevSkim: ignore DS137138 + ExecutionLocality::Cloud, + ) + .await?; + + tracing::info!("Application listening on: {url}"); + + Server::builder() + .add_service(ProviderServiceServer::new(IntentProvider::new())) + .serve_with_ctrl_c_shutdown(socket_address) + .await +} diff --git a/intent_brokering/dogmode/applications/dog-mode-logic/Cargo.toml b/intent_brokering/dogmode/applications/dog-mode-logic/Cargo.toml new file mode 100644 index 0000000..ed4045b --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-logic/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dog-mode-logic-app" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +anyhow = { workspace = true } +async-stream = "0.3.3" +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +examples-common = { path = "../../common/" } +futures = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +serde = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-stream = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } diff --git a/intent_brokering/dogmode/applications/dog-mode-logic/src/dog_mode_status.rs b/intent_brokering/dogmode/applications/dog-mode-logic/src/dog_mode_status.rs new file mode 100644 index 0000000..c71a6c7 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-logic/src/dog_mode_status.rs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use anyhow::{anyhow, Error}; +use async_stream::try_stream; +use examples_common::{ + examples::proto::detection::{DetectRequest, DetectResponse}, + intent_brokering::{ + api::{GrpcIntentBrokering, IntentBrokering, IntentBrokeringExt as _}, + value::Value, + }, +}; +use futures::{stream::BoxStream, TryStreamExt}; +use intent_brokering_proto::common::Blob; +use tokio_stream::StreamExt; +use tracing::{info, warn}; + +use crate::{DogModeState, DOG_MODE_STATUS_ID, KEY_VALUE_STORE_NAMESPACE}; + +pub(crate) async fn stream_dog_mode_status( + mut intent_broker: GrpcIntentBrokering, + state: &mut DogModeState, +) -> Result>, Error> { + if let ok @ Ok(_) = detect_dog(intent_broker.clone()).await { + info!("Using automated dog detection."); + + // The dog mode logic application is responsible for updating the + // dog mode state in the key-value store. + state.write_dog_mode_status = true; + + ok + } else { + warn!("Automatic dog detection failed. Falling back to using an external application to turn on the dog mode."); + + Ok(Box::pin( + intent_broker + .listen(KEY_VALUE_STORE_NAMESPACE, [DOG_MODE_STATUS_ID.into()]) + .await? + .map_err(|e| e.into()) + .map(|r| { + r.and_then(|e| { + e.data + .to_bool() + .map_err(|_| anyhow!("Result was not of type 'Bool'.")) + }) + }), + )) + } +} + +async fn detect_dog( + mut intent_broker: GrpcIntentBrokering, +) -> Result>, Error> { + const CAMERA_NAMESPACE: &str = "sdv.camera.simulated"; + const FRAMES_METADATA_KEY: &str = "frames_per_minute"; + const OBJECT_DETECTION_NAMESPACE: &str = "sdv.detection"; + const DETECT_COMMAND_NAME: &str = "detect"; + const DOG_CATEGORY_NAME: &str = "dog"; + const SYSTEM_REGISTRY_NAMESPACE: &str = "system.registry"; + + /// Asserts whether any intents are registered for the specified + /// namespace. If there are intents, we assume that those are the + /// supported intent. + async fn ensure_vehicle_functionality( + intent_broker: &mut impl IntentBrokering, + namespace: &str, + ) -> Result<(), Error> { + if intent_broker + .inspect(SYSTEM_REGISTRY_NAMESPACE, namespace) + .await? + .is_empty() + { + Err(anyhow!( + "Vehicle does not have registrations for namespace '{namespace}'." + )) + } else { + Ok(()) + } + } + + ensure_vehicle_functionality(&mut intent_broker, CAMERA_NAMESPACE).await?; + ensure_vehicle_functionality(&mut intent_broker, OBJECT_DETECTION_NAMESPACE).await?; + + // Stream images from the camera at the highest frame rate. + + let (subscription_key, frames) = intent_broker + .inspect(CAMERA_NAMESPACE, "**") + .await? + .into_iter() + .filter_map(|entry| { + entry + .get(FRAMES_METADATA_KEY) + .and_then(|frames| frames.to_i32().ok()) + .map(|frames| (entry.path().into(), frames)) + }) + .max_by_key(|(_, frames)| *frames) + .ok_or_else(|| anyhow!("Could not find an entry with maximum framerate."))?; + + // Stream the images and run object detection on them. + + info!("Streaming with frame rate of {frames}fpm."); + + let images = intent_broker + .listen(CAMERA_NAMESPACE, [subscription_key]) + .await?; + + let dog_mode_state_stream = try_stream! { + for await image in images { + yield image_contains_dog(&mut intent_broker, image?.data).await?; + } + }; + + async fn image_contains_dog( + intent_broker: &mut impl IntentBrokering, + image: Value, + ) -> Result { + use prost::Message; + + let (media_type, bytes) = image + .into_blob() + .map_err(|_| anyhow!("Unexpected image return type (expected: 'Blob')."))?; + + let detect_request = DetectRequest { + blob: Some(Blob { media_type, bytes }), + }; + + let mut detect_request_bytes = vec![]; + detect_request.encode(&mut detect_request_bytes)?; + + let detection_result = intent_broker + .invoke( + OBJECT_DETECTION_NAMESPACE, + DETECT_COMMAND_NAME, + [Value::new_any( + "examples.detection.v1.DetectRequest".to_owned(), + detect_request_bytes, + )], + ) + .await?; + + let (_, value) = detection_result + .into_any() + .map_err(|_| anyhow!("Detection response was not of type 'Any'."))?; + + let detected_categories: DetectResponse = Message::decode(&value[..])?; + Ok(detected_categories + .entries + .iter() + .any(|e| e.object == DOG_CATEGORY_NAME)) + } + + Ok(Box::pin(dog_mode_state_stream)) +} diff --git a/intent_brokering/dogmode/applications/dog-mode-logic/src/main.rs b/intent_brokering/dogmode/applications/dog-mode-logic/src/main.rs new file mode 100644 index 0000000..694b6f6 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-logic/src/main.rs @@ -0,0 +1,836 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::{ + fmt::Display, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, Error}; +use dog_mode_status::stream_dog_mode_status; +use examples_common::intent_brokering::{ + api::{GrpcIntentBrokering, IntentBrokering, IntentBrokeringExt as _}, + value::Value, +}; +use tokio::{select, time::sleep_until}; +use tokio_stream::StreamExt; +use tracing::{error, info, warn, Level}; +use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; + +mod dog_mode_status; + +// Namespaces +const VDT_NAMESPACE: &str = "sdv.vdt"; +pub const KEY_VALUE_STORE_NAMESPACE: &str = "sdv.kvs"; + +// Dog mode boundary conditions +const LOW_BATTERY_LEVEL: i32 = 19; +const MIN_TEMPERATURE: i32 = 20; +const MAX_TEMPERATURE: i32 = 26; + +// Method names +const ACTIVATE_AIR_CONDITIONING_ID: &str = "Vehicle.Cabin.HVAC.IsAirConditioningActive"; +const SEND_NOTIFICATION_ID: &str = "send_notification"; +const SET_UI_MESSAGE_ID: &str = "set_ui_message"; + +// Event identifiers +pub const DOG_MODE_STATUS_ID: &str = "Feature.DogMode.Status"; +const CABIN_TEMPERATURE_ID: &str = "Vehicle.Cabin.HVAC.AmbientAirTemperature"; +const AIR_CONDITIONING_STATE_ID: &str = "Vehicle.Cabin.HVAC.IsAirConditioningActive"; +const BATTERY_LEVEL_ID: &str = "Vehicle.OBD.HybridBatteryRemaining"; + +static FUNCTION_INVOCATION_THROTTLING_DURATION: Duration = Duration::from_secs(5); +static AIR_CONDITIONING_ACTIVATION_TIMEOUT: Duration = Duration::from_secs(10); +static TIMEOUT_EVALUATION_INTERVAL: Duration = Duration::from_secs(2); + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct DogModeState { + temperature: i32, + dogmode_status: bool, + battery_level: i32, + air_conditioning_active: bool, + air_conditioning_activation_time: Option, + last_air_conditioning_invocation_time: Instant, + write_dog_mode_status: bool, + send_notification_disabled: bool, + set_ui_message_disabled: bool, +} + +impl DogModeState { + pub fn new() -> Self { + Self { + temperature: 25, + air_conditioning_active: false, + dogmode_status: false, + battery_level: 100, + air_conditioning_activation_time: None, + last_air_conditioning_invocation_time: Instant::now() + - FUNCTION_INVOCATION_THROTTLING_DURATION, + write_dog_mode_status: false, + send_notification_disabled: false, + set_ui_message_disabled: false, + } + } +} + +#[tokio::main] +pub async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(Level::INFO.into()) + .from_env_lossy(), + ) + .finish() + .init(); + + let mut intent_broker = GrpcIntentBrokering::connect().await?; + let mut state = DogModeState::new(); + + // Inspect vehicle hardware to assert that all requirements are met before + // executing the dog mode logic. + + { + const MEMBER_TYPE: &str = "member_type"; + const TYPE: &str = "type"; + const MEMBER_TYPE_COMMAND: &str = "command"; + const MEMBER_TYPE_PROPERTY: &str = "property"; + + for (path, member_type, r#type) in [ + ( + CABIN_TEMPERATURE_ID, + /*..........*/ MEMBER_TYPE_PROPERTY, + "int32", + ), + ( + AIR_CONDITIONING_STATE_ID, + /*.....*/ MEMBER_TYPE_PROPERTY, + "bool", + ), + ( + BATTERY_LEVEL_ID, + /*..............*/ MEMBER_TYPE_PROPERTY, + "int32", + ), + ( + ACTIVATE_AIR_CONDITIONING_ID, + /*..*/ MEMBER_TYPE_COMMAND, + "IAcmeAirconControl", + ), + ] { + inspect_dependency( + &mut intent_broker, + path, + &[ + (MEMBER_TYPE, member_type.into()), + (TYPE, /*..*/ r#type.into()), + ], + ) + .await?; + } + + for (path, r#type, state) in [ + ( + SEND_NOTIFICATION_ID, + "ISendNotification", + &mut state.send_notification_disabled, + ), + ( + SET_UI_MESSAGE_ID, + "ISetUiMessage", + &mut state.set_ui_message_disabled, + ), + ] { + if let Err(e) = inspect_dependency( + &mut intent_broker, + path, + &[ + (MEMBER_TYPE, MEMBER_TYPE_COMMAND.into()), + (TYPE, /*..*/ r#type.into()), + ], + ) + .await + { + warn!("Error when inspecting for optional dependency {path}: '{e:?}'."); + *state = true + } + } + + if state.send_notification_disabled && state.set_ui_message_disabled { + Err(anyhow!( + "Neither {SEND_NOTIFICATION_ID} nor {SET_UI_MESSAGE_ID} are available", + ))?; + } + + async fn inspect_dependency( + intent_broker: &mut GrpcIntentBrokering, + path: impl Into> + Send, + expected_properties: &[(&str, Value)], + ) -> Result<(), Error> { + let inspection_result = intent_broker.inspect(VDT_NAMESPACE, path).await?; + inspection_result.iter().map(|m| { + expected_properties.iter().map(|(expected_key, expected_value)| m.get(*expected_key) + .ok_or_else(|| anyhow!("Member does not specify {expected_key:?}.")) + .and_then(|actual| { + if expected_value != actual { + Err(anyhow!( + "Member is of {expected_key} '{actual:?}' instead of '{expected_value:?}'." + )) + } + else { + Ok(()) + } + })).reduce(|x, y| x.and(y)).unwrap_or_else(|| Err(anyhow!("Expected properties array was empty."))) + }).fold(Err(anyhow!("Could not find a single member within the specified path.")), |x, y| x.or(y)) + } + } + + // Set up the VDT and dog mode status streaming. + + let mut vdt_stream = intent_broker + .listen( + VDT_NAMESPACE, + [ + CABIN_TEMPERATURE_ID.into(), + AIR_CONDITIONING_STATE_ID.into(), + BATTERY_LEVEL_ID.into(), + ], + ) + .await?; + + let mut dog_mode_status_stream = + stream_dog_mode_status(intent_broker.clone(), &mut state).await?; + + let mut next_timer_wakeup = Instant::now() + TIMEOUT_EVALUATION_INTERVAL; + + loop { + // Keeping track of the previous state allows us to use Markov chain style business logic. + let previous_state = state; + + let state_update = select! { + _ = sleep_until(next_timer_wakeup.into()) => { + next_timer_wakeup = Instant::now() + TIMEOUT_EVALUATION_INTERVAL; + if let Some(new_state) = on_dog_mode_timer(&state, &mut intent_broker).await? { + state = new_state; + } + + None + }, + dog_mode_status = dog_mode_status_stream.next() => { + if let Some(dog_mode_status) = dog_mode_status { + match dog_mode_status { + Ok(dog_mode_status) => Some(DogModeState { dogmode_status: dog_mode_status, ..state }), + Err(err) => { error!("Error when handling dog mode status update: '{err:?}'."); None } + } + } else { + return Err(anyhow!("Dog mode status stream broke.")); + } + } + event = vdt_stream.next() => { + if let Some(event) = event { + match event { + Ok(event) => match event.id.as_ref() { + BATTERY_LEVEL_ID => event.data.to_i32().ok().map(|battery_level| DogModeState { battery_level, ..state }), + AIR_CONDITIONING_STATE_ID => event.data.to_bool().ok().map(|air_conditioning_active| DogModeState { air_conditioning_active, ..state }), + CABIN_TEMPERATURE_ID => event.data.to_i32().ok().map(|temperature| DogModeState { temperature, ..state }), + method => { error!("No method '{method}' found."); None } + } + Err(err) => { error!("Error when handling event: '{err:?}'."); None } + } + } + else { + return Err(anyhow!("Event stream broke.")); + } + } + }; + + if let Some(new_state) = state_update { + state = new_state + } + + match run_dog_mode(&state, &previous_state, &mut intent_broker).await { + Ok(Some(new_state)) => state = new_state, + Err(e) => error!("{e:?}"), + Ok(None) => {} + } + } +} + +async fn run_dog_mode( + state: &DogModeState, + previous_state: &DogModeState, + intent_broker: &mut impl IntentBrokering, +) -> Result, Error> { + if state == previous_state { + return Ok(None); + } + + fn log_change( + label: &str, + curr: &DogModeState, + prev: &DogModeState, + f: fn(&DogModeState) -> T, + ) -> Result<(), Error> { + let v = f(curr); + if v != f(prev) { + info!("{label}: {v}"); + } + Ok(()) + } + + log_change("Dog mode", state, previous_state, |s| s.dogmode_status)?; + log_change("Cabin Temperature", state, previous_state, |s| { + s.temperature + })?; + log_change("Air conditioning", state, previous_state, |s| { + s.air_conditioning_active + })?; + log_change("Battery level", state, previous_state, |s| s.battery_level)?; + + if state.write_dog_mode_status && (state.dogmode_status != previous_state.dogmode_status) { + intent_broker + .write( + KEY_VALUE_STORE_NAMESPACE, + DOG_MODE_STATUS_ID, + state.dogmode_status.into(), + ) + .await?; + } + + // Immediately end, if dog mode is disabled + if !state.dogmode_status { + if previous_state.dogmode_status { + activate_air_conditioning(intent_broker, false).await?; + } + + return Ok(None); + } + + let mut output_state = None; + + // If the temperature falls below the set minimum, turn off air conditioning + if MIN_TEMPERATURE >= state.temperature && state.air_conditioning_active { + if let Some(last_air_conditioning_invocation_time) = + activate_air_conditioning_with_throttling(false, state, intent_broker).await? + { + output_state = Some(DogModeState { + last_air_conditioning_invocation_time, + ..*state + }); + } + } + + // If all criteria is fulfilled, activate air conditioning + if state.temperature > MAX_TEMPERATURE && !state.air_conditioning_active { + if let Some(last_air_conditioning_invocation_time) = + activate_air_conditioning_with_throttling(true, state, intent_broker).await? + { + output_state = Some(DogModeState { + last_air_conditioning_invocation_time, + air_conditioning_activation_time: Some(last_air_conditioning_invocation_time), + ..*state + }); + } + } + + async fn activate_air_conditioning_with_throttling( + value: bool, + state: &DogModeState, + intent_broker: &mut impl IntentBrokering, + ) -> Result, Error> { + let now = Instant::now(); + if now + > state.last_air_conditioning_invocation_time + FUNCTION_INVOCATION_THROTTLING_DURATION + { + activate_air_conditioning(intent_broker, value).await?; + return Ok(Some(now)); + } + + Ok(None) + } + + // Air conditioning state was changed by the provider. + if state.air_conditioning_active && !previous_state.air_conditioning_active { + send_notification(intent_broker, "The car is now being cooled.", state).await?; + set_ui_message(intent_broker, "The car is cooled, no need to worry.", state).await?; + } + + // If the battery level fell below a threshold value, send a warning to the car owner. + if previous_state.battery_level > LOW_BATTERY_LEVEL && state.battery_level <= LOW_BATTERY_LEVEL + { + send_notification( + intent_broker, + "The battery is low, please return to the car.", + state, + ) + .await?; + set_ui_message( + intent_broker, + "The battery is low, the animal is in danger.", + state, + ) + .await?; + } + + async fn activate_air_conditioning( + intent_broker: &mut impl IntentBrokering, + value: bool, + ) -> Result<(), Error> { + _ = intent_broker + .invoke(VDT_NAMESPACE, ACTIVATE_AIR_CONDITIONING_ID, [value.into()]) + .await?; + Ok(()) + } + + async fn send_notification( + intent_broker: &mut impl IntentBrokering, + message: &str, + state: &DogModeState, + ) -> Result<(), Error> { + if !state.send_notification_disabled { + _ = intent_broker + .invoke(VDT_NAMESPACE, SEND_NOTIFICATION_ID, [message.into()]) + .await?; + Ok(()) + } else { + // as this is an optional method we don't care + Ok(()) + } + } + + async fn set_ui_message( + intent_broker: &mut impl IntentBrokering, + message: &str, + state: &DogModeState, + ) -> Result<(), Error> { + if !state.set_ui_message_disabled { + _ = intent_broker + .invoke(VDT_NAMESPACE, SET_UI_MESSAGE_ID, [message.into()]) + .await?; + Ok(()) + } else { + // as this is an optional method we don't care + Ok(()) + } + } + + Ok(output_state) +} + +async fn on_dog_mode_timer( + state: &DogModeState, + intent_broker: &mut impl IntentBrokering, +) -> Result, Error> { + if let Some(air_conditioning_activation_time) = state.air_conditioning_activation_time { + if state.air_conditioning_active { + return Ok(Some(DogModeState { + air_conditioning_activation_time: None, + ..*state + })); + } else if Instant::now() + > air_conditioning_activation_time + AIR_CONDITIONING_ACTIVATION_TIMEOUT + { + _ = intent_broker + .invoke( + VDT_NAMESPACE, + SEND_NOTIFICATION_ID, + ["Error while activating air conditioning, please return to the car immediately.".into()], + ) + .await?; + + return Ok(Some(DogModeState { + air_conditioning_activation_time: None, + ..*state + })); + } + } + + Ok(None) +} + +#[cfg(test)] +mod tests { + use async_trait::async_trait; + use examples_common::intent_brokering::{api::Service, inspection::Entry}; + use intent_brokering_common::error::Error; + + use super::*; + + #[derive(PartialEq, Eq, Debug, Default)] + struct CarControllerMock { + ui_message: Option, + notification: Option, + air_conditioning_state: Option, + } + + #[async_trait] + impl IntentBrokering for CarControllerMock { + async fn invoke + Send>( + &mut self, + namespace: impl Into> + Send, + command: impl Into> + Send, + args: I, + ) -> Result { + let arg = args.into_iter().next().unwrap(); + match (namespace.into().as_ref(), command.into().as_ref()) { + (VDT_NAMESPACE, SET_UI_MESSAGE_ID) => { + if let Ok(value) = arg.into_string() { + self.ui_message = Some(value); + } + } + (VDT_NAMESPACE, SEND_NOTIFICATION_ID) => { + if let Ok(value) = arg.into_string() { + self.notification = Some(value); + } + } + (VDT_NAMESPACE, ACTIVATE_AIR_CONDITIONING_ID) => { + if let Ok(value) = arg.to_bool() { + self.air_conditioning_state = Some(value); + } + } + _ => {} + } + + Ok(Value::TRUE) + } + + async fn subscribe> + Send>( + &mut self, + _namespace: impl Into> + Send, + _channel_id: impl Into> + Send, + _event_ids: I, + ) -> Result<(), Error> { + todo!() + } + + async fn discover( + &mut self, + _namespace: impl Into> + Send, + ) -> Result, Error> { + todo!() + } + + async fn inspect( + &mut self, + _namespace: impl Into> + Send, + _query: impl Into> + Send, + ) -> Result, Error> { + todo!() + } + + async fn write( + &mut self, + _: impl Into> + Send, + _: impl Into> + Send, + _: Value, + ) -> Result<(), Error> { + todo!() + } + + async fn read( + &mut self, + _: impl Into> + Send, + _: impl Into> + Send, + ) -> Result, Error> { + todo!() + } + } + + #[tokio::test] + async fn test_dog_mode_activation_has_no_effect_when_no_conditions_are_met() { + let mut car_controller: CarControllerMock = Default::default(); + + let original_state = DogModeState::new(); + + // Act + let state = DogModeState { + dogmode_status: true, + ..original_state + }; + + let result = run_dog_mode(&state, &original_state, &mut car_controller).await; + + // Assert + assert!(result.is_ok()); + assert_eq!(::default(), car_controller); + } + + #[tokio::test] + async fn test_air_con_is_turned_on_when_temperature_exceeds_max_threshold() { + let mut car_controller = Default::default(); + + let original_state = DogModeState { + temperature: MAX_TEMPERATURE, + battery_level: 100, + dogmode_status: true, + ..DogModeState::new() + }; + + // Act + let state = DogModeState { + temperature: MAX_TEMPERATURE + 1, + ..original_state + }; + + let result = run_dog_mode(&state, &original_state, &mut car_controller).await; + + // Assert + assert!(result.is_ok()); + assert_eq!( + CarControllerMock { + air_conditioning_state: Some(true), + ..Default::default() + }, + car_controller + ); + } + + #[tokio::test] + async fn test_user_is_notified_when_air_con_is_reported_to_be_on() { + let mut car_controller = Default::default(); + + let original_state = DogModeState { + temperature: MAX_TEMPERATURE + 1, + battery_level: 100, + dogmode_status: true, + ..DogModeState::new() + }; + + // Act + let state = DogModeState { + air_conditioning_active: true, + ..original_state + }; + + let result = run_dog_mode(&state, &original_state, &mut car_controller).await; + + // Assert + assert!(result.is_ok()); + assert_eq!( + CarControllerMock { + air_conditioning_state: None, + notification: Some("The car is now being cooled.".to_string()), + ui_message: Some("The car is cooled, no need to worry.".to_string()) + }, + car_controller + ); + } + + #[tokio::test] + async fn test_user_is_notified_when_battery_is_low() { + let mut car_controller = Default::default(); + + let original_state = DogModeState { + dogmode_status: true, + temperature: MAX_TEMPERATURE + 1, + battery_level: LOW_BATTERY_LEVEL + 1, + ..DogModeState::new() + }; + + // Act + let state = DogModeState { + battery_level: LOW_BATTERY_LEVEL, + ..original_state + }; + + let result = run_dog_mode(&state, &original_state, &mut car_controller).await; + + // Assert + assert!(result.is_ok()); + assert_eq!( + CarControllerMock { + air_conditioning_state: Some(true), + notification: Some("The battery is low, please return to the car.".to_string()), + ui_message: Some("The battery is low, the animal is in danger.".to_string()) + }, + car_controller + ); + } + + #[tokio::test] + async fn test_air_con_is_turned_off_when_temperature_below_min_threshold() { + let mut car_controller = Default::default(); + + let original_state = DogModeState { + dogmode_status: true, + temperature: MIN_TEMPERATURE, + air_conditioning_active: true, + ..DogModeState::new() + }; + + // Act + let state = DogModeState { + temperature: MIN_TEMPERATURE - 1, + ..original_state + }; + + let result = run_dog_mode(&state, &original_state, &mut car_controller).await; + + // Assert + assert!(result.is_ok()); + assert_eq!( + CarControllerMock { + air_conditioning_state: Some(false), + ..Default::default() + }, + car_controller + ); + } + + #[tokio::test] + async fn air_conditioning_activation_is_set_when_air_conditioning_turned_on() { + // arrange + let mut car_controller: CarControllerMock = Default::default(); + + let original_state = DogModeState { + dogmode_status: true, + air_conditioning_active: false, + ..DogModeState::new() + }; + + let state = DogModeState { + temperature: MAX_TEMPERATURE + 1, + ..original_state + }; + + // act + let result = run_dog_mode(&state, &original_state, &mut car_controller).await; + + // assert + assert_instant( + Instant::now(), + result + .unwrap() + .unwrap() + .air_conditioning_activation_time + .unwrap(), + Duration::from_secs(5), + ); + } + + #[tokio::test] + async fn notification_is_sent_when_air_conditioning_timeout_expires() { + // arrange + let mut car_controller: CarControllerMock = Default::default(); + let state = DogModeState { + air_conditioning_active: false, + air_conditioning_activation_time: Some( + Instant::now() - AIR_CONDITIONING_ACTIVATION_TIMEOUT * 2, + ), + ..DogModeState::new() + }; + + // act + _ = on_dog_mode_timer(&state, &mut car_controller).await; + + // assert + assert_eq!( + CarControllerMock { + notification: Some("Error while activating air conditioning, please return to the car immediately.".to_owned()), + ..Default::default() + }, + car_controller, + ); + } + + #[tokio::test] + async fn air_conditioning_activation_timestamp_is_reset_when_air_conditioning_activation_timeout_expires( + ) { + // arrange + let mut car_controller: CarControllerMock = Default::default(); + let state = DogModeState { + air_conditioning_active: false, + air_conditioning_activation_time: Some( + Instant::now() - AIR_CONDITIONING_ACTIVATION_TIMEOUT * 2, + ), + ..DogModeState::new() + }; + + // act + let result = on_dog_mode_timer(&state, &mut car_controller).await; + + // assert + assert_eq!( + None, + result.unwrap().unwrap().air_conditioning_activation_time + ); + } + + #[tokio::test] + async fn air_conditioning_activation_timestamp_is_reset_when_air_conditioning_is_activated() { + // arrange + let mut car_controller: CarControllerMock = Default::default(); + let state = DogModeState { + air_conditioning_active: true, + air_conditioning_activation_time: Some(Instant::now()), + ..DogModeState::new() + }; + + // act + let result = on_dog_mode_timer(&state, &mut car_controller).await; + + // assert + assert_eq!( + None, + result.unwrap().unwrap().air_conditioning_activation_time + ); + } + + #[tokio::test] + async fn air_conditioning_should_throttle_function_invocations() { + // arrange + let mut car_controller: CarControllerMock = Default::default(); + let now = Instant::now(); + let state = DogModeState { + temperature: 40, + dogmode_status: true, + air_conditioning_active: false, + last_air_conditioning_invocation_time: now, + ..DogModeState::new() + }; + + // act + let result = run_dog_mode(&state, &DogModeState::new(), &mut car_controller).await; + + // assert + assert_eq!(None, result.unwrap()); + } + + #[tokio::test] + async fn air_conditioning_should_invoke_function_after_throttling_expired() { + // arrange + let mut car_controller: CarControllerMock = Default::default(); + let previous_state = DogModeState { + temperature: 40, + dogmode_status: true, + ..DogModeState::new() + }; + let state = DogModeState { + temperature: 15, + dogmode_status: true, + air_conditioning_active: true, + last_air_conditioning_invocation_time: Instant::now() + - FUNCTION_INVOCATION_THROTTLING_DURATION, + ..DogModeState::new() + }; + + // act + let result = run_dog_mode(&state, &previous_state, &mut car_controller).await; + + // assert + let result = result.unwrap().unwrap(); + assert_instant( + Instant::now(), + result.last_air_conditioning_invocation_time, + Duration::from_secs(3), + ); + + assert!(result.air_conditioning_active); + } + + fn assert_instant(expected: Instant, actual: Instant, margin: Duration) { + assert!(actual < expected + margin); + assert!(expected - margin < actual); + } +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/.editorconfig b/intent_brokering/dogmode/applications/dog-mode-ui/.editorconfig new file mode 100644 index 0000000..98a8790 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/.editorconfig @@ -0,0 +1,49 @@ +# http://editorconfig.org/ + +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +[*.{sln}] +indent_style = tab + +[*.{cs,tt}] +max_line_length = 100 + +[*.cs] +# this. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion + +# Spacing +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false + +# Wrapping +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/.gitignore b/intent_brokering/dogmode/applications/dog-mode-ui/.gitignore new file mode 100644 index 0000000..08fadd7 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/.gitignore @@ -0,0 +1,459 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,visualstudio + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudio diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/DogModeDashboard.sln b/intent_brokering/dogmode/applications/dog-mode-ui/DogModeDashboard.sln new file mode 100644 index 0000000..4572d73 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/DogModeDashboard.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32602.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DogModeDashboard", "src\DogModeDashboard.csproj", "{7EE908F1-9676-483E-ABB5-9F81AEBDC8FA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7FC61FDA-0855-4BA3-A461-F57384197999}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + global.json = global.json + install.sh = install.sh + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7EE908F1-9676-483E-ABB5-9F81AEBDC8FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EE908F1-9676-483E-ABB5-9F81AEBDC8FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EE908F1-9676-483E-ABB5-9F81AEBDC8FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EE908F1-9676-483E-ABB5-9F81AEBDC8FA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {315702A0-4BE3-419C-B8BE-7FC84FEB4A33} + EndGlobalSection +EndGlobal diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/README.md b/intent_brokering/dogmode/applications/dog-mode-ui/README.md new file mode 100644 index 0000000..68bbe77 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/README.md @@ -0,0 +1,53 @@ +# Dog Mode UI + +This solution contains a web front-end served by a ASP.NET back-end +application that implements a user interface and provides a demonstration of +the SDV Application Programming Model through the dog mode scenario. + +## Setup + +You will need the .NET SDK and ASP.NET Core Runtime version 6. As of the writing of this, +installing the .NET SDK on Ubuntu installs the SDK, runtime, and ASP.NET Core runtime. + +If you do not have these already, follow the instructions +[here](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-2004#add-the-microsoft-package-repository), +but replace the current version of the SDK with version 6 (dotnet-sdk-6.0). + +Once the update is done, run: + +```bash +dotnet --info +``` + +to ensure the installation was successful. At the end of the output message, you should see +something like the following. Ensure that they are major version 6, and that you have both the +SDK and ASP.NET Core runtime. + +```bash +.NET SDKs installed: + 6.0.412 [/usr/share/dotnet/sdk] + +.NET runtimes installed: + Microsoft.AspNetCore.App 6.0.20 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App] + Microsoft.NETCore.App 6.0.20 [/usr/share/dotnet/shared/Microsoft.NETCore.App] +``` + +## Running + +Execute the following command (assuming the current working directory is the +same as the directory of this document): + + dotnet run --project src + +to start the ASP.NET web application. If a browser does not open automtically +at the address of the application, open a browser manually and navigate to +. + +Other components such as the VAS (Vehicle Abstraction Service), the mock provider and the dog mode +logic application may be started after launching the ASP.NET application. + +Use the `mock_provider_dog_mode_demo.sh` script to pipe its output into the mock +VAS to generate mocked sensor data (assuming the current working directory is +the root of the repo): + + ./intent_brokering/dogmode/applications/dog-mode-ui/mock_provider_dog_mode_demo.sh | cargo run ... diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/global.json b/intent_brokering/dogmode/applications/dog-mode-ui/global.json new file mode 100644 index 0000000..70e3dcc --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "7.0.100" + } +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/mock_provider_dog_mode_demo.sh b/intent_brokering/dogmode/applications/dog-mode-ui/mock_provider_dog_mode_demo.sh new file mode 100755 index 0000000..16dbdfa --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/mock_provider_dog_mode_demo.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +set -e +# run a loop to oscillate temperature +( + >&2 echo "PID(temp) = $BASHPID" + while true; do + for x in $(seq 18 26; seq 27 -1 17); do + echo temp $x + sleep 1 + done; + done +) & +# run a loop that drains the battery +( + >&2 echo "PID(battery) = $BASHPID" + while true; do + for x in $(seq 100 -1 0); do + echo battery $x + sleep 10 + done; + done +) & +# the PID of each loop is printed to kill them if needed diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/DogModeDashboard.csproj b/intent_brokering/dogmode/applications/dog-mode-ui/src/DogModeDashboard.csproj new file mode 100644 index 0000000..05040f3 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/DogModeDashboard.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/Program.cs b/intent_brokering/dogmode/applications/dog-mode-ui/src/Program.cs new file mode 100644 index 0000000..e63e22a --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/Program.cs @@ -0,0 +1,473 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +using System.Collections; +using System.Collections.Specialized; +using System.Text.Json; +using System.Threading.Channels; +using Grpc.Core; +using Grpc.Net.Client; +using Microsoft.AspNetCore.Mvc; +using IntentBrokeringCommon = IntentBrokering.Common.V1; +using IntentBrokeringRuntime = IntentBrokering.Runtime.V1; +using IntentBrokeringStreaming = IntentBrokering.Streaming.V1; + +var clients = new ConcurrentList>(); + +var builder = WebApplication.CreateBuilder(new WebApplicationOptions +{ + Args = args, + WebRootPath = "wwwroot", +}); +builder.Services.AddHostedService(); +builder.Services.AddSingleton>>(clients); +builder.Services.AddSingleton(_ => new SocketsHttpHandler { EnableMultipleHttp2Connections = true }); + +builder.Services.AddGrpcClient((sp, options) => +{ + options.Address = new Uri("http://localhost:4243/"); // DevSkim: ignore DS162092 + options.ChannelOptionsActions.Add(options => + { + options.HttpHandler = sp.GetRequiredService(); + if (options.ServiceProvider is { } services) + options.LoggerFactory = services.GetService(); + }); +}); + +var app = builder.Build(); +app.UseDefaultFiles(); +app.UseStaticFiles(); + +app.MapPost("/dog-mode", async (HttpContext context, [FromServices] IntentBrokeringRuntime.IntentBrokeringService.IntentBrokeringServiceClient client) => +{ + _ = await client.FulfillAsync(new IntentBrokeringRuntime.FulfillRequest + { + Namespace = KeyValueStoreProperties.Namespace, + Intent = new IntentBrokeringCommon.Intent + { + Write = new IntentBrokeringCommon.WriteIntent + { + Key = KeyValueStoreProperties.DogModeStatus, + Value = new IntentBrokeringCommon.Value { Bool = "on" == context.Request.Form["on"] } + } + } + }); +}); + +app.MapGet("/events", async context => +{ + var response = context.Response; + response.ContentType = "text/event-stream"; + var channel = Channel.CreateUnbounded(); + clients.Add(channel.Writer); + try + { + await foreach (var obj in channel.Reader.ReadAllAsync(context.RequestAborted)) + { + var dataLine = obj switch + { + IntentBrokeringStreaming.Event e => + JsonSerializer.Serialize(new + { + id = e.Source, + data = e.Value.ValueCase switch + { + IntentBrokeringCommon.Value.ValueOneofCase.Int32 => (object)e.Value.Int32, + IntentBrokeringCommon.Value.ValueOneofCase.Bool => e.Value.Bool, + IntentBrokeringCommon.Value.ValueOneofCase.Blob => new { type = e.Value.Blob.MediaType, value = e.Value.Blob.Bytes.ToBase64() }, + _ => "Unsupported value type", + } + }), + string str => str, + _ => null + }; + + if (dataLine is { } someDataLine) + { + if (dataLine.IndexOf('\n') >= 0) + { + foreach (var line in dataLine.Split('\n')) + await response.WriteAsync($"data: {line}\n"); + await response.WriteAsync("\n"); + } + else + { + await response.WriteAsync($"data: {someDataLine}\n\n"); + } + } + } + } + catch (OperationCanceledException) // DevSkim: ignore DS176209 TODO investigate why this is needed; + { // seems to "sometimes" crash the process + // ignore // if browser is closed (request aborted). + } + finally + { + clients.Remove(channel.Writer); + } +}); + +app.Run(); + +sealed class SdvEventReadingService : BackgroundService +{ + readonly IntentBrokeringRuntime.IntentBrokeringService.IntentBrokeringServiceClient _client; + readonly IEnumerable> _writers; + readonly SocketsHttpHandler _httpHandler; + readonly ILoggerFactory _loggerFactory; + readonly ILogger _logger; + + public SdvEventReadingService(IntentBrokeringRuntime.IntentBrokeringService.IntentBrokeringServiceClient client, + IEnumerable> writers, + SocketsHttpHandler httpHandler, + ILoggerFactory loggerFactory, + ILogger logger) + { + _client = client; + _writers = writers; + _httpHandler = httpHandler; + _loggerFactory = loggerFactory; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (true) + { + var newClientNoticeChannel = Channel.CreateUnbounded>(); + + void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + if (args.Action is NotifyCollectionChangedAction.Add && args.NewItems?[0] is ChannelWriter writer) + { + _ = Task.Run(cancellationToken: stoppingToken, function: async () => + { + try + { + await newClientNoticeChannel.Writer.WriteAsync(writer, stoppingToken); + } + catch (Exception ex) + { + _logger.LogDebug(ex, null); + } + }); + } + } + + { + if (_writers is INotifyCollectionChanged changeSource) + changeSource.CollectionChanged += OnCollectionChanged; + } + + try + { + IDisposable? d1 = null; + IDisposable? d2 = null; + IDisposable? d3 = null; + + try + { + (d1, var vdtStream) = await StreamAsync(VdtProperties.Namespace, new[] { VdtProperties.CabinTemperature, VdtProperties.BatteryLevel, VdtProperties.AirConditioningState }, stoppingToken); + (d2, var keyValueStoreStream) = await StreamAsync(KeyValueStoreProperties.Namespace, new[] { KeyValueStoreProperties.DogModeStatus }, stoppingToken); + (d3, var cameraStream) = await StreamAsync(SimulatedCameraProperties.Namespace, new[] { SimulatedCameraProperties.DesiredFrequency }, stoppingToken); + + await using var @event = AsyncEnumerableEx.Merge(vdtStream, keyValueStoreStream, cameraStream).GetAsyncEnumerator(stoppingToken); + var nextEventTask = @event.MoveNextAsync(stoppingToken).AsTask(); + var newClientNoticeTask = newClientNoticeChannel.Reader.ReadAsync(stoppingToken).AsTask(); + + while (true) + { + var completedTask = await Task.WhenAny(nextEventTask, newClientNoticeTask); + + while (newClientNoticeChannel.Reader.TryRead(out var writer)) + await writer.WriteAsync("connected", stoppingToken); + + if (completedTask == newClientNoticeTask) + { + var writer = await newClientNoticeTask; + await writer.WriteAsync("connected", stoppingToken); + newClientNoticeTask = newClientNoticeChannel.Reader.ReadAsync(stoppingToken).AsTask(); + } + + if (completedTask == nextEventTask) + { + if (!await nextEventTask) + break; + + foreach (var writer in _writers) + await writer.WriteAsync(@event.Current, stoppingToken); + + nextEventTask = @event.MoveNextAsync(stoppingToken).AsTask(); + } + } + } + finally + { + d1?.Dispose(); + d2?.Dispose(); + d3?.Dispose(); + } + } + catch (Exception ex) when (ex is RpcException { StatusCode: StatusCode.Unavailable or StatusCode.Cancelled } + or OperationCanceledException) + { + _logger.LogDebug(ex, null); + + if (_writers is INotifyCollectionChanged changeSource) + changeSource.CollectionChanged -= OnCollectionChanged; + + foreach (var writer in _writers) + await writer.WriteAsync("disconnected", stoppingToken); + + if (ex is RpcException { StatusCode: StatusCode.Unavailable }) + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } + } + + sealed class DisposableList : IDisposable + { + readonly List _disposables = new(); + + public void Add(IDisposable disposable) => + _disposables.Add(disposable); + + public void Dispose() => _disposables.ForEach(d => d.Dispose()); + } + + async Task<(IDisposable, IAsyncEnumerable)> StreamAsync(string @namespace, IEnumerable sources, CancellationToken cancellationToken) + { + var disposables = new DisposableList(); + + try + { + var streamingAddressCandidates = await _client.FulfillAsync(new IntentBrokeringRuntime.FulfillRequest + { + Namespace = @namespace, + Intent = new IntentBrokeringCommon.Intent { Discover = new IntentBrokeringCommon.DiscoverIntent() } + }, + cancellationToken: cancellationToken); + + var streamingAddress = streamingAddressCandidates.Fulfillment.Discover.Services + .First(s => s.SchemaReference == "intent_brokering.streaming.v1" && s.SchemaKind == "grpc+proto") + .Url; + + var channel = GrpcChannel.ForAddress(streamingAddress, new GrpcChannelOptions + { + LoggerFactory = _loggerFactory, + HttpHandler = _httpHandler + }); + + disposables.Add(channel); + + var streamingClient = new IntentBrokeringStreaming.ChannelService.ChannelServiceClient(channel); + var streamingCall = streamingClient.Open(new IntentBrokeringStreaming.OpenRequest(), cancellationToken: cancellationToken); + disposables.Add(streamingCall); + var channelId = (await streamingCall.GetResponseHeadersAsync(cancellationToken)).Get("x-chariott-channel-id")?.Value ?? + throw new InvalidOperationException("Channel ID not present in response header."); + + using (var timeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + using (var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken)) + _ = await streamingCall.GetResponseHeadersAsync(linkedCancellationTokenSource.Token); + + foreach (var writer in _writers) + await writer.WriteAsync("connected", cancellationToken); + + var rsr = await _client.FulfillAsync(new IntentBrokeringRuntime.FulfillRequest + { + Namespace = @namespace, + Intent = new IntentBrokeringCommon.Intent + { + Subscribe = new IntentBrokeringCommon.SubscribeIntent + { + ChannelId = channelId, + Sources = { sources } + } + } + }, cancellationToken: cancellationToken); + + _logger.LogDebug(rsr.ToString()); + + return (disposables, streamingCall.ResponseStream.ReadAllAsync()); + } + catch (Exception) + { + disposables.Dispose(); + throw; + } + } + + public override Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation($"{nameof(SdvEventReadingService)} is starting."); + return base.StartAsync(cancellationToken); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation($"{nameof(SdvEventReadingService)} is stopping."); + return base.StopAsync(cancellationToken); + } +} + +static class VdtProperties +{ + public const string Namespace = "sdv.vdt"; + public const string CabinTemperature = "Vehicle.Cabin.HVAC.AmbientAirTemperature"; + public const string BatteryLevel = "Vehicle.OBD.HybridBatteryRemaining"; + public const string AirConditioningState = "Vehicle.Cabin.HVAC.IsAirConditioningActive"; +} + +static class KeyValueStoreProperties +{ + public const string Namespace = "sdv.kvs"; + public const string DogModeStatus = "Feature.DogMode.Status"; +} + +static class SimulatedCameraProperties +{ + public const string Namespace = "sdv.camera.simulated"; + public const string DesiredFrequency = "camera.12fpm"; +} + +static class Extensions +{ + public static async Task GetResponseHeadersAsync(this AsyncServerStreamingCall call, CancellationToken cancellationToken) + { + var done = 0; + + await using (cancellationToken.Register(() => + { + if (Interlocked.CompareExchange(ref done, 1, 0) == 0) + call.Dispose(); + })) + { + Metadata metadata; + + try + { + metadata = await call.ResponseHeadersAsync; + } + catch (RpcException ex) when (ex.StatusCode is StatusCode.Cancelled + && cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(null, ex, cancellationToken); + } + + if (Interlocked.CompareExchange(ref done, 1, 0) == 1) + throw new OperationCanceledException(cancellationToken); // lost the race + + return metadata; + } + } +} + +/// +/// An wrapper that provides synchronized access to the +/// underlying list with copy-on-write semantics such that members reading the +/// list provide a snapshot in time. It also allows the collection to be +/// observed for changes via . +/// + +sealed class ConcurrentList : IList, INotifyCollectionChanged +{ + readonly object _lock = new(); + readonly IList _items; + IList? _copy; + + public ConcurrentList() : this(new List()) { } + public ConcurrentList(IList items) => _items = items; + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + IList Items + { + get + { + lock (_lock) + return _copy ??= _items.ToArray(); + } + } + + void Update(Action> action) => Update(action, static (items, action) => action(items)); + + void Update(TArg arg, Action, TArg> action) => + Update(action, arg, static (items, action, arg) => action(items, arg)); + + void Update(T1 arg1, T2 arg2, Action, T1, T2> action) => + Update(action, arg1, arg2, static (items, action, arg1, arg2) => + { + action(items, arg1, arg2); + return 0; + }); + + TResult Update(TArg arg, Func, TArg, TResult> func) => + Update(func, arg, static (items, func, arg) => func(items, arg)); + + TResult Update(T1 arg1, T2 arg2, Func, T1, T2, TResult> func) => + Update(func, arg1, arg2, static (items, func, arg1, arg2) => func(items, arg1, arg2)); + + TResult Update(T1 arg1, T2 arg2, T3 arg3, Func, T1, T2, T3, TResult> func) + { + lock (_lock) + { + _copy = null; + var result = func(_items, arg1, arg2, arg3); + return result; + } + } + + public int Count => Items.Count; + public bool IsReadOnly => _items.IsReadOnly; // unprotected access is okay + + public void Add(T item) + { + Update(item, static (items, item) => items.Add(item)); + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Add, item)); + } + + public void Clear() + { + Update(static items => items.Clear()); + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Reset)); + } + + public bool Remove(T item) + { + var removed = Update(item, static (items, item) => items.Remove(item)); + if (removed) + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Remove, item)); + return removed; + } + + public void Insert(int index, T item) + { + Update(index, item, static (items, index, item) => items.Insert(index, item)); + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Add, item, index)); + } + + public void RemoveAt(int index) + { + var item = Update(index, static (items, index) => + { + var item = items[index]; + items.RemoveAt(index); + return item; + }); + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Remove, item, index)); + } + + public bool Contains(T item) => Items.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => Items.CopyTo(array, arrayIndex); + public int IndexOf(T item) => Items.IndexOf(item); + + public T this[int index] + { + get => Items[index]; + set => Update(index, value, static (items, index, value) => items[index] = value); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator GetEnumerator() => Items.GetEnumerator(); +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/Properties/launchSettings.json b/intent_brokering/dogmode/applications/dog-mode-ui/src/Properties/launchSettings.json new file mode 100644 index 0000000..10647d6 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22759", + "sslPort": 0 + } + }, + "profiles": { + "DogModeDashboard": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5079", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/appsettings.Development.json b/intent_brokering/dogmode/applications/dog-mode-ui/src/appsettings.Development.json new file mode 100644 index 0000000..355238e --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Grpc.Net.Client.Balancer": "Debug" + } + } +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/appsettings.json b/intent_brokering/dogmode/applications/dog-mode-ui/src/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/fa/LICENSE.txt b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/fa/LICENSE.txt new file mode 100644 index 0000000..cc557ec --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/fa/LICENSE.txt @@ -0,0 +1,165 @@ +Fonticons, Inc. (https://fontawesome.com) + +-------------------------------------------------------------------------------- + +Font Awesome Free License + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +-------------------------------------------------------------------------------- + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) + +The Font Awesome Free download is licensed under a Creative Commons +Attribution 4.0 International License and applies to all icons packaged +as SVG and JS file types. + +-------------------------------------------------------------------------------- + +# Fonts: SIL OFL 1.1 License + +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com) +with Reserved Font Name: "Font Awesome". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE +Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting — in part or in whole — any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +-------------------------------------------------------------------------------- + +# Code: MIT License (https://opensource.org/licenses/MIT) + +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +Copyright 2022 Fonticons, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +# Attribution + +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +-------------------------------------------------------------------------------- + +# Brand Icons + +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/fa/js/fontawesome.min.js b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/fa/js/fontawesome.min.js new file mode 100644 index 0000000..cd28abb --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/fa/js/fontawesome.min.js @@ -0,0 +1,6 @@ +/*! + * Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2022 Fonticons, Inc. + */ +!function(){"use strict";function a(a,t){var e,n=Object.keys(a);return Object.getOwnPropertySymbols&&(e=Object.getOwnPropertySymbols(a),t&&(e=e.filter(function(t){return Object.getOwnPropertyDescriptor(a,t).enumerable})),n.push.apply(n,e)),n}function k(n){for(var t=1;tt.length)&&(a=t.length);for(var e=0,n=new Array(a);e>>0;e--;)a[e]=t[e];return a}function Z(t){return t.classList?Q(t.classList):(t.getAttribute("class")||"").split(" ").filter(function(t){return t})}function $(t){return"".concat(t).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function tt(e){return Object.keys(e||{}).reduce(function(t,a){return t+"".concat(a,": ").concat(e[a].trim(),";")},"")}function at(t){return t.size!==G.size||t.x!==G.x||t.y!==G.y||t.rotate!==G.rotate||t.flipX||t.flipY}function et(){var t,a,e=x,n=X.familyPrefix,i=X.replacementClass,r=':host,:root{--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Solid";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Regular";--fa-font-light:normal 300 1em/1 "Font Awesome 6 Light";--fa-font-thin:normal 100 1em/1 "Font Awesome 6 Thin";--fa-font-duotone:normal 900 1em/1 "Font Awesome 6 Duotone";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}svg:not(:host).svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible;box-sizing:content-box}.svg-inline--fa{display:var(--fa-display,inline-block);height:1em;overflow:visible;vertical-align:-.125em}.svg-inline--fa.fa-2xs{vertical-align:.1em}.svg-inline--fa.fa-xs{vertical-align:0}.svg-inline--fa.fa-sm{vertical-align:-.0714285705em}.svg-inline--fa.fa-lg{vertical-align:-.2em}.svg-inline--fa.fa-xl{vertical-align:-.25em}.svg-inline--fa.fa-2xl{vertical-align:-.3125em}.svg-inline--fa.fa-pull-left{margin-right:var(--fa-pull-margin,.3em);width:auto}.svg-inline--fa.fa-pull-right{margin-left:var(--fa-pull-margin,.3em);width:auto}.svg-inline--fa.fa-li{width:var(--fa-li-width,2em);top:.25em}.svg-inline--fa.fa-fw{width:var(--fa-fw-width,1.25em)}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:var(--fa-counter-background-color,#ff253a);border-radius:var(--fa-counter-border-radius,1em);box-sizing:border-box;color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);max-width:var(--fa-counter-max-width,5em);min-width:var(--fa-counter-min-width,1.5em);overflow:hidden;padding:var(--fa-counter-padding,.25em .5em);right:var(--fa-right,0);text-overflow:ellipsis;top:var(--fa-top,0);-webkit-transform:scale(var(--fa-counter-scale,.25));transform:scale(var(--fa-counter-scale,.25));-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:var(--fa-bottom,0);right:var(--fa-right,0);top:auto;-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:var(--fa-bottom,0);left:var(--fa-left,0);right:auto;top:auto;-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{top:var(--fa-top,0);right:var(--fa-right,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:var(--fa-left,0);right:auto;top:var(--fa-top,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:top left;transform-origin:top left}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.0833333337em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.0714285718em;vertical-align:.0535714295em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.0416666682em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width,2em) * -1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-color:var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);border-style:var(--fa-border-style,solid);border-width:var(--fa-border-width,.08em);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1,1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1,1) translateY(0)}100%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1,1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1,1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1,1) translateY(0)}100%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1,1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}24%,8%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}100%,40%{-webkit-transform:rotate(0);transform:rotate(0)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}24%,8%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}100%,40%{-webkit-transform:rotate(0);transform:rotate(0)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;vertical-align:middle;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0;z-index:var(--fa-stack-z-index,auto)}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-sr-only,.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fa-sr-only-focusable:not(:focus),.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor);opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.fa-duotone.fa-inverse,.fad.fa-inverse{color:var(--fa-inverse,#fff)}';return"fa"===n&&i===e||(t=new RegExp("\\.".concat("fa","\\-"),"g"),a=new RegExp("\\--".concat("fa","\\-"),"g"),e=new RegExp("\\.".concat(e),"g"),r=r.replace(t,".".concat(n,"-")).replace(a,"--".concat(n,"-")).replace(e,".".concat(i))),r}var nt=!1;function it(){X.autoAddCss&&!nt&&(function(t){if(t&&p){var a=h.createElement("style");a.setAttribute("type","text/css"),a.innerHTML=t;for(var e=h.head.childNodes,n=null,i=e.length-1;-1").concat(n.map(lt).join(""),"")}function ut(t,a,e){if(t&&t[a]&&t[a][e])return{prefix:a,iconName:e,icon:t[a][e]}}p&&((ct=(h.documentElement.doScroll?/^loaded|^c/:/^loaded|^i|^c/).test(h.readyState))||h.addEventListener("DOMContentLoaded",rt));function mt(t,a,e,n){for(var i,r,o=Object.keys(t),s=o.length,c=void 0!==n?dt(a,n):a,f=void 0===e?(i=1,t[o[0]]):(i=0,e);iC.length)&&(c=C.length);for(var L=0,V=new Array(c);L svg { + font-size: 1vw; +} + +#temp-value { + width: 1.25em; + text-align: right; +} + +#temp-value.warm { + color: #ffd966; +} + +#temp .fa-temperature-half { + padding-left: 1vw; +} + +#deg-c > sup { + font-size: 5vw; + font-style: italic; +} + +#panel { + display: flex; + flex-direction: row; + justify-content: space-around; +} + +#dog, #aircon { + color: #444; +} + +#aircon.on { + color: skyblue; +} + +#dog.on { + color: #70ad47; +} + +#dog.on:hover { + color: limegreen; +} + +#dog:hover { + color: #888; + cursor: pointer; +} + +#panel > div:last-child { + margin-right: 10px; +} + +#panel > div { + display: flex; + align-items: center; + flex: 1; + justify-content: center; + margin: 10px 0 10px 10px; +} + +#battery { + display: flex; + flex-direction: column; +} + +#battery > div:last-child, #battery-level { + font-size: 3vw; +} + +#battery > div:last-child { + margin-top: -2vw; +} + +#panel .fa-temperature-half { + font-size: 7vw; +} + +#camera { + margin: auto; + padding: 10px; +} + +#camera img { + max-width: 100%; + max-height: 90vh; + margin-left: auto; + margin-right: auto; + display: block; +} diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/index.html b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/index.html new file mode 100644 index 0000000..10bc147 --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/index.html @@ -0,0 +1,114 @@ + + + + + + Dog Mode Dashboard + + + + + + +
+
+
+
+ 0°C + +
+
+ +
+
+
+
100%
+
+
+ + diff --git a/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/streaming.html b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/streaming.html new file mode 100644 index 0000000..66cb96f --- /dev/null +++ b/intent_brokering/dogmode/applications/dog-mode-ui/src/wwwroot/streaming.html @@ -0,0 +1,79 @@ + + + + + + Camera streaming + + + + + + + +
+
+ Camera +
+ + + diff --git a/intent_brokering/dogmode/applications/kv-app/Cargo.toml b/intent_brokering/dogmode/applications/kv-app/Cargo.toml new file mode 100644 index 0000000..eca48bd --- /dev/null +++ b/intent_brokering/dogmode/applications/kv-app/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "kv-app" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +ess = { path = "../../../../external/chariott/intent_brokering/ess" } +examples-common = { path = "../../common/" } +keyvalue = { path = "../../../../external/chariott/intent_brokering/keyvalue" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-stream = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } diff --git a/intent_brokering/dogmode/applications/kv-app/README.md b/intent_brokering/dogmode/applications/kv-app/README.md new file mode 100644 index 0000000..96dcc5b --- /dev/null +++ b/intent_brokering/dogmode/applications/kv-app/README.md @@ -0,0 +1,129 @@ +# Key-Value Store Application + +This is an example provider that offers the capability to read from +and write to an in-memory key-value store. It also supports subscribing to +changes in the key store where the events are delivered over an opened +channel. + +## Testing + +Start the Intent Brokering Service followed by this application: + +```bash +cargo run -p intent_brokering & +cargo run -p kv-app & +``` + +Once both are up and running successfully, use the following to write a +key-value pair to the store via the Intent Brokering Service: + +```bash +grpcurl -plaintext -d @ 0.0.0.0:4243 intent_brokering.runtime.v1.IntentBrokeringService/Fulfill <; + +pub struct IntentProvider { + url: Url, + streaming_store: Arc, +} + +impl IntentProvider { + pub fn new(url: Url, streaming_store: Arc) -> Self { + Self { + url, + streaming_store, + } + } + + fn write(&self, intent: WriteIntent) -> Result { + let key = intent.key.into(); + let value = intent + .value + .and_then(|v| v.value) + .ok_or_else(|| Status::unknown("Value must be specified."))?; + self.streaming_store.set(key, value); + Ok(WriteFulfillment {}) + } +} + +#[async_trait] +impl ProviderService for IntentProvider { + async fn fulfill( + &self, + request: Request, + ) -> Result, Status> { + let fulfillment = match request + .into_inner() + .intent + .and_then(|i| i.intent) + .ok_or_else(|| Status::invalid_argument("Intent must be specified."))? + { + IntentEnum::Read(intent) => Ok(self.streaming_store.read(intent)), + IntentEnum::Write(intent) => self.write(intent).map(FulfillmentEnum::Write), + IntentEnum::Subscribe(intent) => self.streaming_store.subscribe(intent), + IntentEnum::Discover(_intent) => Ok(FulfillmentEnum::Discover(DiscoverFulfillment { + services: vec![Service { + url: self.url.to_string(), + schema_kind: "grpc+proto".to_owned(), + schema_reference: "intent_brokering.streaming.v1".to_owned(), + metadata: HashMap::new(), + }], + })), + _ => Err(Status::unknown("Unsupported or unknown intent."))?, + }; + + fulfillment.map(|f| { + Response::new(FulfillResponse { + fulfillment: Some(FulfillmentMessage { + fulfillment: Some(f), + }), + }) + }) + } +} diff --git a/intent_brokering/dogmode/applications/kv-app/src/main.rs b/intent_brokering/dogmode/applications/kv-app/src/main.rs new file mode 100644 index 0000000..2da8c3d --- /dev/null +++ b/intent_brokering/dogmode/applications/kv-app/src/main.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod intent_provider; + +use std::sync::Arc; + +use examples_common::intent_brokering; +use intent_brokering_common::error::Error; +use intent_brokering_common::shutdown::RouterExt as _; +use intent_brokering_proto::{ + provider::provider_service_server::ProviderServiceServer, + runtime::{intent_registration::Intent, intent_service_registration::ExecutionLocality}, + streaming::channel_service_server::ChannelServiceServer, +}; +use tonic::transport::Server; + +use crate::intent_provider::{IntentProvider, StreamingStore}; + +intent_brokering::provider::main!(wain); + +async fn wain() -> Result<(), Error> { + let (url, socket_address) = intent_brokering::provider::register( + "sdv.key-value-store", + "0.0.1", + "sdv.kvs", + [ + Intent::Read, + Intent::Write, + Intent::Subscribe, + Intent::Discover, + ], + "KVS_URL", + "http://0.0.0.0:50064", // DevSkim: ignore DS137138 + ExecutionLocality::Local, + ) + .await?; + + tracing::info!("Application listening on: {url}"); + + let streaming_store = Arc::new(StreamingStore::new()); + let provider = Arc::new(IntentProvider::new( + url.clone(), + Arc::clone(&streaming_store), + )); + + Server::builder() + .add_service(ProviderServiceServer::from_arc(Arc::clone(&provider))) + .add_service(ChannelServiceServer::new(streaming_store.ess().clone())) + .serve_with_ctrl_c_shutdown(socket_address) + .await +} diff --git a/intent_brokering/dogmode/applications/local-object-detection/Cargo.toml b/intent_brokering/dogmode/applications/local-object-detection/Cargo.toml new file mode 100644 index 0000000..e67ba53 --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "local-object-detection-app" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +examples-common = { path = "../../common/" } +image = "0.24.8" +lazy_static = { workspace = true } +ndarray = "0.15.6" +serde = { workspace = true } +serde_json = { workspace = true } +tensorflow = "0.21.0" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-util = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/intent_brokering/dogmode/applications/local-object-detection/README.md b/intent_brokering/dogmode/applications/local-object-detection/README.md new file mode 100644 index 0000000..bb93eba --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/README.md @@ -0,0 +1,40 @@ +# Local object detection application + +This code sample shows you an implementation of object detection based on +TensorFlow and ssd_mobilenet_v1_coco model. + +To run the application: + +1. Start the Intent Brokering runtime by executing `cargo run -p intent_brokering` +2. Start detection application by executing `cargo run` from the + `intent_brokering/dogmode/applications/local-object-detection` directory. +3. In the root directory create a `detect_image.json` file with the following + message structure: + + ```json + { + "intent": { + "invoke": { + "command": "detect", + "args": [ + { + "any": { + "@type": "examples.detection.v1.DetectRequest", + "blob": { + "media_type": "image/jpg", + "bytes": "base64 encoding of the image" + } + } + } + ] + } + }, + "namespace": "sdv.detection" + } + ``` + +4. Execute detection with `grpcurl -v -plaintext -import-path proto/ \ + -import-path intent_brokering/dogmode/applications/proto -use-reflection -proto \ + intent_brokering/dogmode/applications/proto/examples/detection/v1/detection.proto -d @ \ + localhost:4243 intent_brokering.runtime.v1.IntentBrokeringService/Fulfill < \ + detect_image.json` diff --git a/intent_brokering/dogmode/applications/local-object-detection/models/categories.json b/intent_brokering/dogmode/applications/local-object-detection/models/categories.json new file mode 100644 index 0000000..17274a5 --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/models/categories.json @@ -0,0 +1,402 @@ +[ + { + "supercategory": "person", + "id": 1, + "name": "person" + }, + { + "supercategory": "vehicle", + "id": 2, + "name": "bicycle" + }, + { + "supercategory": "vehicle", + "id": 3, + "name": "car" + }, + { + "supercategory": "vehicle", + "id": 4, + "name": "motorcycle" + }, + { + "supercategory": "vehicle", + "id": 5, + "name": "airplane" + }, + { + "supercategory": "vehicle", + "id": 6, + "name": "bus" + }, + { + "supercategory": "vehicle", + "id": 7, + "name": "train" + }, + { + "supercategory": "vehicle", + "id": 8, + "name": "truck" + }, + { + "supercategory": "vehicle", + "id": 9, + "name": "boat" + }, + { + "supercategory": "outdoor", + "id": 10, + "name": "traffic light" + }, + { + "supercategory": "outdoor", + "id": 11, + "name": "fire hydrant" + }, + { + "supercategory": "outdoor", + "id": 13, + "name": "stop sign" + }, + { + "supercategory": "outdoor", + "id": 14, + "name": "parking meter" + }, + { + "supercategory": "outdoor", + "id": 15, + "name": "bench" + }, + { + "supercategory": "animal", + "id": 16, + "name": "bird" + }, + { + "supercategory": "animal", + "id": 17, + "name": "cat" + }, + { + "supercategory": "animal", + "id": 18, + "name": "dog" + }, + { + "supercategory": "animal", + "id": 19, + "name": "horse" + }, + { + "supercategory": "animal", + "id": 20, + "name": "sheep" + }, + { + "supercategory": "animal", + "id": 21, + "name": "cow" + }, + { + "supercategory": "animal", + "id": 22, + "name": "elephant" + }, + { + "supercategory": "animal", + "id": 23, + "name": "bear" + }, + { + "supercategory": "animal", + "id": 24, + "name": "zebra" + }, + { + "supercategory": "animal", + "id": 25, + "name": "giraffe" + }, + { + "supercategory": "accessory", + "id": 27, + "name": "backpack" + }, + { + "supercategory": "accessory", + "id": 28, + "name": "umbrella" + }, + { + "supercategory": "accessory", + "id": 31, + "name": "handbag" + }, + { + "supercategory": "accessory", + "id": 32, + "name": "tie" + }, + { + "supercategory": "accessory", + "id": 33, + "name": "suitcase" + }, + { + "supercategory": "sports", + "id": 34, + "name": "frisbee" + }, + { + "supercategory": "sports", + "id": 35, + "name": "skis" + }, + { + "supercategory": "sports", + "id": 36, + "name": "snowboard" + }, + { + "supercategory": "sports", + "id": 37, + "name": "sports ball" + }, + { + "supercategory": "sports", + "id": 38, + "name": "kite" + }, + { + "supercategory": "sports", + "id": 39, + "name": "baseball bat" + }, + { + "supercategory": "sports", + "id": 40, + "name": "baseball glove" + }, + { + "supercategory": "sports", + "id": 41, + "name": "skateboard" + }, + { + "supercategory": "sports", + "id": 42, + "name": "surfboard" + }, + { + "supercategory": "sports", + "id": 43, + "name": "tennis racket" + }, + { + "supercategory": "kitchen", + "id": 44, + "name": "bottle" + }, + { + "supercategory": "kitchen", + "id": 46, + "name": "wine glass" + }, + { + "supercategory": "kitchen", + "id": 47, + "name": "cup" + }, + { + "supercategory": "kitchen", + "id": 48, + "name": "fork" + }, + { + "supercategory": "kitchen", + "id": 49, + "name": "knife" + }, + { + "supercategory": "kitchen", + "id": 50, + "name": "spoon" + }, + { + "supercategory": "kitchen", + "id": 51, + "name": "bowl" + }, + { + "supercategory": "food", + "id": 52, + "name": "banana" + }, + { + "supercategory": "food", + "id": 53, + "name": "apple" + }, + { + "supercategory": "food", + "id": 54, + "name": "sandwich" + }, + { + "supercategory": "food", + "id": 55, + "name": "orange" + }, + { + "supercategory": "food", + "id": 56, + "name": "broccoli" + }, + { + "supercategory": "food", + "id": 57, + "name": "carrot" + }, + { + "supercategory": "food", + "id": 58, + "name": "hot dog" + }, + { + "supercategory": "food", + "id": 59, + "name": "pizza" + }, + { + "supercategory": "food", + "id": 60, + "name": "donut" + }, + { + "supercategory": "food", + "id": 61, + "name": "cake" + }, + { + "supercategory": "furniture", + "id": 62, + "name": "chair" + }, + { + "supercategory": "furniture", + "id": 63, + "name": "couch" + }, + { + "supercategory": "furniture", + "id": 64, + "name": "potted plant" + }, + { + "supercategory": "furniture", + "id": 65, + "name": "bed" + }, + { + "supercategory": "furniture", + "id": 67, + "name": "dining table" + }, + { + "supercategory": "furniture", + "id": 70, + "name": "toilet" + }, + { + "supercategory": "electronic", + "id": 72, + "name": "tv" + }, + { + "supercategory": "electronic", + "id": 73, + "name": "laptop" + }, + { + "supercategory": "electronic", + "id": 74, + "name": "mouse" + }, + { + "supercategory": "electronic", + "id": 75, + "name": "remote" + }, + { + "supercategory": "electronic", + "id": 76, + "name": "keyboard" + }, + { + "supercategory": "electronic", + "id": 77, + "name": "cell phone" + }, + { + "supercategory": "appliance", + "id": 78, + "name": "microwave" + }, + { + "supercategory": "appliance", + "id": 79, + "name": "oven" + }, + { + "supercategory": "appliance", + "id": 80, + "name": "toaster" + }, + { + "supercategory": "appliance", + "id": 81, + "name": "sink" + }, + { + "supercategory": "appliance", + "id": 82, + "name": "refrigerator" + }, + { + "supercategory": "indoor", + "id": 84, + "name": "book" + }, + { + "supercategory": "indoor", + "id": 85, + "name": "clock" + }, + { + "supercategory": "indoor", + "id": 86, + "name": "vase" + }, + { + "supercategory": "indoor", + "id": 87, + "name": "scissors" + }, + { + "supercategory": "indoor", + "id": 88, + "name": "teddy bear" + }, + { + "supercategory": "indoor", + "id": 89, + "name": "hair drier" + }, + { + "supercategory": "indoor", + "id": 90, + "name": "toothbrush" + } +] diff --git a/intent_brokering/dogmode/applications/local-object-detection/models/readme.txt b/intent_brokering/dogmode/applications/local-object-detection/models/readme.txt new file mode 100644 index 0000000..44bf92c --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/models/readme.txt @@ -0,0 +1,2 @@ +Model used in this folder is 'ssd_mobilenet_v2_coco' taken from +https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/tf1_detection_zoo.md. diff --git a/intent_brokering/dogmode/applications/local-object-detection/models/ssd_mobilenet_v2_coco.pb b/intent_brokering/dogmode/applications/local-object-detection/models/ssd_mobilenet_v2_coco.pb new file mode 100644 index 0000000..0b2a1ba --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/models/ssd_mobilenet_v2_coco.pb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a8d8a89d695842e60d8c6d144181100555563e21acf2fa1e8f561fec5c3c6ad +size 69688296 diff --git a/intent_brokering/dogmode/applications/local-object-detection/src/detection.rs b/intent_brokering/dogmode/applications/local-object-detection/src/detection.rs new file mode 100644 index 0000000..802ab19 --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/src/detection.rs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use examples_common::examples::detection::{DetectRequest, DetectResponse, DetectionObject}; +use image::{io::Reader, DynamicImage, GenericImageView}; +use intent_brokering_common::error::Error; +use ndarray::prelude::Array; +use serde::{Deserialize, Serialize}; +use std::{ + env::var, + fs::File, + io::{BufReader, Cursor}, +}; +use tensorflow::{Graph, ImportGraphDefOptions, Session, SessionOptions, SessionRunArgs, Tensor}; + +const INVALID_IMAGE_FORMAT: &str = "Could not identify image format"; +const INVALID_IMAGE: &str = "Could not decode image"; +const NO_MATCHING_CATEGORY: &str = "NO_MATCHING_CATEGORY"; + +pub struct DetectionLogic { + graph: Graph, +} + +impl DetectionLogic { + pub fn new() -> Self { + let mut graph = Graph::new(); + let proto = include_bytes!("../models/ssd_mobilenet_v2_coco.pb"); + + graph + .import_graph_def(proto, &ImportGraphDefOptions::new()) + .unwrap(); + + Self { graph } + } + + pub fn detect_local(&self, body: DetectRequest) -> Result { + // Get image into DynamicImage type + let image = Reader::new(Cursor::new(Vec::::from(body))) + .with_guessed_format() + .map_err(|_| Error::new(INVALID_IMAGE_FORMAT))? + .decode() + .map_err(|_| Error::new(INVALID_IMAGE))?; + + let result = detect_local_inner(&self.graph, image); + return result.map_err(|e| Error::from_error(e.to_string(), e)); + + fn detect_local_inner( + graph: &Graph, + image: DynamicImage, + ) -> Result> { + // Build ndarray + let (width, height) = image.dimensions(); + let image_array_expanded = + Array::from_shape_vec((height as usize, width as usize, 3), image.into_bytes())? + .insert_axis(ndarray::Axis(0)); + + let image_tensor_op = graph.operation_by_name_required("image_tensor")?; + let input_image_tensor = Tensor::new(&[1, height as u64, width as u64, 3]) + .with_values(image_array_expanded.as_slice().unwrap())?; + let mut session_args = SessionRunArgs::new(); + session_args.add_feed(&image_tensor_op, 0, &input_image_tensor); + + let classes = graph.operation_by_name_required("detection_classes")?; + let classes_token = session_args.request_fetch(&classes, 0); + + let scores = graph.operation_by_name_required("detection_scores")?; + let scores_token = session_args.request_fetch(&scores, 0); + + // Run detection session + let detection_session = Session::new(&SessionOptions::new(), graph)?; + detection_session.run(&mut session_args)?; + + // Parse detection session results + let classes_tensor = session_args.fetch::(classes_token)?; + let scores_tensor = session_args.fetch::(scores_token)?; + + // Collect results and map to human readable categories + let categories = get_categories()?; + let classes_categories = + classes_tensor + .iter() + .map(|v| match categories.iter().find(|c| c.id.eq(v)) { + Some(c) => c.name.clone(), + None => NO_MATCHING_CATEGORY.to_owned(), + }); + + Ok(DetectResponse::new( + scores_tensor + .iter() + .zip(classes_categories) + .filter(|(&score, _)| score > 0.0) + .map(|(&score, category)| DetectionObject::new(category, score.into())) + .collect(), + )) + } + + fn get_categories() -> Result, Box> { + const DEFAULT_CATEGORIES_FILE_PATH: &str = "./models/categories.json"; + const CATEGORIES_FILE_PATH_ENV_NAME: &str = "CATEGORIES_FILE_PATH"; + + let file = File::open( + var(CATEGORIES_FILE_PATH_ENV_NAME) + .unwrap_or_else(|_| DEFAULT_CATEGORIES_FILE_PATH.to_owned()), + )?; + let reader = BufReader::new(file); + let result = serde_json::from_reader(reader)?; + Ok(result) + } + } +} + +impl Default for DetectionLogic { + fn default() -> Self { + Self::new() + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Category { + supercategory: String, + name: String, + id: f32, +} diff --git a/intent_brokering/dogmode/applications/local-object-detection/src/intent_provider.rs b/intent_brokering/dogmode/applications/local-object-detection/src/intent_provider.rs new file mode 100644 index 0000000..fa67ab7 --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/src/intent_provider.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use async_trait::async_trait; +use intent_brokering_proto::{ + common::{FulfillmentEnum, FulfillmentMessage, IntentEnum}, + provider::{provider_service_server::ProviderService, FulfillRequest, FulfillResponse}, +}; +use tonic::{Request, Response, Status}; +use tracing::error; + +use crate::detection::DetectionLogic; + +use examples_common::{ + examples::detection::DetectRequest, + intent_brokering::inspection::{fulfill, Entry}, +}; + +pub struct IntentProvider { + internal_logic: DetectionLogic, +} + +impl IntentProvider { + pub fn new() -> Self { + let internal_logic = DetectionLogic::new(); + Self { internal_logic } + } +} + +lazy_static::lazy_static! { + static ref INSPECT_FULFILLMENT_SCHEMA: Vec = vec![ + Entry::new("detect", [ + ("member_type", "command"), + ("request", "examples.detection.v1.DetectRequest"), + ("response", "examples.detection.v1.DetectResponse"), + ]) + ]; +} + +#[async_trait] +impl ProviderService for IntentProvider { + async fn fulfill( + &self, + request: Request, + ) -> Result, Status> { + let response = match request + .into_inner() + .intent + .and_then(|i| i.intent) + .ok_or_else(|| Status::invalid_argument("Intent must be specified"))? + { + IntentEnum::Inspect(inspect) => fulfill(inspect.query, &*INSPECT_FULFILLMENT_SCHEMA), + IntentEnum::Invoke(intent) => { + let arg = DetectRequest::try_from(intent) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + + let result = self.internal_logic.detect_local(arg).map_err(|e| { + error!("Error when running detection: '{e:?}'."); + Status::unknown(format!("Error when invoking function: '{}'", e)) + })?; + + FulfillmentEnum::Invoke(result.into()) + } + _ => Err(Status::not_found(""))?, + }; + + Ok(Response::new(FulfillResponse { + fulfillment: Some(FulfillmentMessage { + fulfillment: Some(response), + }), + })) + } +} diff --git a/intent_brokering/dogmode/applications/local-object-detection/src/main.rs b/intent_brokering/dogmode/applications/local-object-detection/src/main.rs new file mode 100644 index 0000000..7f69186 --- /dev/null +++ b/intent_brokering/dogmode/applications/local-object-detection/src/main.rs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod detection; +mod intent_provider; + +use examples_common::intent_brokering; +use intent_brokering_common::error::Error; +use intent_brokering_common::shutdown::RouterExt as _; +use intent_brokering_proto::{ + provider::provider_service_server::ProviderServiceServer, + runtime::{intent_registration::Intent, intent_service_registration::ExecutionLocality}, +}; +use tonic::transport::Server; + +use crate::intent_provider::IntentProvider; + +intent_brokering::provider::main!(wain); + +async fn wain() -> Result<(), Error> { + let (url, socket_address) = intent_brokering::provider::register( + "sdv.local-detection", + "0.0.1", + "sdv.detection", + [Intent::Inspect, Intent::Invoke], + "LOCAL_DETECTION_URL", + "http://0.0.0.0:50061", // DevSkim: ignore DS137138 + ExecutionLocality::Local, + ) + .await?; + + tracing::info!("Application application listening: {url}"); + + Server::builder() + .add_service(ProviderServiceServer::new(IntentProvider::new())) + .serve_with_ctrl_c_shutdown(socket_address) + .await +} diff --git a/intent_brokering/dogmode/applications/mock-vas/Cargo.toml b/intent_brokering/dogmode/applications/mock-vas/Cargo.toml new file mode 100644 index 0000000..b1d2df2 --- /dev/null +++ b/intent_brokering/dogmode/applications/mock-vas/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "mock-vas" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +anyhow = { workspace = true } +async-std = "1.12" +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +ess = { path = "../../../../external/chariott/intent_brokering/ess" } +examples-common = { path = "../../common/" } +futures = { workspace = true } +lazy_static = { workspace = true } +regex = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-stream = { workspace = true } +tokio-util = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } +env_logger = "*" +test-log = "0.2.14" +tokio-test = "0.4.3" + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/intent_brokering/dogmode/applications/mock-vas/README.md b/intent_brokering/dogmode/applications/mock-vas/README.md new file mode 100644 index 0000000..8f270f5 --- /dev/null +++ b/intent_brokering/dogmode/applications/mock-vas/README.md @@ -0,0 +1,63 @@ +# Mock Vehicle Abstraction Service (VAS) + +This code sample shows you how to use the mocked VAS for the dog mode scenario. +The dog mode allows a car owner to keep their dog safe, while they are away from +the car. + +## How-to consume a streaming service using Chariott Intent Broker + +> As an application developer, I want to consume events from a streaming +> service. + +The example application [dog-mode-logic](../dog-mode-logic/) showcases how to +achieve this using a Rust gRPC client. In this section we show how to do this +using [gRPCurl](https://github.com/fullstorydev/grpcurl) calls from the command line. + +From the root directory: + +1. In a terminal (A) start Intent Brokering with `cargo run -p intent_brokering`. +2. In another terminal (B) start the mock-vas with `cargo run -p mock-vas`. +3. In a terminal (C), open a channel to the mock-vas with `grpcurl -v -plaintext \ + -import-path proto -proto proto/intent_brokering/streaming/v1/streaming.proto \ + localhost:50051 intent_brokering.streaming.v1.ChannelService/Open` and take a note of + the returned channel id in the metadata _x-chariott-channel-id_. +4. In another terminal D call the following, using the channel id from the + previous step: + + ```shell + grpcurl -v -plaintext -d @ localhost:4243 intent_brokering.runtime.v1.IntentBrokeringService/Fulfill < As a provider developer, I want to create a streaming service for events. + +In order to do so, you need to: + +- Implement the [streaming proto](https://github.com/eclipse-chariott/chariott/blob/main/intent_brokering/proto/intent_brokering/streaming/v1/streaming.proto) + and specifically the `OpenRequest` endpoint with a service. + - This is done in the common examples library in [streaming.rs](../../common/src/intent_brokering/streaming.rs) + - Make sure to serve this service with your gRPC server. +- The application will send `SubscribeIntent` that your service would need to + handle. + - In order to create the required client and register the subscriptions, you + can use the + [Event Sub System crate aka ESS crate](https://github.com/eclipse-chariott/chariott/tree/main/intent_brokering/ess). + - This is done in mock-vas in [intent_provider.rs](./src/intent_provider.rs) diff --git a/intent_brokering/dogmode/applications/mock-vas/src/communication.rs b/intent_brokering/dogmode/applications/mock-vas/src/communication.rs new file mode 100644 index 0000000..1ad0565 --- /dev/null +++ b/intent_brokering/dogmode/applications/mock-vas/src/communication.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use futures::future::join_all; +use intent_brokering_common::shutdown::{ctrl_c_cancellation, RouterExt}; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::spawn; +use url::Url; + +use tonic::transport::Server; + +use intent_brokering_common::error::{Error, ResultExt as _}; +use intent_brokering_proto::{ + provider::provider_service_server::ProviderServiceServer, + streaming::channel_service_server::ChannelServiceServer, +}; + +use crate::intent_provider::{IntentProvider, StreamingStore}; +use crate::simulation::VehicleSimulation; + +pub async fn serve(url: Url, address: SocketAddr) -> Result<(), Error> { + let streaming_store = Arc::new(StreamingStore::new()); + let simulation = VehicleSimulation::new(Arc::clone(&streaming_store)); + let provider = IntentProvider::new(url, simulation.clone(), Arc::clone(&streaming_store)); + + let cancellation_token = ctrl_c_cancellation(); + let server_token = cancellation_token.child_token(); + + let simulation_handle = spawn(async move { + let result = simulation + .execute(cancellation_token.child_token()) + .await + .map_err(|e| Error::from_error("Error when executing simulation.", e.into())); + + // If the simulation terminates, we shut down the entire program. In + // case the simulation exited for a different reason, this will cause + // the cancellation token to be canceled again, which will have no + // effect. + cancellation_token.cancel(); + + result + }); + + let server_handle = spawn( + Server::builder() + .add_service(ProviderServiceServer::new(provider)) + .add_service(ChannelServiceServer::new(streaming_store.ess().clone())) + .serve_with_cancellation(address, server_token), + ); + + for result in join_all([simulation_handle, server_handle]).await { + result.map_err_with("Joining the handle failed.")??; + } + + Ok(()) +} diff --git a/intent_brokering/dogmode/applications/mock-vas/src/intent_provider.rs b/intent_brokering/dogmode/applications/mock-vas/src/intent_provider.rs new file mode 100644 index 0000000..8f1cc84 --- /dev/null +++ b/intent_brokering/dogmode/applications/mock-vas/src/intent_provider.rs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; +use std::sync::Arc; +use std::vec; + +use async_trait::async_trait; +use examples_common::intent_brokering; +use examples_common::intent_brokering::inspection::{fulfill, Entry}; +use examples_common::intent_brokering::streaming::ProtoExt as _; +use examples_common::intent_brokering::value::Value; +use intent_brokering_proto::{ + common::{ + discover_fulfillment::Service, DiscoverFulfillment, FulfillmentEnum, FulfillmentMessage, + IntentEnum, InvokeFulfillment, ValueMessage, + }, + provider::{provider_service_server::ProviderService, FulfillRequest, FulfillResponse}, +}; +use tonic::{Request, Response, Status}; +use url::Url; + +use crate::simulation::VehicleSimulation; + +pub const CABIN_TEMPERATURE_PROPERTY: &str = "Vehicle.Cabin.HVAC.AmbientAirTemperature"; +pub const BATTERY_LEVEL_PROPERTY: &str = "Vehicle.OBD.HybridBatteryRemaining"; +pub const AIR_CONDITIONING_STATE_PROPERTY: &str = "Vehicle.Cabin.HVAC.IsAirConditioningActive"; +pub const ACTIVATE_AIR_CONDITIONING_COMMAND: &str = "Vehicle.Cabin.HVAC.IsAirConditioningActive"; +pub const SEND_NOTIFICATION_COMMAND: &str = "send_notification"; +pub const SET_UI_MESSAGE_COMMAND: &str = "set_ui_message"; + +const SCHEMA_VERSION_STREAMING: &str = "intent_brokering.streaming.v1"; +const SCHEMA_REFERENCE: &str = "grpc+proto"; + +pub type StreamingStore = intent_brokering::streaming::StreamingStore; + +pub struct IntentProvider { + url: Url, + vehicle_simulation: VehicleSimulation, + streaming_store: Arc, +} + +impl IntentProvider { + pub fn new( + url: Url, + simulation: VehicleSimulation, + streaming_store: Arc, + ) -> Self { + Self { + url, + vehicle_simulation: simulation, + streaming_store, + } + } +} + +lazy_static::lazy_static! { + static ref VDT_SCHEMA: Vec = vec![ + property(CABIN_TEMPERATURE_PROPERTY, "int32"), + property(BATTERY_LEVEL_PROPERTY, "int32"), + property(AIR_CONDITIONING_STATE_PROPERTY, "bool"), + command(ACTIVATE_AIR_CONDITIONING_COMMAND, "IAcmeAirconControl"), + command(SEND_NOTIFICATION_COMMAND, "ISendNotification"), + command(SET_UI_MESSAGE_COMMAND, "ISetUiMessage"), + ]; +} + +fn property(path: &str, r#type: &str) -> Entry { + Entry::new( + path, + [ + ("member_type", "property".into()), + ("type", r#type.into()), + ("read", Value::TRUE), + ("write", Value::FALSE), + ("watch", Value::TRUE), + ], + ) +} + +fn command(path: &str, r#type: &str) -> Entry { + Entry::new(path, [("member_type", "command"), ("type", r#type)]) +} + +#[async_trait] +impl ProviderService for IntentProvider { + async fn fulfill( + &self, + request: Request, + ) -> Result, Status> { + let response = match request + .into_inner() + .intent + .and_then(|i| i.intent) + .ok_or_else(|| Status::invalid_argument("Intent must be specified"))? + { + IntentEnum::Discover(_) => FulfillmentEnum::Discover(DiscoverFulfillment { + services: vec![Service { + url: self.url.to_string(), + schema_kind: SCHEMA_REFERENCE.to_owned(), + schema_reference: SCHEMA_VERSION_STREAMING.to_owned(), + metadata: HashMap::new(), + }], + }), + IntentEnum::Invoke(intent) => { + let args = intent + .args + .into_iter() + .map(|arg| arg.try_into()) + .collect::, ()>>() + .map_err(|_| Status::invalid_argument("Invalid argument."))?; + + let result = self + .vehicle_simulation + .invoke(&intent.command, args) + .await + .map_err(|e| { + Status::unknown(format!("Error when invoking hardware function: '{}'", e)) + })? + .into(); + + FulfillmentEnum::Invoke(InvokeFulfillment { + r#return: Some(ValueMessage { + value: Some(result), + }), + }) + } + IntentEnum::Inspect(inspect) => fulfill(inspect.query, &*VDT_SCHEMA), + IntentEnum::Subscribe(subscribe) => self.streaming_store.subscribe(subscribe)?, + IntentEnum::Read(read) => self.streaming_store.read(read), + _ => return Err(Status::unknown("Unknown or unsupported intent!")), + }; + + Ok(Response::new(FulfillResponse { + fulfillment: Some(FulfillmentMessage { + fulfillment: Some(response), + }), + })) + } +} diff --git a/intent_brokering/dogmode/applications/mock-vas/src/main.rs b/intent_brokering/dogmode/applications/mock-vas/src/main.rs new file mode 100644 index 0000000..ea04fd6 --- /dev/null +++ b/intent_brokering/dogmode/applications/mock-vas/src/main.rs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod communication; +mod intent_provider; +mod simulation; + +use examples_common::intent_brokering; +use intent_brokering_common::error::Error; +use intent_brokering_proto::runtime::{ + intent_registration::Intent, intent_service_registration::ExecutionLocality, +}; + +intent_brokering::provider::main!(wain); + +async fn wain() -> Result<(), Error> { + let (url, socket_address) = intent_brokering::provider::register( + "sdv.mock-vas", + "0.0.1", + "sdv.vdt", + [ + Intent::Discover, + Intent::Invoke, + Intent::Inspect, + Intent::Subscribe, + Intent::Read, + ], + "VAS_URL", + "http://0.0.0.0:50051", // DevSkim: ignore DS137138 + ExecutionLocality::Local, + ) + .await?; + + tracing::info!("Application listening on: {url}"); + + communication::serve(url, socket_address).await +} diff --git a/intent_brokering/dogmode/applications/mock-vas/src/simulation.rs b/intent_brokering/dogmode/applications/mock-vas/src/simulation.rs new file mode 100644 index 0000000..63668f9 --- /dev/null +++ b/intent_brokering/dogmode/applications/mock-vas/src/simulation.rs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use examples_common::intent_brokering::value::Value; +use intent_brokering_common::error::{Error, ResultExt}; +use std::{env, sync::Arc}; +use tokio::sync::broadcast::{self, Sender}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info}; + +use crate::intent_provider::{ + StreamingStore, ACTIVATE_AIR_CONDITIONING_COMMAND, AIR_CONDITIONING_STATE_PROPERTY, + BATTERY_LEVEL_PROPERTY, CABIN_TEMPERATURE_PROPERTY, SEND_NOTIFICATION_COMMAND, + SET_UI_MESSAGE_COMMAND, +}; + +const DEFAULT_COMMAND_CHANNEL_SIZE: usize = 200; + +#[derive(Clone)] +pub struct VehicleSimulation { + key_value_store: Arc, + cmd_sender: Sender, +} + +impl VehicleSimulation { + pub fn new(key_value_store: Arc) -> Self { + let command_channel_size = env::var("COMMAND_CHANNEL_SIZE") + .map(|s| s.parse::().unwrap()) + .unwrap_or(DEFAULT_COMMAND_CHANNEL_SIZE); + + let (cmd_sender, _) = broadcast::channel(command_channel_size); + + VehicleSimulation { + key_value_store, + cmd_sender, + } + } + + pub async fn execute( + &self, + cancellation_token: CancellationToken, + ) -> Result<(), anyhow::Error> { + let mut vehicle_state = VehicleState::new(); + let (done_tx, mut done_rx) = broadcast::channel(1); + // Ensure cancellation does not leak via cancellation_token. If we do + // not create a child but use the passed cancellation_token directly to + // cancel the handle_input, the caller might get notified of "internal" + // cancellations. + let scoped_shutdown_token = cancellation_token.child_token(); + + let input_handle = tokio::task::spawn(handle_input( + self.cmd_sender.clone(), + scoped_shutdown_token.clone(), + Done(done_tx), + )); + + tokio::pin!(input_handle); + + enum LoopBreaker { + Publisher(anyhow::Error), + Handler(Result, tokio::task::JoinError>), + } + + let mut cmd_receiver = self.cmd_sender.subscribe(); + + let res = loop { + tokio::select! { + command = cmd_receiver.recv() => { + if let Ok(command) = command { + match command { + Action::Temperature(value) => vehicle_state.temperature = value, + Action::BatteryLevel(value) => vehicle_state.battery_level = value, + Action::AirConditioning(value) => vehicle_state.air_conditioning_enabled = value, + } + if let Err(err) = self.publish_data(&vehicle_state) { + break Some(LoopBreaker::Publisher(err)); + } + } else { + break None; + } + } + res = &mut input_handle => { + break Some(LoopBreaker::Handler(res)) + } + _ = scoped_shutdown_token.cancelled() => { + break Some(LoopBreaker::Handler(Ok(Ok(())))) + } + } + }; + + scoped_shutdown_token.cancel(); + debug!("Waiting for all tasks to shutdown."); + + _ = done_rx.recv().await.unwrap_err(); + debug!("Shutdown complete."); + + use LoopBreaker::*; + + match res { + Some(Publisher(err)) => Err(err), + Some(Handler(Ok(ok @ Ok(_)))) => ok, + Some(Handler(Ok(err @ Err(_)))) => err, + Some(Handler(Err(err))) => Err(err.into()), + None => Ok(()), + } + } + + fn publish_data(&self, vehicle_state: &VehicleState) -> Result<(), anyhow::Error> { + let publish = |event_id: &str, data: Value| { + self.key_value_store.set(event_id.into(), data); + }; + + publish(CABIN_TEMPERATURE_PROPERTY, vehicle_state.temperature.into()); + publish( + AIR_CONDITIONING_STATE_PROPERTY, + vehicle_state.air_conditioning_enabled.into(), + ); + publish(BATTERY_LEVEL_PROPERTY, vehicle_state.battery_level.into()); + + Ok(()) + } + + pub async fn invoke(&self, command: &str, args: Vec) -> Result { + let action = match (command, args.as_slice()) { + (ACTIVATE_AIR_CONDITIONING_COMMAND, [value]) => { + let value = value + .to_bool() + .map_err(|_| Error::new("Argument must be of type 'Bool'."))?; + info!("Set air conditioning: {}", value); + Ok(Some(Action::AirConditioning(value))) + } + (SEND_NOTIFICATION_COMMAND, [value]) => { + let value = value + .as_str() + .map_err(|_| Error::new("Argument must be of type 'String'."))?; + info!("Sending notification: {}", value); + Ok(None) + } + (SET_UI_MESSAGE_COMMAND, [value]) => { + let value = value + .as_str() + .map_err(|_| Error::new("Argument must be of type 'String'."))?; + info!("Setting message in UI: {}", value); + Ok(None) + } + _ => Err(Error::new( + "No command found which accepts the invocation arguments.", + )), + }?; + + if let Some(action) = action { + self.cmd_sender + .send(action) + .map_err_with("Error when sending a command.")?; + } + + Ok(Value::TRUE) + } +} + +// Emulates the state of a car: +// Function invocations cause the state to update. +// "Emulation" causes the state to update (e.g. battery drains over time, temperature changes over time). +struct VehicleState { + temperature: i32, + battery_level: i32, + air_conditioning_enabled: bool, +} + +impl VehicleState { + fn new() -> Self { + Self { + temperature: 20, + battery_level: 100, + air_conditioning_enabled: false, + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Action { + Temperature(i32), + BatteryLevel(i32), + AirConditioning(bool), +} + +async fn handle_input( + sender: Sender, + shutdown_token: CancellationToken, + _done: Done, +) -> Result<(), anyhow::Error> { + use async_std::{ + io::{prelude::BufReadExt, stdin, BufReader}, + stream::StreamExt, + }; + use Action::*; + + info!("-- Data update input ready --"); + + let stdin = BufReader::new(stdin()); + let mut lines = stdin.lines(); + + loop { + let input = tokio::select! { + line = lines.next() => line, + _ = shutdown_token.cancelled() => break, + }; + + if let Some(input) = input { + let input = input?; + let input_list: Vec<&str> = input.split(' ').collect(); + let data_type = input_list[0].to_lowercase(); + if let Some(b'#') = data_type.as_bytes().first() { + continue; + } + let data: Box = input_list[1].to_lowercase().trim().into(); + + let command = match data_type.as_str() { + "temp" => Temperature(str::parse::(&data).unwrap_or(25)), + "air_conditioning" => AirConditioning(data.as_ref() == "on"), + "battery" => BatteryLevel(str::parse::(&data).unwrap_or(100)), + _ => { + info!("No data update triggered, due to wrong input"); + continue; + } + }; + + sender.send(command)?; + } else { + break; + } + } + + debug!("Shutting down input handler."); + Ok(()) +} + +struct Done(broadcast::Sender<()>); diff --git a/intent_brokering/dogmode/applications/proto/buf.yaml b/intent_brokering/dogmode/applications/proto/buf.yaml new file mode 100644 index 0000000..7a511e8 --- /dev/null +++ b/intent_brokering/dogmode/applications/proto/buf.yaml @@ -0,0 +1,9 @@ +version: v1 +breaking: + use: + - FILE +lint: + use: + - DEFAULT + except: + - ENUM_ZERO_VALUE_SUFFIX diff --git a/intent_brokering/dogmode/applications/proto/examples/detection/v1/detection.proto b/intent_brokering/dogmode/applications/proto/examples/detection/v1/detection.proto new file mode 100644 index 0000000..1b89b3b --- /dev/null +++ b/intent_brokering/dogmode/applications/proto/examples/detection/v1/detection.proto @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +syntax = "proto3"; + +package examples.detection.v1; + +import "intent_brokering/common/v1/common.proto"; + +message DetectRequest { + intent_brokering.common.v1.Blob blob = 1; +} + +message DetectResponse { + repeated DetectEntry entries = 1; +} + +message DetectEntry { + string object = 1; + double confidence = 2; +} diff --git a/intent_brokering/dogmode/applications/run_demo.sh b/intent_brokering/dogmode/applications/run_demo.sh new file mode 100755 index 0000000..ffed643 --- /dev/null +++ b/intent_brokering/dogmode/applications/run_demo.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +set -e +cd "$(dirname "$0")/../../.." + +if [[ "$1" == "-h" || "$1" == "--help" ]]; then + echo 'Run Intent Brokering DogMode demo. + +This script allows you to specify the following parameters: + +- Cognitive services endpoint (--cognitive_endpoint) +- Cognitive services key (--cognitive_key) + +If you specify a wrong endpoint/key, the demo will still run but use local object detection instead. +' + exit 1 +fi + +cognitive_endpoint="" +cognitive_key="" + +while [ $# -gt 0 ]; do + + if [[ $1 == *"--"* ]]; then + param="${1/--/}" + declare "$param"="$2" + fi + + shift +done + +trap cleanup SIGINT + +cleanup() +{ + echo>&2 "Stopping applications..." + pkill kv-app || true + pkill dog-mode-logic-app || true + pkill DogModeDashboard || true + if [[ ! -z "$CLOUD_DETECTION_PID" ]]; then + kill $CLOUD_DETECTION_PID || true + fi + kill $LOCAL_DETECTION_PID || true + kill $MOCK_VAS_PID || true + kill $CAMERA_PID || true + exit 1 +} + +if [ ! -d target/logs ]; then + mkdir -p target/logs +fi + +cargo build --workspace + +sleep 2 + +./intent_brokering/dogmode/applications/dog-mode-ui/mock_provider_dog_mode_demo.sh | ANNOUNCE_URL=http://localhost:50051 ./target/debug/mock-vas > target/logs/mock_vas.txt 2>&1 & # DevSkim: ignore DS162092 +MOCK_VAS_PID=$! +ANNOUNCE_URL=http://localhost:50064 ./target/debug/kv-app > target/logs/kv_app.txt 2>&1 & # DevSkim: ignore DS162092 +SIMULATED_CAMERA_APP_IMAGES_DIRECTORY=./intent_brokering/dogmode/applications/simulated-camera/images ANNOUNCE_URL=http://localhost:50066 ./target/debug/simulated-camera-app > target/logs/simulated_camera_app.txt 2>&1 & # DevSkim: ignore DS162092 +CAMERA_PID=$! +TENSORFLOW_LIB_PATH="$(dirname $(find target -name libtensorflow.so -printf '%T@\t%p\n' | sort -nr | cut -f 2- | head -1))" +LIBRARY_PATH=$TENSORFLOW_LIB_PATH LD_LIBRARY_PATH=$TENSORFLOW_LIB_PATH CATEGORIES_FILE_PATH=./intent_brokering/dogmode/applications/local-object-detection/models/categories.json ANNOUNCE_URL=http://localhost:50061 ./target/debug/local-object-detection-app > target/logs/local_object_detection_app.txt 2>&1 & # DevSkim: ignore DS162092 +LOCAL_DETECTION_PID=$! +if [[ ! -z "$cognitive_endpoint" || ! -z "$cognitive_key" ]]; then + COGNITIVE_ENDPOINT=$cognitive_endpoint COGNITIVE_KEY=$cognitive_key ANNOUNCE_URL=http://localhost:50063 ./target/debug/cloud-object-detection-app > target/logs/cloud_object_detection_app.txt 2>&1 & # DevSkim: ignore DS162092 + CLOUD_DETECTION_PID=$! +else + echo "Did not start cloud object detection application. Specify 'cognitive_endpoint' and 'cognitive_key' to start it." +fi + +sleep 5 + +./target/debug/dog-mode-logic-app > target/logs/dog_mode_logic_app.txt 2>&1 & + +sleep 2 + +dotnet run --project intent_brokering/dogmode/applications/dog-mode-ui/src > ./target/logs/ui.txt 2>&1 & + +wait diff --git a/intent_brokering/dogmode/applications/run_demo_containers.sh b/intent_brokering/dogmode/applications/run_demo_containers.sh new file mode 100755 index 0000000..e6404d8 --- /dev/null +++ b/intent_brokering/dogmode/applications/run_demo_containers.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +# After running build-all-containers.sh, this script can be used to run the demo with all services +# running as containers. **Note: you must build the intent_brokering container with tag "1" before +# running this script as well. It starts all services as containers, but runs local object +# detection and the Dog Mode UI locally (not in containers). Logs can be found in target/logs. + +set -e + +docker run --network=host intent_brokering:1 > target/logs/intent_brokering.txt 2>&1 & + +sleep 2 + +docker run --network=host -e ANNOUNCE_URL=http://localhost:50051 mock-vas:1 > target/logs/mock_vas.txt 2>&1 & # DevSkim: ignore DS162092 +docker run --network=host -e ANNOUNCE_URL=http://localhost:50064 kv-app:1 > target/logs/kv_app.txt 2>&1 & # DevSkim: ignore DS162092 +docker run --network=host -e ANNOUNCE_URL=http://localhost:50066 simulated-camera-app:1 > target/logs/simulated_camera_app.txt 2>&1 & # DevSkim: ignore DS162092 +TENSORFLOW_LIB_PATH="$(dirname $(find target -name libtensorflow.so -printf '%T@\t%p\n' | sort -nr | cut -f 2- | head -1))" +LIBRARY_PATH=$TENSORFLOW_LIB_PATH LD_LIBRARY_PATH=$TENSORFLOW_LIB_PATH CATEGORIES_FILE_PATH=./intent_brokering/dogmode/applications/local-object-detection/models/categories.json ANNOUNCE_URL=http://localhost:50061 ./target/debug/local-object-detection-app > target/logs/local_object_detection_app.txt 2>&1 & # DevSkim: ignore DS162092 + +sleep 5 + +docker run --network=host dog-mode-logic-app:1 > target/logs/dog_mode_logic_app.txt 2>&1 & + +sleep 2 + +dotnet run --project ./intent_brokering/dogmode/applications/dog-mode-ui/src > ./target/logs/ui.txt 2>&1 & + diff --git a/intent_brokering/dogmode/applications/simulated-camera/Cargo.toml b/intent_brokering/dogmode/applications/simulated-camera/Cargo.toml new file mode 100644 index 0000000..62ee275 --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "simulated-camera-app" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +anyhow = { workspace = true } +async-std = "1.12" +async-stream = "0.3.3" +async-trait = { workspace = true } +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +ess = { path = "../../../../external/chariott/intent_brokering/ess" } +examples-common = { path = "../../common/" } +futures = { workspace = true } +lazy_static = { workspace = true } +regex = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-stream = { workspace = true } +tokio-util = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } diff --git a/intent_brokering/dogmode/applications/simulated-camera/LICENSE_IMAGES b/intent_brokering/dogmode/applications/simulated-camera/LICENSE_IMAGES new file mode 100644 index 0000000..8bccdca --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/LICENSE_IMAGES @@ -0,0 +1,121 @@ +Following license applies to the images you can find in the "images" folder. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. + +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + + 1. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + 2. moral rights retained by the original author(s) and/or performer(s); + + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + + 5. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “Waiver”). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer’s heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer’s express + Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer’s express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer’s Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “License”). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + 2. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. diff --git a/intent_brokering/dogmode/applications/simulated-camera/README.md b/intent_brokering/dogmode/applications/simulated-camera/README.md new file mode 100644 index 0000000..3942126 --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/README.md @@ -0,0 +1,81 @@ +# Simulated camera application + +This code sample shows you an implementation of a simulated camera streaming, +which is looping through the files in a `images` folder and streaming those +frames at the following rates: 2/6/12 frames per minute or respectively in +manual mode where you specify the frame rate event yourself. + +## To run the application + +1. Start the Intent Brokering runtime by executing `cargo run -p intent_brokering` from the root directory +2. Navigate to `intent_brokering/dogmode/applications/simulated-camera` directory +3. Create an `images` directory and place there all the `.jpg` files you want the + camera application to stream +4. Start camera application by executing `cargo run` from the + `intent_brokering/dogmode/applications/simulated-camera` directory. +5. In another terminal, open a channel to the simulated-camera with `grpcurl -v \ + -plaintext -import-path proto -proto \ + intent_brokering/proto/intent_brokering/streaming/v1/streaming.proto localhost:50066 \ + intent_brokering.streaming.v1.ChannelService/Open` and take a note of the returned + channel id in the metadata _x-chariott-channel-id_. +6. In yet another terminal, call the following, using the channel id from the + previous step: + + ```shell + grpcurl -v -plaintext -d @ localhost:4243 intent_brokering.runtime.v1.IntentBrokeringService/Fulfill < +``` + +For example, to load an image `image.jpg` at 2 frames per minute, you would +send the following to the application: + +```shell +load image.jpg camera.2fpm +``` + +Valid frame rates are `camera.2fpm`, `camera.6fpm` and `camera.12fpm`. The image +path is relative from where you start the application. diff --git a/intent_brokering/dogmode/applications/simulated-camera/images/dog_1.jpg b/intent_brokering/dogmode/applications/simulated-camera/images/dog_1.jpg new file mode 100644 index 0000000..7b9ead0 Binary files /dev/null and b/intent_brokering/dogmode/applications/simulated-camera/images/dog_1.jpg differ diff --git a/intent_brokering/dogmode/applications/simulated-camera/images/dog_2.jpg b/intent_brokering/dogmode/applications/simulated-camera/images/dog_2.jpg new file mode 100644 index 0000000..4807901 Binary files /dev/null and b/intent_brokering/dogmode/applications/simulated-camera/images/dog_2.jpg differ diff --git a/intent_brokering/dogmode/applications/simulated-camera/images/dog_3.jpg b/intent_brokering/dogmode/applications/simulated-camera/images/dog_3.jpg new file mode 100644 index 0000000..988150a Binary files /dev/null and b/intent_brokering/dogmode/applications/simulated-camera/images/dog_3.jpg differ diff --git a/intent_brokering/dogmode/applications/simulated-camera/images/dog_4.jpg b/intent_brokering/dogmode/applications/simulated-camera/images/dog_4.jpg new file mode 100644 index 0000000..b5cb713 Binary files /dev/null and b/intent_brokering/dogmode/applications/simulated-camera/images/dog_4.jpg differ diff --git a/intent_brokering/dogmode/applications/simulated-camera/images/no_dog.jpg b/intent_brokering/dogmode/applications/simulated-camera/images/no_dog.jpg new file mode 100644 index 0000000..67dc98d Binary files /dev/null and b/intent_brokering/dogmode/applications/simulated-camera/images/no_dog.jpg differ diff --git a/intent_brokering/dogmode/applications/simulated-camera/src/camera.rs b/intent_brokering/dogmode/applications/simulated-camera/src/camera.rs new file mode 100644 index 0000000..d0de77d --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/src/camera.rs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use async_stream::try_stream; +use core::panic; +use examples_common::intent_brokering::value::Value; +use intent_brokering_common::error::Error; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use tokio::select; +use tokio::time::sleep; +use tokio_stream::Stream; +use tokio_stream::StreamExt; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +use crate::intent_provider::StreamingStore; + +pub struct CameraLogic { + store: Arc, + images_directory: PathBuf, +} + +impl CameraLogic { + pub fn new(store: Arc) -> Result { + let images_directory: PathBuf = std::env::var("SIMULATED_CAMERA_APP_IMAGES_DIRECTORY") + .unwrap_or_else(|_| "./images".to_owned()) + .into(); + + if !images_directory.is_dir() { + return Err(Error::new(format!( + "Images directory {images_directory:?} does not exist." + ))); + } + + Ok(CameraLogic { + store, + images_directory, + }) + } + + // A note for the reader of this example + // The code that follows is loading in memory all the images included in images folder + // The approach taken here was aiming to simplicity, while sacrificing the memory consumption + // A more memory efficient approach would have been to just take the file names and stream them one by one when needed in camera_loop + fn get_images_from_folder(&self) -> Result, Error> { + let images_directory = std::fs::read_dir(self.images_directory.clone())?; + + let mut images = vec![]; + for entry in images_directory { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let file_bytes = std::fs::read(entry.path())?; + images.push(Value::new_blob("images/jpeg".to_owned(), file_bytes)); + } + + if images.is_empty() { + panic!("No images"); + } + + Ok(images) + } + + pub async fn camera_loop(&self, cancellation_token: CancellationToken) -> Result<(), Error> { + let images = self.get_images_from_folder()?; + let mut cycle = images.iter().cycle(); + let mut hashmap: HashMap, (Instant, Duration)> = HashMap::new(); + hashmap.insert( + "camera.2fpm".into(), + (Instant::now(), Duration::from_secs(30)), + ); + hashmap.insert( + "camera.6fpm".into(), + (Instant::now(), Duration::from_secs(10)), + ); + hashmap.insert( + "camera.12fpm".into(), + (Instant::now(), Duration::from_secs(5)), + ); + let loop_cycle = Duration::from_secs(1); + + loop { + // For simplicity, we are looping through photos only if a timer elapsed + let mut cycled = false; + let mut event = Value::new_blob("".to_owned(), vec![]); + for (event_id, (last_occurrency, interval)) in &mut hashmap { + if last_occurrency.elapsed().gt(interval) { + if !cycled { + event = cycle.next().unwrap().to_owned(); + cycled = true; + } + self.store.set(event_id.to_owned(), event.clone()); + *last_occurrency = Instant::now(); + } + } + + select! { + _ = sleep(loop_cycle) => {}, + _ = cancellation_token.cancelled() => { break; } + } + } + + Ok(()) + } + + pub async fn execute(&mut self, cancellation_token: CancellationToken) -> Result<(), Error> { + let input_stream = handle_input(cancellation_token.clone()); + + tokio::pin!(input_stream); + + while let Some(items) = input_stream.next().await { + match items { + Ok((image, frame_rate)) => { + let file_bytes = std::fs::read(image.clone()); + if file_bytes.is_err() { + error!("Error reading file: {}", image); + continue; + } + let image = Value::new_blob("images/jpeg".to_owned(), file_bytes.unwrap()); + self.store.set(frame_rate.into(), image); + } + Err(err) => { + error!("Error reading stream: {}", err); + break; + } + } + } + + Ok(()) + } +} + +// In 1.64 or later clippy checks get_first +// as described in this lint rule https://rust-lang.github.io/rust-clippy/master/index.html#get_first +// try_stream macro is still using accessing first element with `$crate::async_stream_impl::try_stream_inner!(($crate) $($tt)*).get(0)` +#[allow(clippy::get_first)] +fn handle_input( + shutdown_token: CancellationToken, +) -> impl Stream> { + use async_std::io::{prelude::BufReadExt, stdin, BufReader}; + + info!("-- Data update input ready --"); + + let stdin = BufReader::new(stdin()); + let mut lines = stdin.lines(); + + try_stream! { + loop { + let input = tokio::select! { + line = lines.next() => line, + _ = shutdown_token.cancelled() => break, + }; + + if let Some(input) = input { + let input = input?; + let input_list: Vec<&str> = input.split(' ').collect(); + if input_list.len() != 3 { + warn!("Please use 'load image_path frame_rate' as input format!"); + continue; + } + let data_type = input_list[0].to_lowercase(); + if let Some(b'#') = data_type.as_bytes().get(0) { + continue; + } + let image: Box = input_list[1].to_lowercase().trim().into(); + let frame_rate: Box = input_list[2].to_lowercase().trim().into(); + + yield (image.into(), frame_rate.into()) + } else { + debug!("Shutting down input handler."); + break; + } + } + } +} diff --git a/intent_brokering/dogmode/applications/simulated-camera/src/communication.rs b/intent_brokering/dogmode/applications/simulated-camera/src/communication.rs new file mode 100644 index 0000000..4f6f8fe --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/src/communication.rs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::sync::Arc; +use std::{env::args, net::SocketAddr}; + +use futures::future::join_all; +use intent_brokering_common::{ + error::{Error, ResultExt as _}, + shutdown::{ctrl_c_cancellation, RouterExt as _}, +}; +use intent_brokering_proto::{ + provider::provider_service_server::ProviderServiceServer, + streaming::channel_service_server::ChannelServiceServer, +}; +use tokio::spawn; +use tonic::transport::Server; +use url::Url; + +use crate::{ + camera::CameraLogic, + intent_provider::{IntentProvider, StreamingStore}, +}; + +pub async fn serve(url: Url, address: SocketAddr) -> Result<(), Error> { + let streaming_store = Arc::new(StreamingStore::new()); + let mut camera_logic = CameraLogic::new(Arc::clone(&streaming_store))?; + + let cancellation_token = ctrl_c_cancellation(); + let server_token = cancellation_token.child_token(); + + let camera_handle = spawn(async move { + let result = if args().any(|arg| arg == "-m") { + camera_logic.execute(cancellation_token.child_token()).await + } else { + camera_logic + .camera_loop(cancellation_token.child_token()) + .await + }; + + cancellation_token.cancel(); + + result + }); + + let server_handle = spawn( + Server::builder() + .add_service(ProviderServiceServer::new(IntentProvider::new( + url, + Arc::clone(&streaming_store), + ))) + .add_service(ChannelServiceServer::new(streaming_store.ess().clone())) + .serve_with_cancellation(address, server_token), + ); + + for result in join_all([camera_handle, server_handle]).await { + result.map_err_with("Joining the handle failed.")??; + } + + Ok(()) +} diff --git a/intent_brokering/dogmode/applications/simulated-camera/src/intent_provider.rs b/intent_brokering/dogmode/applications/simulated-camera/src/intent_provider.rs new file mode 100644 index 0000000..a5c063c --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/src/intent_provider.rs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use intent_brokering_proto::{ + common::{ + discover_fulfillment::Service, intent::Intent, DiscoverFulfillment, FulfillmentEnum, + FulfillmentMessage, + }, + provider::{provider_service_server::ProviderService, FulfillRequest, FulfillResponse}, +}; +use tonic::{Request, Response, Status}; +use url::Url; + +use examples_common::intent_brokering::{ + self, + inspection::{fulfill, Entry}, + streaming::ProtoExt as _, + value::Value, +}; + +pub type StreamingStore = intent_brokering::streaming::StreamingStore; + +const SCHEMA_VERSION_STREAMING: &str = "intent_brokering.streaming.v1"; +const SCHEMA_REFERENCE: &str = "grpc+proto"; + +pub struct IntentProvider { + url: Url, + store: Arc, +} + +impl IntentProvider { + pub fn new(url: Url, store: Arc) -> Self { + Self { url, store } + } +} + +lazy_static::lazy_static! { + static ref CAMERA_SCHEMA: Vec = vec![ + property("camera.2fpm", 2), + property("camera.6fpm", 6), + property("camera.12fpm", 12), + ]; +} + +fn property(path: &str, fpm: i32) -> Entry { + Entry::new( + path, + [ + ("member_type", "property".into()), + ("type", "blob".into()), + ("frames_per_minute", fpm.into()), + ("write", Value::FALSE), + ("watch", Value::TRUE), + ], + ) +} + +#[async_trait] +impl ProviderService for IntentProvider { + async fn fulfill( + &self, + request: Request, + ) -> Result, Status> { + let response = match request + .into_inner() + .intent + .and_then(|i| i.intent) + .ok_or_else(|| Status::invalid_argument("Intent must be specified"))? + { + Intent::Discover(_) => FulfillmentEnum::Discover(DiscoverFulfillment { + services: vec![Service { + url: self.url.to_string(), + schema_kind: SCHEMA_REFERENCE.to_owned(), + schema_reference: SCHEMA_VERSION_STREAMING.to_owned(), + metadata: HashMap::new(), + }], + }), + Intent::Inspect(inspect) => fulfill(inspect.query, &*CAMERA_SCHEMA), + Intent::Subscribe(subscribe) => self.store.subscribe(subscribe)?, + Intent::Read(read) => self.store.read(read), + _ => Err(Status::not_found(""))?, + }; + + Ok(Response::new(FulfillResponse { + fulfillment: Some(FulfillmentMessage { + fulfillment: Some(response), + }), + })) + } +} diff --git a/intent_brokering/dogmode/applications/simulated-camera/src/main.rs b/intent_brokering/dogmode/applications/simulated-camera/src/main.rs new file mode 100644 index 0000000..fd189ce --- /dev/null +++ b/intent_brokering/dogmode/applications/simulated-camera/src/main.rs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod camera; +mod communication; +mod intent_provider; + +use examples_common::intent_brokering; +use intent_brokering_common::error::Error; +use intent_brokering_proto::runtime::{ + intent_registration::Intent, intent_service_registration::ExecutionLocality, +}; + +intent_brokering::provider::main!(wain); + +async fn wain() -> Result<(), Error> { + let (url, socket_address) = intent_brokering::provider::register( + "sdv.cabin.camera", + "0.0.1", + "sdv.camera.simulated", + [ + Intent::Discover, + Intent::Subscribe, + Intent::Inspect, + Intent::Read, + ], + "SIMULATED_CAMERA_URL", + "http://0.0.0.0:50066", // DevSkim: ignore DS137138 + ExecutionLocality::Local, + ) + .await?; + + tracing::info!("Application application listening: {url}"); + + communication::serve(url, socket_address).await +} diff --git a/intent_brokering/dogmode/common/Cargo.toml b/intent_brokering/dogmode/common/Cargo.toml new file mode 100644 index 0000000..a0fda1c --- /dev/null +++ b/intent_brokering/dogmode/common/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "examples-common" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +async-trait = { workspace = true } +bytes = "1.5" +intent_brokering_common = { workspace = true } +intent_brokering_proto = { workspace = true } +ess = { path = "../../../external/chariott/intent_brokering/ess" } +keyvalue = { path = "../../../external/chariott/intent_brokering/keyvalue" } +futures = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-stream = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/intent_brokering/dogmode/common/build.rs b/intent_brokering/dogmode/common/build.rs new file mode 100644 index 0000000..4e5cd0a --- /dev/null +++ b/intent_brokering/dogmode/common/build.rs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::{error::Error, path::Path}; +use tonic_build::configure; + +fn main() -> Result<(), Box> { + configure().compile( + &[Path::new( + "../applications/proto/examples/detection/v1/detection.proto", + )], + &[ + Path::new("../../../external/chariott/intent_brokering/proto/"), + Path::new("../applications/proto/examples/detection/v1/"), + ], + )?; + + Ok(()) +} diff --git a/intent_brokering/dogmode/common/src/examples/detection.rs b/intent_brokering/dogmode/common/src/examples/detection.rs new file mode 100644 index 0000000..007ff54 --- /dev/null +++ b/intent_brokering/dogmode/common/src/examples/detection.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use intent_brokering_common::error::{Error, ResultExt}; +use intent_brokering_proto::common::{Blob, InvokeFulfillment, InvokeIntent}; +use prost::Message; + +use crate::intent_brokering::value::Value; + +use super::proto::detection::{ + DetectEntry, DetectRequest as DetectRequestMessage, DetectResponse as DetectResponseMessage, +}; + +pub struct DetectRequest(Vec); + +impl From for Vec { + fn from(detect_request: DetectRequest) -> Self { + detect_request.0 + } +} + +pub struct DetectResponse(Vec); + +impl DetectResponse { + pub fn new(detection_objects: Vec) -> Self { + Self(detection_objects) + } +} + +#[derive(Clone)] +pub struct DetectionObject { + object: Box, + confidence: f64, +} + +impl DetectionObject { + pub fn new(object: impl Into>, confidence: f64) -> Self { + Self { + object: object.into(), + confidence, + } + } +} + +impl TryFrom for DetectRequest { + type Error = Error; + + fn try_from(intent: InvokeIntent) -> Result { + if intent.args.len() != 1 || intent.command != "detect" { + return Err(Error::new( + "No command found which accepts the invocation arguments.", + )); + } + + let value: Value = intent.args[0] + .clone() + .try_into() + .map_err(|_| Error::new("Could not parse value."))?; + + let (type_url, value) = value + .into_any() + .map_err(|_| Error::new("Argument was not of type 'Any'."))?; + + if type_url == "examples.detection.v1.DetectRequest" { + DetectRequestMessage::decode(&*value) + .map_err_with("DetectRequest decoding failed.") + .and_then(|DetectRequestMessage { blob }| { + blob.ok_or_else(|| Error::new("No blob was present.")) + }) + .map(|Blob { bytes, .. }| DetectRequest(bytes)) + } else { + Err(Error::new( + "Argument was not of type 'examples.detection.v1.DetectRequest'.", + )) + } + } +} + +impl From for InvokeFulfillment { + fn from(value: DetectResponse) -> Self { + let entries = value + .0 + .into_iter() + .map(|o| DetectEntry { + object: o.object.into(), + confidence: o.confidence, + }) + .collect(); + + InvokeFulfillment { + r#return: Some( + Value::new_any( + "examples.detection.v1.DetectResponse".to_string(), + DetectResponseMessage { entries }.encode_to_vec(), + ) + .into(), + ), + } + } +} diff --git a/intent_brokering/dogmode/common/src/examples/mod.rs b/intent_brokering/dogmode/common/src/examples/mod.rs new file mode 100644 index 0000000..ae16d1a --- /dev/null +++ b/intent_brokering/dogmode/common/src/examples/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +/// Modules in `examples` are not common to IntentBrokering in general, but common to +/// one of our examples. The sub-modules illustrate how shared code between +/// similar examples can look like. +pub mod detection; +pub mod proto; diff --git a/intent_brokering/dogmode/common/src/examples/proto.rs b/intent_brokering/dogmode/common/src/examples/proto.rs new file mode 100644 index 0000000..e3533f4 --- /dev/null +++ b/intent_brokering/dogmode/common/src/examples/proto.rs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod intent_brokering { + pub mod common { + pub use intent_brokering_proto::common as v1; + } +} + +mod examples { + pub mod detection { + pub mod v1 { + // see https://github.com/hyperium/tonic/issues/1056 + // and https://github.com/tokio-rs/prost/issues/661#issuecomment-1156606409 + // why we use allow derive_partial_eq_without_eq + #![allow(clippy::derive_partial_eq_without_eq)] + tonic::include_proto!("examples.detection.v1"); + } + } +} + +pub use examples::detection::v1 as detection; diff --git a/intent_brokering/dogmode/common/src/intent_brokering/api.rs b/intent_brokering/dogmode/common/src/intent_brokering/api.rs new file mode 100644 index 0000000..eaef4df --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/api.rs @@ -0,0 +1,434 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +// api.rs contains code that can be considered "boilerplate" when +// interacting with the Intent Brokering runtime. It will most likely need to be +// repeated for all applications interacting with Intent Brokering. + +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + env, +}; + +use super::{inspection::Entry as InspectionEntry, value::Value}; + +use async_trait::async_trait; +use futures::{stream::BoxStream, StreamExt}; +use intent_brokering_common::error::{Error, ResultExt as _}; +use intent_brokering_proto::{ + common::{ + discover_fulfillment::Service as ServiceMessage, DiscoverFulfillment, DiscoverIntent, + FulfillmentEnum, InspectFulfillment, InspectIntent, IntentEnum, IntentMessage, + InvokeFulfillment, InvokeIntent, ReadFulfillment, ReadIntent, SubscribeFulfillment, + SubscribeIntent, WriteFulfillment, WriteIntent, + }, + runtime::{ + intent_brokering_service_client::IntentBrokeringServiceClient, FulfillRequest, + FulfillResponse, + }, + streaming::{channel_service_client::ChannelServiceClient, OpenRequest}, +}; +use tonic::{transport::Channel, Request, Response}; +use tracing::debug; + +const INTENT_BROKER_URL_KEY: &str = "INTENT_BROKER_URL"; +const DEFAULT_INTENT_BROKER_URL: &str = env!("DEFAULT_INTENT_BROKER_URL"); + +struct Fulfillment(FulfillmentEnum); + +trait FulfillResponseExt { + fn fulfillment(self) -> Result + where + F: TryFrom; +} + +impl FulfillResponseExt for Response { + fn fulfillment(self) -> Result + where + F: TryFrom, + { + self.into_inner() + .fulfillment + .and_then(|fulfillment| fulfillment.fulfillment) + .ok_or_else(|| Error::new("Did not receive fulfillment")) + .and_then(|f| { + Fulfillment(f) + .try_into() + .map_err(|_| Error::new("Unpexpected fulfillment")) + }) + } +} + +macro_rules! impl_try_from_var { + ($source:ty, $variant:path, $target:ty) => { + impl TryFrom<$source> for $target { + type Error = (); + + fn try_from(value: $source) -> Result { + match value.0 { + $variant(f) => Ok(f), + _ => Err(()), + } + } + } + }; +} + +impl_try_from_var!(Fulfillment, FulfillmentEnum::Inspect, InspectFulfillment); +impl_try_from_var!(Fulfillment, FulfillmentEnum::Read, ReadFulfillment); +impl_try_from_var!(Fulfillment, FulfillmentEnum::Write, WriteFulfillment); +impl_try_from_var!(Fulfillment, FulfillmentEnum::Invoke, InvokeFulfillment); +impl_try_from_var!( + Fulfillment, + FulfillmentEnum::Subscribe, + SubscribeFulfillment +); +impl_try_from_var!(Fulfillment, FulfillmentEnum::Discover, DiscoverFulfillment); + +#[derive(Clone)] +pub struct GrpcIntentBrokering { + client: IntentBrokeringServiceClient, +} + +impl GrpcIntentBrokering { + pub async fn connect() -> Result { + let intent_brokering_url = env::var(INTENT_BROKER_URL_KEY) + .unwrap_or_else(|_| DEFAULT_INTENT_BROKER_URL.to_string()); + let client = IntentBrokeringServiceClient::connect(intent_brokering_url) + .await + .map_err_with("Connecting to IntentBrokering failed.")?; + + Ok(Self { client }) + } +} + +#[async_trait] +impl IntentBrokeringCommunication for GrpcIntentBrokering { + async fn fulfill( + &mut self, + namespace: impl Into> + Send, + intent: IntentEnum, + ) -> Result, Error> { + self.client + .fulfill(Request::new(FulfillRequest { + intent: Some(IntentMessage { + intent: Some(intent), + }), + namespace: namespace.into().into(), + })) + .await + .map_err_with("Intent fulfillment failed.") + } +} + +/// IntentBrokering abstracts the Communication layer, but is based on the Protobuf +/// definitions of the IntentBrokering API. +#[async_trait] +pub trait IntentBrokeringCommunication: Send { + async fn fulfill( + &mut self, + namespace: impl Into> + Send, + intent: IntentEnum, + ) -> Result, Error>; +} + +/// IntentBrokering abstracts the Protobuf definitions that define IntentBrokering's API. +#[async_trait] +pub trait IntentBrokering: Send { + async fn invoke + Send>( + &mut self, + namespace: impl Into> + Send, + command: impl Into> + Send, + args: I, + ) -> Result; + + async fn subscribe> + Send>( + &mut self, + namespace: impl Into> + Send, + channel_id: impl Into> + Send, + event_ids: I, + ) -> Result<(), Error>; + + async fn discover( + &mut self, + namespace: impl Into> + Send, + ) -> Result, Error>; + + async fn inspect( + &mut self, + namespace: impl Into> + Send, + query: impl Into> + Send, + ) -> Result, Error>; + + async fn write( + &mut self, + namespace: impl Into> + Send, + key: impl Into> + Send, + value: Value, + ) -> Result<(), Error>; + + async fn read( + &mut self, + namespace: impl Into> + Send, + key: impl Into> + Send, + ) -> Result, Error>; +} + +#[async_trait] +impl IntentBrokering for T { + async fn invoke + Send>( + &mut self, + namespace: impl Into> + Send, + command: impl Into> + Send, + args: I, + ) -> Result { + let command = command.into(); + debug!("Invoking command '{:?}'.", command); + + let args = args.into_iter().map(|arg| arg.into()).collect(); + + self.fulfill( + namespace, + IntentEnum::Invoke(InvokeIntent { + args, + command: command.into(), + }), + ) + .await? + .fulfillment() + .and_then(|invoke: InvokeFulfillment| { + invoke + .r#return + .and_then(|v| v.try_into().ok()) + .ok_or_else(|| Error::new("Return value could not be parsed.")) + }) + } + + async fn subscribe> + Send>( + &mut self, + namespace: impl Into> + Send, + channel_id: impl Into> + Send, + event_ids: I, + ) -> Result<(), Error> { + let channel_id = channel_id.into(); + debug!("Subscribing to events on channel '{:?}'.", channel_id); + + let sources = event_ids.into_iter().map(|e| e.into()).collect(); + + self.fulfill( + namespace, + IntentEnum::Subscribe(SubscribeIntent { + channel_id: channel_id.into(), + sources, + }), + ) + .await? + .fulfillment() + .map(|_: SubscribeFulfillment| ()) + } + + async fn discover( + &mut self, + namespace: impl Into> + Send, + ) -> Result, Error> { + let namespace = namespace.into(); + debug!("Discovering services for namespace '{:?}'.", namespace); + + self.fulfill(namespace, IntentEnum::Discover(DiscoverIntent {})) + .await? + .fulfillment() + .map(|discover: DiscoverFulfillment| { + discover.services.into_iter().map(|s| s.into()).collect() + }) + } + + async fn inspect( + &mut self, + namespace: impl Into> + Send, + query: impl Into> + Send, + ) -> Result, Error> { + let namespace = namespace.into(); + let query = query.into(); + debug!( + "Inspecting namespace '{:?}' with query '{:?}'.", + namespace, query + ); + + self.fulfill( + namespace, + IntentEnum::Inspect(InspectIntent { + query: query.into(), + }), + ) + .await? + .fulfillment() + .and_then(|inspect: InspectFulfillment| { + inspect + .entries + .into_iter() + .map(|e| { + let items_parse_result: Result, Value>, ()> = e + .items + .into_iter() + .map(|(key, value)| value.try_into().map(|value| (key.into(), value))) + .collect(); + match items_parse_result { + Ok(items) => Ok(InspectionEntry::new(e.path, items)), + Err(_) => Err(Error::new("Could not parse value.")), + } + }) + .collect() + }) + } + + async fn write( + &mut self, + namespace: impl Into> + Send, + key: impl Into> + Send, + value: Value, + ) -> Result<(), Error> { + let key = key.into(); + debug!("Writing key '{:?}' with value '{:?}'.", key, value); + + self.fulfill( + namespace, + IntentEnum::Write(WriteIntent { + key: key.into(), + value: Some(value.into()), + }), + ) + .await? + .fulfillment() + .map(|_: WriteFulfillment| ()) + } + + async fn read( + &mut self, + namespace: impl Into> + Send, + key: impl Into> + Send, + ) -> Result, Error> { + let key = key.into(); + let namespace = namespace.into(); + debug!("Reading key '{:?}' on namespace '{:?}'.", key, namespace); + + self.fulfill(namespace, IntentEnum::Read(ReadIntent { key: key.into() })) + .await? + .fulfillment() + .and_then(|read: ReadFulfillment| match read.value { + Some(value) => value + .value + .map(|v| { + Value::try_from(v).map_err(|_| Error::new("Could not parse read value.")) + }) + .map_or(Ok(None), |r| r.map(Some)), + None => Ok(None), + }) + } +} + +#[async_trait::async_trait] +pub trait IntentBrokeringExt { + async fn listen<'b>( + self, + namespace: impl Into> + Send, + subscription_sources: impl IntoIterator> + Send, + ) -> Result>, Error>; +} + +#[async_trait::async_trait] +impl IntentBrokeringExt for &mut T +where + T: IntentBrokering + Send, +{ + async fn listen<'b>( + self, + namespace: impl Into> + Send, + subscription_sources: impl IntoIterator> + Send, + ) -> Result>, Error> { + const CHANNEL_ID_HEADER_NAME: &str = "x-chariott-channel-id"; + const SDV_EVENT_STREAMING_SCHEMA_REFERENCE: &str = "intent_brokering.streaming.v1"; + const SDV_EVENT_STREAMING_SCHEMA_KIND: &str = "grpc+proto"; + + let namespace = namespace.into(); + + let streaming_endpoint = self + .discover(namespace.clone()) + .await? + .into_iter() + .find(|service| { + service.schema_reference.as_ref() == SDV_EVENT_STREAMING_SCHEMA_REFERENCE + && service.schema_kind.as_ref() == SDV_EVENT_STREAMING_SCHEMA_KIND + }) + .ok_or_else(|| { + Error::new("No compatible streaming endpoint found for '{namespace:?}'.") + })? + .url; + + debug!("Streaming endpoint for '{namespace:?}' is: {streaming_endpoint}"); + + let mut provider_client = ChannelServiceClient::connect(streaming_endpoint.into_string()) + .await + .map_err_with("Connecting to streaming endpoint failed.")?; + + let response = provider_client + .open(Request::new(OpenRequest {})) + .await + .map_err_with("Opening stream failed.")?; + + debug!("Now listening for events in namespace '{namespace:?}'"); + + let channel_id: Box = response + .metadata() + .get(CHANNEL_ID_HEADER_NAME) + .and_then(|h| h.to_str().ok()) + .ok_or_else(|| Error::new("Channel ID header not found."))? + .into(); + + let result_stream = response.into_inner().map(|r| { + r.map_err_with("Could not establish stream.") + .and_then(|event| { + event + .value + .ok_or_else(|| Error::new("No value found in event payload.")) + .and_then(|v| { + v.try_into() + .map_err(|_e: ()| Error::new("Could not parse protobuf value.")) + }) + .map(|data| Event { + id: event.source.into_boxed_str(), + data, + seq: event.seq, + }) + }) + }); + + self.subscribe(namespace, channel_id, subscription_sources) + .await?; + + Ok(result_stream.boxed()) + } +} + +pub struct Event { + pub id: Box, + pub data: Value, + pub seq: u64, +} + +pub struct Service { + pub url: Box, + pub schema_kind: Box, + pub schema_reference: Box, +} + +impl From for Service { + fn from(value: ServiceMessage) -> Self { + Service { + url: value.url.into(), + schema_kind: value.schema_kind.into(), + schema_reference: value.schema_reference.into(), + } + } +} diff --git a/intent_brokering/dogmode/common/src/intent_brokering/inspection.rs b/intent_brokering/dogmode/common/src/intent_brokering/inspection.rs new file mode 100644 index 0000000..a347834 --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/inspection.rs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::{borrow::Borrow, collections::HashMap}; + +use intent_brokering_common::query::regex_from_query; +use intent_brokering_proto::common::{ + fulfillment::Fulfillment, inspect_fulfillment::Entry as EntryMessage, InspectFulfillment, +}; + +use super::value::Value; + +pub struct Entry(Box, HashMap, Value>); + +impl Entry { + pub fn new( + path: impl Into>, + items: impl IntoIterator>, impl Into)>, + ) -> Self { + Self( + path.into(), + items + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ) + } + + pub fn get(&self, key: impl Borrow) -> Option<&Value> { + self.1.get(key.borrow()) + } + + pub fn path(&self) -> &str { + &self.0 + } +} + +pub fn fulfill<'a>( + query: impl AsRef, + entries: impl IntoIterator, +) -> Fulfillment { + let regex = regex_from_query(query.as_ref()); + Fulfillment::Inspect(InspectFulfillment { + entries: entries + .into_iter() + .filter(|Entry(path, _)| regex.is_match(path)) + .map(|Entry(path, items)| EntryMessage { + path: path.to_string(), + items: items + .iter() + .map(|(k, v)| (k.to_string(), v.clone().into())) + .collect(), + }) + .collect(), + }) +} diff --git a/intent_brokering/dogmode/common/src/intent_brokering/mod.rs b/intent_brokering/dogmode/common/src/intent_brokering/mod.rs new file mode 100644 index 0000000..dd192a2 --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/mod.rs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +pub mod api; +pub mod inspection; +pub mod provider; +pub mod registration; +pub mod streaming; +pub mod value; diff --git a/intent_brokering/dogmode/common/src/intent_brokering/provider.rs b/intent_brokering/dogmode/common/src/intent_brokering/provider.rs new file mode 100644 index 0000000..12c14eb --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/provider.rs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +#[macro_export] +macro_rules! intent_provider_main { + ($main:ident) => { + #[cfg(not(tarpaulin_include))] + #[::tokio::main] + async fn main() -> ::std::process::ExitCode { + use ::examples_common::intent_brokering::provider::internal::trace_result; + use ::std::process::ExitCode; + use ::tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; + + let main = async { + let collector = tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(tracing::Level::INFO.into()) + .from_env_lossy(), + ) + .finish(); + + collector.init(); + + $main().await + }; + + let result = main.await; + trace_result("Error when executing main", &result); + result + .map(|_| ExitCode::from(0)) + .unwrap_or(ExitCode::from(1)) + } + }; +} + +pub use intent_provider_main as main; + +use std::net::SocketAddr; + +use url::Url; + +use intent_brokering_common::config::env; +use intent_brokering_common::error::{Error, ResultExt}; +use intent_brokering_proto::runtime::{ + intent_registration::Intent, intent_service_registration::ExecutionLocality, +}; + +pub async fn register( + name: impl Into<&str>, + version: impl Into<&str>, + namespace: impl Into<&str>, + intents: impl IntoIterator, + url_env_name: impl Into<&str>, + url: impl Into<&str>, + locality: ExecutionLocality, +) -> Result<(Url, SocketAddr), Error> { + let url: Url = env(url_env_name.into()) + .unwrap_or_else(|| url.into().to_owned()) + .parse() + .map_err_with("Failed to parse URL.")?; + + let registration = super::registration::Builder::new( + name.into(), + version.into(), + url, + namespace.into(), + intents, + locality, + ) + .from_env(); + + let socket_address = registration.parse_provider_socket_address()?; + let announce_url = registration.announce_url().to_owned(); + + // Potential race condition if we register before the server is up. + // Since this is only an example, we do not ensure that the race does not + // happen. + tokio::task::spawn(registration.register()); + + Ok((announce_url, socket_address)) +} + +pub mod internal { + use super::Error; + use tracing::error; + + /// Ensures that a result is tracked in case of an error. + pub fn trace_result(action: &str, result: &Result) { + if let Err(e) = result { + trace_error(action, &e); + } + /// Recursively traces the source error + fn trace_error(action: &str, error: &(dyn std::error::Error)) { + error!("{action}\nDescription: '{error}'\nDebug: '{error:?}'"); + if let Some(source) = error.source() { + trace_error("--- Inner Error", source); + } + } + } +} diff --git a/intent_brokering/dogmode/common/src/intent_brokering/registration.rs b/intent_brokering/dogmode/common/src/intent_brokering/registration.rs new file mode 100644 index 0000000..59a7fd9 --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/registration.rs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::{env, net::SocketAddr, time::Duration}; + +use intent_brokering_common::{ + config, + error::{Error, ResultExt}, +}; +use intent_brokering_proto::runtime::{ + intent_brokering_service_client::IntentBrokeringServiceClient, intent_registration::Intent, + intent_service_registration::ExecutionLocality, AnnounceRequest, IntentRegistration, + IntentServiceRegistration, RegisterRequest, RegistrationState, +}; +use tokio::time::sleep; +use tonic::transport::Channel; +use tracing::warn; +use url::Url; + +use crate::url::UrlExt as _; + +const INTENT_BROKER_URL_KEY: &str = "INTENT_BROKER_URL"; +const DEFAULT_INTENT_BROKER_URL: &str = env!("DEFAULT_INTENT_BROKER_URL"); +const ANNOUNCE_URL_KEY: &str = "ANNOUNCE_URL"; + +pub enum ConfigSource<'a, T> { + Value(T), + Environment(Option<&'a str>), +} + +pub struct Builder { + name: Box, + version: Box, + announce_url: Url, + provider_url: Url, + namespace: Box, + intents: Vec, + intent_broker_url: Url, + registration_interval: Duration, + locality: ExecutionLocality, +} + +impl Builder { + pub fn new( + name: &str, + version: &str, + url: Url, + namespace: &str, + intents: impl IntoIterator, + locality: ExecutionLocality, + ) -> Self { + let intent_broker_url = env::var(INTENT_BROKER_URL_KEY) + .unwrap_or_else(|_| DEFAULT_INTENT_BROKER_URL.to_string()) + .parse() + .unwrap(); + + let announce_url: Url = + intent_brokering_common::config::env(ANNOUNCE_URL_KEY).unwrap_or_else(|| url.clone()); + + Self { + name: name.into(), + version: version.into(), + announce_url, + provider_url: url, + namespace: namespace.into(), + intents: intents.into_iter().collect(), + intent_broker_url, + registration_interval: Duration::from_secs(5), + locality, + } + } + + pub fn set_registration_interval(mut self, value: ConfigSource) -> Self { + match value { + ConfigSource::Value(value) => self.registration_interval = value, + ConfigSource::Environment(name) => { + let name = name.unwrap_or("INTENT_BROKER_REGISTRATION_INTERVAL"); + let registration_interval = self.registration_interval; + return self.set_registration_interval(ConfigSource::Value( + config::env::(name) + .map(Duration::from_secs) + .unwrap_or(registration_interval), + )); + } + } + self + } + + pub fn set_intent_broker_url(mut self, value: ConfigSource) -> Self { + match value { + ConfigSource::Value(value) => self.intent_broker_url = value, + ConfigSource::Environment(name) => { + let name = name.unwrap_or("INTENT_BROKER_URL"); + if let Some(url) = config::env::(name) { + return self.set_intent_broker_url(ConfigSource::Value(url)); + } + } + } + self + } + + pub fn from_env(self) -> Self { + self.set_intent_broker_url(ConfigSource::Environment(None)) + .set_registration_interval(ConfigSource::Environment(None)) + } + + pub fn announce_url(&self) -> &Url { + &self.announce_url + } + + pub fn provider_url(&self) -> &Url { + &self.provider_url + } + + pub fn parse_provider_socket_address(&self) -> Result { + self.provider_url() + .parse_socket_address() + .map_err_with("Error parsing provider socket address.") + } + + pub async fn register(self) { + let mut client = None; + let mut first_iteration = true; + + loop { + match self.register_once(&mut client, first_iteration).await { + Ok(_) => { + first_iteration = false; + } + Err(e) => { + warn!( + "Registration failed with '{:?}'. Retrying after {:?}.", + e, self.registration_interval + ); + client = None; + } + } + + sleep(self.registration_interval).await; + } + } + + pub async fn register_once( + &self, + client: &mut Option>, + first_iteration: bool, + ) -> Result<(), Error> { + if client.is_none() { + *client = Some( + IntentBrokeringServiceClient::connect(self.intent_broker_url.to_string()) + .await + .map_err_with(format!( + "Could not connect to IntentBrokering ({})", + self.intent_broker_url + ))?, + ); + } + + if let Some(client) = client { + let announce_request = AnnounceRequest { + service: Some(IntentServiceRegistration { + name: self.name.to_string(), + url: self.announce_url.to_string(), + version: self.version.to_string(), + locality: self.locality as i32, + }), + }; + + let registration_state = client + .announce(announce_request.clone()) + .await + .map_err_with("Error when announcing to IntentBrokering.")? + .into_inner() + .registration_state; + + if first_iteration || registration_state == RegistrationState::Announced as i32 { + let register_request = RegisterRequest { + service: announce_request.service.clone(), + intents: self + .intents + .iter() + .map(|i| IntentRegistration { + intent: *i as i32, + namespace: self.namespace.to_string(), + }) + .collect(), + }; + + tracing::info!( + "Registered with IntentBrokering runtime: {:?}", + register_request + ); + _ = client + .register(register_request.clone()) + .await + .map_err_with("Error when registering with IntentBrokering.")?; + } + } + + Ok(()) + } +} diff --git a/intent_brokering/dogmode/common/src/intent_brokering/streaming.rs b/intent_brokering/dogmode/common/src/intent_brokering/streaming.rs new file mode 100644 index 0000000..c9384d4 --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/streaming.rs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use intent_brokering_common::streaming_ess::StreamingEss; +use intent_brokering_proto::common::{ + fulfillment::Fulfillment, ReadFulfillment, ReadIntent, SubscribeIntent, ValueEnum, ValueMessage, +}; +use keyvalue::{InMemoryKeyValueStore, Observer}; +use std::sync::RwLock; +use tonic::Status; + +type EventId = Box; + +/// Wrapper around the [`StreamingEss`](StreamingEss) to allow implementing the +/// `Observer` trait for said type. +#[derive(Clone)] +struct InternalStreamingEss(StreamingEss<(EventId, T)>); + +impl Observer for InternalStreamingEss { + fn on_set(&mut self, key: &EventId, value: &T) { + self.0.publish(key, (key.clone(), value.clone())); + } +} + +/// Represents an in-memory store that contains a blanket implementation for +/// integration with the IntentBrokering streaming API. It generalizes over any type of +/// value to be published, as long as that value can be transformed into a value +/// which is compatible with the Proto contract. +pub struct StreamingStore { + ess: InternalStreamingEss, + store: RwLock>>, +} + +impl StreamingStore { + pub fn ess(&self) -> &StreamingEss<(EventId, T)> { + &self.ess.0 + } +} + +impl StreamingStore { + pub fn new() -> Self { + let ess = InternalStreamingEss(StreamingEss::new()); + let store = RwLock::new(InMemoryKeyValueStore::new(Some(ess.clone()))); + Self { ess, store } + } +} + +impl Default for StreamingStore { + fn default() -> Self { + Self::new() + } +} + +impl StreamingStore +where + T: Into + Clone + Send + Sync + 'static, +{ + /// Read a value from the store. + pub fn get(&self, key: &EventId) -> Option { + self.store.read().unwrap().get(key).cloned() + } + + /// Write a value to the store. + pub fn set(&self, key: EventId, value: T) { + self.store.write().unwrap().set(key, value) + } +} + +pub trait ProtoExt { + fn subscribe(&self, subscribe_intent: SubscribeIntent) -> Result; + fn read(&self, intent: ReadIntent) -> Fulfillment; +} + +impl ProtoExt for StreamingStore +where + T: Into + Clone + Send + Sync + 'static, +{ + fn subscribe(&self, subscribe_intent: SubscribeIntent) -> Result { + let result = self + .ess() + .serve_subscriptions(subscribe_intent, |(_, v)| v.into())?; + Ok(Fulfillment::Subscribe(result)) + } + + fn read(&self, intent: ReadIntent) -> Fulfillment { + let value = self.get(&intent.key.into()); + Fulfillment::Read(ReadFulfillment { + value: Some(ValueMessage { + value: value.map(|v| v.into()), + }), + }) + } +} diff --git a/intent_brokering/dogmode/common/src/intent_brokering/value.rs b/intent_brokering/dogmode/common/src/intent_brokering/value.rs new file mode 100644 index 0000000..5ede072 --- /dev/null +++ b/intent_brokering/dogmode/common/src/intent_brokering/value.rs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::{error::Error, fmt::Display}; + +use intent_brokering_proto::common::{Blob, ValueEnum, ValueMessage}; + +#[derive(Debug)] +pub struct InvalidType; + +impl Display for InvalidType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Invalid type.") + } +} + +impl Error for InvalidType {} + +#[derive(Debug)] +pub struct InvalidValueType(Value); + +impl Display for InvalidValueType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Invalid type.") + } +} + +impl Error for InvalidValueType {} + +impl From for Value { + fn from(InvalidValueType(value): InvalidValueType) -> Self { + value + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Value(ValueEnum); + +impl Value { + pub const TRUE: Self = Self(ValueEnum::Bool(true)); + pub const FALSE: Self = Self(ValueEnum::Bool(false)); + pub const NULL: Self = Self(ValueEnum::Null(0)); + + pub fn new_any(type_url: String, value: Vec) -> Self { + Self(ValueEnum::Any(prost_types::Any { type_url, value })) + } + + pub fn new_blob(media_type: String, bytes: Vec) -> Self { + Self(ValueEnum::Blob(Blob { media_type, bytes })) + } + + pub fn to_i32(&self) -> Result { + if let Self(ValueEnum::Int32(value)) = self { + Ok(*value as _) + } else { + Err(InvalidType) + } + } + + pub fn to_i64(&self) -> Result { + if let Self(ValueEnum::Int64(value)) = self { + Ok(*value as _) + } else { + Err(InvalidType) + } + } + + pub fn to_bool(&self) -> Result { + if let Self(ValueEnum::Bool(value)) = self { + Ok(*value) + } else { + Err(InvalidType) + } + } + + pub fn as_str(&self) -> Result<&str, InvalidType> { + if let Self(ValueEnum::String(value)) = self { + Ok(value.as_str()) + } else { + Err(InvalidType) + } + } + + pub fn into_string(self) -> Result { + if let Self(ValueEnum::String(value)) = self { + Ok(value) + } else { + Err(InvalidValueType(self)) + } + } + + pub fn into_any(self) -> Result<(String, Vec), InvalidValueType> { + if let Self(ValueEnum::Any(any)) = self { + Ok((any.type_url, any.value)) + } else { + Err(InvalidValueType(self)) + } + } + + pub fn into_blob(self) -> Result<(String, Vec), InvalidValueType> { + if let Self(ValueEnum::Blob(blob)) = self { + Ok((blob.media_type, blob.bytes)) + } else { + Err(InvalidValueType(self)) + } + } +} + +impl TryFrom for Value { + type Error = (); + + fn try_from(value: ValueMessage) -> Result { + value.value.ok_or(()).map(|v| v.into()) + } +} + +impl From for Value { + fn from(value: ValueEnum) -> Self { + Value(value) + } +} + +impl From for ValueMessage { + fn from(value: Value) -> Self { + ValueMessage { + value: Some(value.into()), + } + } +} + +impl From for ValueEnum { + fn from(Value(value): Value) -> Self { + value + } +} + +impl From<&str> for Value { + fn from(value: &str) -> Self { + Value(ValueEnum::String(value.into())) + } +} + +impl From for Value { + fn from(value: i32) -> Self { + Value(ValueEnum::Int32(value)) + } +} + +impl From for Value { + fn from(value: i64) -> Self { + Value(ValueEnum::Int64(value)) + } +} + +impl From for Value { + fn from(value: f32) -> Self { + Value(ValueEnum::Float32(value)) + } +} + +impl From for Value { + fn from(value: f64) -> Self { + Value(ValueEnum::Float64(value)) + } +} + +impl From for Value { + fn from(value: bool) -> Self { + Value(ValueEnum::Bool(value)) + } +} diff --git a/intent_brokering/dogmode/common/src/lib.rs b/intent_brokering/dogmode/common/src/lib.rs new file mode 100644 index 0000000..c549ccc --- /dev/null +++ b/intent_brokering/dogmode/common/src/lib.rs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +pub mod examples; +pub mod intent_brokering; +pub mod url; diff --git a/intent_brokering/dogmode/common/src/url.rs b/intent_brokering/dogmode/common/src/url.rs new file mode 100644 index 0000000..b561f71 --- /dev/null +++ b/intent_brokering/dogmode/common/src/url.rs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use std::{ + error::Error, + fmt::Display, + net::{IpAddr, SocketAddr}, +}; + +use url::{Host, Url}; + +#[derive(Debug)] +pub enum UrlSocketAddrParseError { + InvalidScheme, + MissingHost, + InvalidAddress, +} + +impl Display for UrlSocketAddrParseError { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use UrlSocketAddrParseError::*; + fmt.write_str(match self { + InvalidScheme => "invalid scheme", + MissingHost => "missing host", + InvalidAddress => "invalid address", + }) + } +} + +impl Error for UrlSocketAddrParseError {} + +pub trait UrlExt { + fn parse_socket_address(&self) -> Result; +} + +impl UrlExt for Url { + fn parse_socket_address(&self) -> Result { + use UrlSocketAddrParseError::*; + + let port = match self.scheme() { + "http" => Ok(80), + "https" => Ok(443), + _ => Err(InvalidScheme), + }?; + let mut addr = match self.host().ok_or(MissingHost)? { + Host::Domain(_) => Err(InvalidAddress), + Host::Ipv4(addr) => Ok(SocketAddr::new(IpAddr::V4(addr), port)), + Host::Ipv6(addr) => Ok(SocketAddr::new(IpAddr::V6(addr), port)), + }?; + if let Some(port) = self.port() { + addr.set_port(port); + } + Ok(addr) + } +} diff --git a/tools/.markdownlinkcheck.jsonc b/tools/.markdownlinkcheck.json similarity index 63% rename from tools/.markdownlinkcheck.jsonc rename to tools/.markdownlinkcheck.json index b2446d8..8a46048 100644 --- a/tools/.markdownlinkcheck.jsonc +++ b/tools/.markdownlinkcheck.json @@ -2,6 +2,14 @@ "ignorePatterns": [ { "pattern": "^http://localhost" + }, + { + "_patternComment": "Checking aka.ms links is unstable and frequently fails on valid links", + "pattern": "^https://aka.ms" + }, + { + "_patternComment": "Checking microsoft.com links is unstable and frequently fails on valid links", + "pattern": "^https://www.microsoft.com" } ], "aliveStatusCodes": [