diff --git a/application-services/custom/camera-management/Dockerfile b/application-services/custom/camera-management/Dockerfile index 417118bf..f79cf8fc 100644 --- a/application-services/custom/camera-management/Dockerfile +++ b/application-services/custom/camera-management/Dockerfile @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 Intel Corporation +# Copyright (c) 2022-2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,14 +15,14 @@ # #build stage -ARG BASE=golang:1.17-alpine3.15 +ARG BASE=golang:1.18-alpine3.16 FROM ${BASE} AS builder ARG ALPINE_PKG_BASE="make git gcc libc-dev libsodium-dev zeromq-dev" ARG ALPINE_PKG_EXTRA="" LABEL license='SPDX-License-Identifier: Apache-2.0' \ - copyright='Copyright (c) 2022: Intel' + copyright='Copyright (c) 2023: Intel' #disabled for now due to issues with the nl.alpinelinux.org mirror, Edgex is working on a fix for this. #RUN sed -e 's/dl-cdn[.]alpinelinux.org/nl.alpinelinux.org/g' -i~ /etc/apk/repositories RUN apk add --update --no-cache ${ALPINE_PKG_BASE} ${ALPINE_PKG_EXTRA} @@ -37,7 +37,7 @@ ARG MAKE="make build-app" RUN $MAKE #final stage -FROM alpine:3.15 +FROM alpine:3.16 LABEL license='SPDX-License-Identifier: Apache-2.0' \ copyright='Copyright (c) 2022: Intel' LABEL Name=app-camera-management Version=${VERSION} diff --git a/application-services/custom/camera-management/README.md b/application-services/custom/camera-management/README.md index 620ccb12..9024581c 100644 --- a/application-services/custom/camera-management/README.md +++ b/application-services/custom/camera-management/README.md @@ -1,7 +1,8 @@ # Camera Management Example App Service Use the Camera Management Example application service to auto discover and connect to nearby ONVIF and USB based cameras. This application will also control cameras via commands, create inference pipelines for the camera video streams and publish inference results to MQTT broker. -This app uses [EdgeX compose][edgex-compose], [Edgex Onvif Camera device service][device-onvif-camera], [Edgex USB Camera device service][device-usb-camera] and [Edge Video Analytics Microservice][evam]. +This app uses [EdgeX compose][edgex-compose], [Edgex Onvif Camera device service][device-onvif-camera], +[Edgex USB Camera device service][device-usb-camera], [Edgex MQTT device service][device-mqtt] and [Edge Video Analytics Microservice][evam]. A brief video demonstration of building and using the example app service can be found [here](https://www.youtube.com/watch?v=vZqd3j2Zn2Y). @@ -107,13 +108,25 @@ sudo apt install build-essential b. Under the `ports` section, find the entry for port 8554 and change the host_ip from `127.0.0.1` to either `0.0.0.0` or the ip address you put in the previous step. -6. Run the following `make` command to run the edgex core services along with the Onvif and Usb device services. +6. Run the following `make` command to generate the edgex core services along with MQTT, Onvif and Usb device services. > **Note**: The `ds-onvif-camera` parameter can be omitted if no Onvif cameras are present, or the `ds-usb-camera` parameter can be omitted if no usb cameras are present. ```shell - make run no-secty ds-onvif-camera ds-usb-camera + make gen no-secty ds-mqtt mqtt-broker ds-onvif-camera ds-usb-camera ``` +7. Configure [device-mqtt] service to send [Edge Video Analytics Microservice][evam] inference results into Edgex via MQTT + + a. Copy the entire [evam-mqtt-edgex](edge-video-analytics/evam-mqtt-edgex) folder into `edgex-compose/compose-builder` directory. + + b. Copy and paste [docker-compose.override.yml](edge-video-analytics/evam-mqtt-edgex/docker-compose.override.yml) from the above copied folder into edgex-compose/compose-builder directory. + Insert full path of `edgex-compose/compose-builder` directory under volumes in this `docker-compose.override.yml`. + > **Note**: Please note that both the services in this file need the full path to be inserted for their volumes. + +8. Run the following command to start all the Edgex services. +```shell + docker compose -f docker-compose.yml -f docker-compose.override.yml up -d +``` ### 2. Start [Edge Video Analytics Microservice][evam] running for inference. @@ -142,7 +155,7 @@ make run-edge-video-analytics > **Note**: This step is only required if you have Onvif cameras. Currently, this example app is limited to supporting > only 1 username/password combination for all Onvif cameras. - > **Note:** Please follow the instructions for the [Edgex Onvif Camera device service][device-onvif-camera] in order to connect your Onvif cameras to EdgeX. + > **Note**: Please follow the instructions for the [Edgex Onvif Camera device service][device-onvif-manage] in order to connect your Onvif cameras to EdgeX. Option 1: Modify the [res/configuration.toml](res/configuration.toml) file @@ -160,7 +173,19 @@ make run-edge-video-analytics export WRITABLE_INSECURESECRETS_CAMERACREDENTIALS_SECRETS_PASSWORD="" ``` -#### 3.2 Build and run +#### 3.2 Configure Default Pipeline +Initially, all new cameras added to the system will start the default analytics pipeline as defined in the configuration file below. The desired pipeline can be changed afterward or the feature can be disabled by setting the `DefaultPipelineName` and `DefaultPipelineVersion` to empty strings. + +Modify the [res/configuration.toml](res/configuration.toml) file with the name and version of the default pipeline to use when a new device is added to the system. + +Note: These values can be left empty to disable the feature. + ```toml +[AppCustom] +DefaultPipelineName = "object_detection" # Name of the default pipeline used when a new device is added to the system +DefaultPipelineVersion = "person" # Version of the default pipeline used when a new device is added to the system + ``` + +#### 3.3 Build and run ```shell # First make sure you are at the root of this example app cd edgex-examples/application-services/custom/camera-management @@ -256,6 +281,16 @@ The API log shows the status of the 5 most recent calls and commands that the ma ![inference events](./images/inference-events.png) +### Inference results in Edgex + +To view inference results in Edgex, open Edgex UI [http://localhost:4000](http://localhost:4000), click on the `DataCenter` +tab and view data streaming under `Event Data Stream`by clicking on the `Start` button. + +![inference events](./images/inference-edgex.png) + +### Next steps +A custom app service can be used to analyze this inference data and take action based on the analysis. + ## Additional Development > **Warning**: The following steps are only useful for developers who wish to make modifications to the code @@ -281,5 +316,7 @@ Open your browser to [http://localhost:4200](http://localhost:4200) [edgex-compose]: https://github.com/edgexfoundry/edgex-compose [device-onvif-camera]: https://github.com/edgexfoundry/device-onvif-camera +[device-onvif-manage]: https://github.com/edgexfoundry/device-onvif-camera/blob/levski/doc/guides/SimpleStartupGuide.md#manage-devices [device-usb-camera]: https://github.com/edgexfoundry/device-usb-camera [evam]: https://www.intel.com/content/www/us/en/developer/articles/technical/video-analytics-service.html +[device-mqtt]: https://github.com/edgexfoundry/device-mqtt-go diff --git a/application-services/custom/camera-management/appcamera/app.go b/application-services/custom/camera-management/appcamera/app.go index 7fbfbd57..a51967d2 100644 --- a/application-services/custom/camera-management/appcamera/app.go +++ b/application-services/custom/camera-management/appcamera/app.go @@ -6,11 +6,12 @@ package appcamera import ( + "net/http" + "sync" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/pkg/errors" - "net/http" - "sync" ) type CameraManagementApp struct { @@ -43,12 +44,30 @@ func (app *CameraManagementApp) Run() error { return err } - if err := app.queryAllPipelineStatuses(); err != nil { + // Subscribe to events. + err := app.service.SetDefaultFunctionsPipeline( + app.processEdgeXDeviceSystemEvent) + if err != nil { + return errors.Wrap(err, "failed to set default pipeline to processEdgeXEvent") + } + + if err = app.queryAllPipelineStatuses(); err != nil { // do not exit, just log app.lc.Errorf("Unable to query EVAM pipeline statuses. Is EVAM running? %s", err.Error()) } - if err := app.service.MakeItRun(); err != nil { + devices, err := app.getAllDevices() + if err != nil { + app.lc.Errorf("no devices found: %s", err.Error()) + } else { + for _, device := range devices { + if err = app.startDefaultPipeline(device); err != nil { + app.lc.Errorf("Error starting default pipeline for %s, %v", device.Name, err) + } + } + } + + if err = app.service.MakeItRun(); err != nil { return errors.Wrap(err, "failed to run pipeline") } diff --git a/application-services/custom/camera-management/appcamera/config.go b/application-services/custom/camera-management/appcamera/config.go index 05574615..b55add59 100644 --- a/application-services/custom/camera-management/appcamera/config.go +++ b/application-services/custom/camera-management/appcamera/config.go @@ -12,6 +12,8 @@ type CustomConfig struct { EvamBaseUrl string MqttAddress string MqttTopic string + DefaultPipelineName string + DefaultPipelineVersion string } // ServiceConfig a struct that wraps CustomConfig which holds the values for driver configuration diff --git a/application-services/custom/camera-management/appcamera/evam.go b/application-services/custom/camera-management/appcamera/evam.go index bc56f0ff..da5c105f 100644 --- a/application-services/custom/camera-management/appcamera/evam.go +++ b/application-services/custom/camera-management/appcamera/evam.go @@ -9,10 +9,15 @@ import ( "context" "encoding/json" "fmt" - "github.com/IOTechSystems/onvif/media" - "github.com/pkg/errors" "net/url" "path" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/common" + + "github.com/IOTechSystems/onvif/media" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/pkg/errors" ) const ( @@ -218,6 +223,93 @@ func (app *CameraManagementApp) getPipelineStatus(deviceName string) (interface{ return nil, nil } +// processEdgeXDeviceSystemEvent is the function that is called when an EdgeX Device System Event is received +func (app *CameraManagementApp) processEdgeXDeviceSystemEvent(_ interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { + return false, fmt.Errorf("processEdgeXDeviceSystemEvent: was called without any data") + } + + systemEvent, ok := data.(dtos.SystemEvent) + if !ok { + return false, fmt.Errorf("type received %T is not a SystemEvent", data) + } + + if systemEvent.Type != common.DeviceSystemEventType { + return false, fmt.Errorf("system event type is not " + common.DeviceSystemEventType) + } + + device := dtos.Device{} + err := systemEvent.DecodeDetails(&device) + if err != nil { + return false, fmt.Errorf("failed to decode device details: %v", err) + } + + switch systemEvent.Action { + case common.DeviceSystemEventActionAdd: + if err = app.startDefaultPipeline(device); err != nil { + return false, err + } + case common.DeviceSystemEventActionDelete: + // stop any running pipelines for the deleted device + if info, found := app.getPipelineInfo(device.Name); found { + if err = app.stopPipeline(device.Name, info.Id); err != nil { + return false, fmt.Errorf("error stopping pipleline for device %s, %v", device.Name, err) + } + } + default: + app.lc.Debugf("System event action %s is not handled", systemEvent.Action) + } + + return false, nil +} + +func (app *CameraManagementApp) startDefaultPipeline(device dtos.Device) error { + pipelineRunning := app.isPipelineRunning(device.Name) + + if pipelineRunning { + app.lc.Debugf("pipeline is already running for device %s", device.Name) + return nil + } + + app.lc.Debugf("pipeline is not running for device %s", device.Name) + + if app.config.AppCustom.DefaultPipelineName == "" || app.config.AppCustom.DefaultPipelineVersion == "" { + app.lc.Warnf("no default pipeline name/version specified, skip starting pipeline for device %s", device.Name) + return nil + } + + startPipelineRequest := StartPipelineRequest{ + PipelineName: app.config.AppCustom.DefaultPipelineName, + PipelineVersion: app.config.AppCustom.DefaultPipelineVersion, + } + + protocol, ok := device.Protocols["Onvif"] + if ok { + app.lc.Debugf("Onvif protocol information found for device: %s message: %v", device.Name, protocol) + profileResponse, err := app.getProfiles(device.Name) + if err != nil { + return fmt.Errorf("failed to get profiles for device %s, message: %v", device.Name, err) + + } + + app.lc.Debugf("Onvif profile information found for device: %s message: %v", device.Name, profileResponse) + startPipelineRequest.Onvif = &OnvifPipelineConfig{ + ProfileToken: string(profileResponse.Profiles[0].Token), + } + } else if _, ok := device.Protocols["USB"]; ok { + app.lc.Debugf("Usb protocol found for device: %s", device.Name) + startPipelineRequest.USB = &USBStartStreamingRequest{} + } + + app.lc.Debugf("Starting default pipeline for device %s", device.Name) + if err := app.startPipeline(device.Name, startPipelineRequest); err != nil { + return fmt.Errorf("pipeline failed to start for device %s, message: %v", device.Name, err) + + } + + return nil +} + // queryAllPipelineStatuses queries EVAM for all pipeline statuses, attempts to link them to devices, and then // insert them into the pipeline map. func (app *CameraManagementApp) queryAllPipelineStatuses() error { diff --git a/application-services/custom/camera-management/edge-video-analytics/docker-compose.yml b/application-services/custom/camera-management/edge-video-analytics/docker-compose.yml index dd1fb2bc..09a9a6dd 100644 --- a/application-services/custom/camera-management/edge-video-analytics/docker-compose.yml +++ b/application-services/custom/camera-management/edge-video-analytics/docker-compose.yml @@ -17,21 +17,11 @@ version: "3" networks: - evam_network: + edgex_edgex-network: + external: true driver: "bridge" services: - broker: - image: eclipse-mosquitto - hostname: mqtt - volumes: - - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro - ports: - - "1883:1883" - - "59001:9001" - networks: - - evam_network - edge_video_analytics_microservice: image: intel/edge_video_analytics_microservice:0.7.2 hostname: edge_video_analytics_microservice @@ -41,7 +31,7 @@ services: - '8080:8080' - '8555:8555' networks: - - evam_network + - edgex_edgex-network environment: ENABLE_RTSP: "true" RTSP_PORT: 8555 diff --git a/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/devices/edge-video-analytics.devices.toml b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/devices/edge-video-analytics.devices.toml new file mode 100644 index 00000000..93a62330 --- /dev/null +++ b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/devices/edge-video-analytics.devices.toml @@ -0,0 +1,12 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Pre-define Devices +[[DeviceList]] + Name = "edge-video-analytics" + ProfileName = "edge-video-analytics-profile" + Description = "Device for EVAM (Edge Video Analytics Microservice) pipeline events" + Labels = ["cv", "dlstreamer", "evam", "pipeline-server", "edge-video-analytics"] + [DeviceList.Protocols] + [DeviceList.Protocols.mqtt] + CommandTopic = "command/edge-video-analytics" diff --git a/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/docker-compose.override.yml b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/docker-compose.override.yml new file mode 100644 index 00000000..8d74520c --- /dev/null +++ b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/docker-compose.override.yml @@ -0,0 +1,35 @@ +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '3.7' + +services: + device-mqtt: + environment: + DEVICE_DEVICESDIR: /evam-mqtt-edgex/devices + DEVICE_PROFILESDIR: /evam-mqtt-edgex/profiles + MQTTBROKERINFO_INCOMINGTOPIC: "incoming/data/#" + MQTTBROKERINFO_USETOPICLEVELS: "true" + volumes: + # example: - /home/github.com/edgexfoundry/edgex-compose/compose-builder/evam-mqtt-edgex:/evam-mqtt-edgex + - /evam-mqtt-edgex:/evam-mqtt-edgex + + mqtt-broker: + volumes: + # example: - /home/github.com/edgexfoundry/edgex-compose/compose-builder/evam-mqtt-edgex:/evam-mqtt-edgex + - /evam-mqtt-edgex/mosquitto.conf:/mosquitto-no-auth.conf:ro + ports: + - "59001:9001" diff --git a/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/mosquitto.conf b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/mosquitto.conf new file mode 100644 index 00000000..3b3edbea --- /dev/null +++ b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/mosquitto.conf @@ -0,0 +1,18 @@ +# +# Copyright (C) 2022-2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +allow_anonymous true +listener 1883 + +log_type error +log_type warning +log_type notice +log_type information +log_type subscribe +log_type unsubscribe +log_type websockets + +listener 9001 +protocol websockets diff --git a/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/profiles/edge-video-analytics.profile.yml b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/profiles/edge-video-analytics.profile.yml new file mode 100644 index 00000000..5c375da2 --- /dev/null +++ b/application-services/custom/camera-management/edge-video-analytics/evam-mqtt-edgex/profiles/edge-video-analytics.profile.yml @@ -0,0 +1,18 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +name: "edge-video-analytics-profile" +manufacturer: "Intel" +model: "EVAM" +description: "Profile for EVAM (Edge Video Analytics Microservice) Inference Events" +labels: + - "cv" + - "evam" + - "edge-video-analytics" +deviceResources: + - name: "inference-event" + isHidden: true # resource is only async + description: "A json message containing the inference event" + properties: + valueType: "String" + readWrite: "R" diff --git a/application-services/custom/camera-management/images/inference-edgex.png b/application-services/custom/camera-management/images/inference-edgex.png new file mode 100644 index 00000000..7f418811 Binary files /dev/null and b/application-services/custom/camera-management/images/inference-edgex.png differ diff --git a/application-services/custom/camera-management/main.go b/application-services/custom/camera-management/main.go index 48e0ebee..c8f5e8c1 100644 --- a/application-services/custom/camera-management/main.go +++ b/application-services/custom/camera-management/main.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Intel Corporation +// Copyright (c) 2022-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,9 +18,11 @@ package main import ( "fmt" + "os" + appsdk "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg" "github.com/edgexfoundry/edgex-examples/application-services/custom/camera-management/appcamera" - "os" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" ) const ( @@ -28,7 +30,7 @@ const ( ) func main() { - service, ok := appsdk.NewAppService(serviceKey) + service, ok := appsdk.NewAppServiceWithTargetType(serviceKey, &dtos.SystemEvent{}) if !ok { fmt.Printf("error: unable to create new app service %s!\n", serviceKey) os.Exit(-1) diff --git a/application-services/custom/camera-management/res/configuration.toml b/application-services/custom/camera-management/res/configuration.toml index 66073247..48be18f8 100644 --- a/application-services/custom/camera-management/res/configuration.toml +++ b/application-services/custom/camera-management/res/configuration.toml @@ -102,12 +102,44 @@ TokenFile = "/tmp/edgex/secrets/app-camera-management/secrets-token.json" Host = "localhost" Port = 59882 -[Trigger] -Type="http" - [AppCustom] OnvifDeviceServiceName = "device-onvif-camera" USBDeviceServiceName = "device-usb-camera" EvamBaseUrl = "http://localhost:8080" -MqttAddress = "broker:1883" +MqttAddress = "edgex-mqtt-broker:1883" MqttTopic = "incoming/data/edge-video-analytics/inference-event" +DefaultPipelineName = "object_detection" # Name of the default pipeline used when a new device is added to the system; can be left blank to disable feature +DefaultPipelineVersion = "person" # Version of the default pipeline used when a new device is added to the system; can be left blank to disable feature + +[Trigger] +Type="edgex-messagebus" + [Trigger.EdgexMessageBus] + Type = "redis" + [Trigger.EdgexMessageBus.SubscribeHost] + Host = "localhost" + Port = 6379 + Protocol = "redis" + AuthMode = "usernamepassword" # required for redis messagebus (secure or insecure). + SecretName = "redisdb" + SubscribeTopics="edgex/system-events/#/device/#" + + [Trigger.EdgexMessageBus.Optional] + authmode = "usernamepassword" # required for redis messagebus (secure or insecure). + secretname = "redisdb" + # Default MQTT Specific options that need to be here to enable environment variable overrides of them + ClientId ="app-camera-management" + Qos = "0" # Quality of Service values are 0 (At most once), 1 (At least once) or 2 (Exactly once) + KeepAlive = "10" # Seconds (must be 2 or greater) + Retained = "false" + AutoReconnect = "true" + ConnectTimeout = "5" # Seconds + SkipCertVerify = "false" + # Default NATS Specific options that need to be here to enable environment variable overrides of them + Format = "nats" + RetryOnFailedConnect = "true" + QueueGroup = "" + Durable = "" + AutoProvision = "true" + Deliver = "new" + DefaultPubRetryAttempts = "2" + Subject = "edgex/#" # Required for NATS JetStream only for stream autoprovisioning