diff --git a/deployment/raspberry-pi-4/10_install_ubuntu.md b/deployment/raspberry-pi-4/10_install_ubuntu.md
index 264782fc..6a82b30e 100644
--- a/deployment/raspberry-pi-4/10_install_ubuntu.md
+++ b/deployment/raspberry-pi-4/10_install_ubuntu.md
@@ -5,7 +5,7 @@
# 1. How to install Ubuntu on RPI
-RPI uses micro SD card as its main disk storage. Ubuntu server 20.10 image file can be downloaded from Canonical and the file can be flashed to a micro SD card by using various ways.
+RPI uses micro SD card as its main disk storage. Ubuntu server 21.10 image file can be downloaded from Canonical and the file can be flashed to a micro SD card by using various ways.
@@ -17,10 +17,10 @@ The OS image file can be found from [https://ubuntu.com/download/raspberry-pi](h
-As mentioned earlier the 64 bit version of Ubuntu server 20.10 will be used so that click the button on the web page or run below command:
+As mentioned earlier the 64 bit version of Ubuntu server 21.10 will be used so that click the button on the web page or run below command:
```sh
$ cd ~/Downloads
-$ wget https://ubuntu.com/download/raspberry-pi/thank-you?version=20.10&architecture=server-arm64+raspi
+$ wget https://cdimage.ubuntu.com/releases/21.10/release/ubuntu-21.10-preinstalled-server-arm64+raspi.img.xz
```
Since many of EdgeX components are built as ARM64 binary, the 64 bit version should be used for the rest of this tutorial.
@@ -46,18 +46,19 @@ $ dmesg | grep Attached
[17709.594036] sd 3:0:0:1: [sdb] Attached SCSI removable disk
```
-Balena etcher, Raspberry Pi OS Imager, or Ubuntu Imager can be used but let's try this commands below:
+From here we can unzip and write to the disk identified (sdb)
```sh
$ cd ~/Downloads
$ sudo -s
-# xzcat extracts the image as raw disk image format and passes to the dd commandlittle by little. Then the dd writes the coming data to the disk.
-$ xzcat ubuntu-20.10-preinstalled-server-arm64+raspi.img.xz | dd of=/dev/sdb bs=8M status=progress; sync
+# xzcat extracts the image as raw disk image format and passes to the dd command little by little. Then the dd writes the coming data to the disk.
+$ xzcat ubuntu-21.10-preinstalled-server-arm64+raspi.img.xz | dd of=/dev/sdb bs=8M status=progress
```
-Once it is done, the host automatically mounts the disks just flashed. The disks get mounted under **/media/\$USER/system-boot** and **/media/\$USER/writable**. However, please do NOT unmount and move on to the next step.
+Once it is done (it will probably take awhile), the host automatically mounts the disks just flashed. The disks get mounted under **/media/\$USER/system-boot** and **/media/\$USER/writable**. However, please do NOT unmount and move on to the next step.
-
+> **NOTE**:
+> The raspberry pi imager or similar utilities can be used as well to simplify this process. To make similar changes on a booted raspberry pi (eg by connecting a monitor) and it should take you through the network setup. File alterations can then be made at the appropriate locations on the mounted file system (eg /media/$USER/writable/etc => /etc).
## 1.3 Network configuration before first boot
diff --git a/deployment/raspberry-pi-4/20_install_packages.md b/deployment/raspberry-pi-4/20_install_packages.md
index df68f51a..d2d53ea2 100644
--- a/deployment/raspberry-pi-4/20_install_packages.md
+++ b/deployment/raspberry-pi-4/20_install_packages.md
@@ -23,7 +23,7 @@ Also, the RPI has its timezone as UTC so that scheduled tasks based on local tim
$ sudo dpkg-reconfigure tzdata
# To confirm the new timezone
-$ timedatactl
+$ timedatectl
Local time: Sun 2020-11-01 11:26:43 PST
Universal time: Sun 2020-11-01 19:26:43 UTC
```
@@ -72,10 +72,10 @@ $ cd ~
$ sudo mkdir /usr/local/go # This may exist
# Download the SDK
-$ wget https://dl.google.com/go/go1.15.3.linux-arm64.tar.gz
+$ wget https://go.dev/dl/go1.17.6.linux-arm64.tar.gz
# Extract and place the SDK under /usr/local/go
-$ sudo bash -c "tar -xf go1.15.3.linux-arm64.tar.gz --strip-components=1 -C /usr/local/go"
+$ sudo bash -c "tar -xf go1.17.6.linux-arm64.tar.gz --strip-components=1 -C /usr/local/go"
# Go needs a directory for its libraries
$ mkdir ~/go
@@ -88,7 +88,7 @@ $ source ~/.bashrc
# To confirm the configurations
$ go version
-go version go1.15.3 linux/arm64
+go version go1.17.6 linux/arm64
# To install Delve
$ go get -u github.com/go-delve/delve/cmd/dlv
@@ -96,8 +96,8 @@ $ go get -u github.com/go-delve/delve/cmd/dlv
# To confirm Delve's version
$ dlv version
Delve Debugger
-Version: 1.5.0
-Build: $Id: ca5318932770ca063fc9885b4764c30bfaf8a199 $
+Version: 1.8.0
+Build: $Id: 6a6c9c332d5354ddf1f8a2da3cc477bd18d2be53 $
```
@@ -107,13 +107,13 @@ Build: $Id: ca5318932770ca063fc9885b4764c30bfaf8a199 $
Docker is a containerization platform/tool. EdgeX' core services are conveniently packaged as docker containers so that we can leverage Docker to run EdgeX. To install Docker and Docker-compose:
```sh
# Install Docker
-$ sudo apt install -y docker.io docker-compose
+$ sudo apt install -y docker.io
# To confirm the versions installed
$ docker -v
-Docker version 19.03.8, build afacb8b7f0
+Docker version 20.10.7, build 20.10.7-0ubuntu5.1
$ docker-compose -v
-docker-compose version 1.25.0, build unknown
+docker-compose version 1.27.4, build unknown
# Enable and start the Docker daemon
$ sudo systemctl enable docker
diff --git a/deployment/raspberry-pi-4/30_install_edgex.md b/deployment/raspberry-pi-4/30_install_edgex.md
index d5a1da30..9aef8459 100644
--- a/deployment/raspberry-pi-4/30_install_edgex.md
+++ b/deployment/raspberry-pi-4/30_install_edgex.md
@@ -8,7 +8,7 @@ The tools to run EdgeX services are ready. The EdgeX stack consists of many dock
## 3.1 Launch EdgeX core services
-The compose files can be found from a repository - [edgexfoundry/developer-scripts](https://github.com/edgexfoundry/developer-scripts). In this chapter, the Geneva version will be used as it is the latest stable version. To launch containers:
+The compose files can be found from a repository - [edgexfoundry/edgex-compose](https://github.com/edgexfoundry/edgex-compose). In this chapter, the Jakarta version will be used as it is the latest stable version. To launch containers:
```sh
$ cd ~
@@ -16,64 +16,75 @@ $ cd ~
$ mkdir repo
$ cd repo
-# Clone the developer-scripts repository
-$ git clone https://github.com/edgexfoundry/developer-scripts
-$ cd developer-scripts/releases/geneva/compose-files
+# Clone the edgex-compose repository
+$ git clone https://github.com/edgexfoundry/edgex-compose
+$ git fetch --all
+$ git checkout jakarta
$ ls
+GOVERNANCE.md
+LICENSE
+Makefile
+OWNERS.md
README.md
-docker-compose-geneva-redis-no-secty-arm64.yml
-docker-compose-geneva-redis-no-secty.yml
-docker-compose-geneva-redis-arm64.yml
-docker-compose-geneva-redis.yml
-docker-compose-geneva-mongo-no-secty-arm64.yml
-docker-compose-geneva-mongo-no-secty.yml
-docker-compose-geneva-mongo-arm64.yml
-docker-compose-geneva-mongo.yml
-docker-compose-geneva-ui-arm64.yml
-docker-compose-geneva-ui.yml
+compose-builder
+docker-compose-arm64.yml
+docker-compose-no-secty-arm64.yml
+docker-compose-no-secty-with-app-sample-arm64.yml
+docker-compose-no-secty-with-app-sample.yml
+docker-compose-no-secty.yml
docker-compose-portainer.yml
+docker-compose-with-app-sample-arm64.yml
+docker-compose-with-app-sample.yml
+docker-compose.yml
+taf
+
# There are several compose files but we only need one to launch for our purpose.
# - ARM64 version should be used for RPI.
-# - Redis is the choice of DB because of MongoDB's license.
# - Security is out of scope in this tutorial.
-# With these criteria, we will use "docker-compose-geneva-redis-no-secty-arm64.yml".
+# With these criteria, we will use "docker-compose-no-secty-arm64.yml".
# This command launches the stack but might take couple minutes depends on the network.
-$ docker-compose -f docker-compose-geneva-redis-no-secty-arm64.yml up -d
+$ docker-compose -f docker-compose-no-secty-arm64.yml up -d
...
+Creating edgex-ui-go ... done
Creating edgex-redis ... done
Creating edgex-core-consul ... done
-Creating edgex-support-scheduler ... done
Creating edgex-support-notifications ... done
+Creating edgex-support-scheduler ... done
+Creating edgex-kuiper ... done
Creating edgex-core-metadata ... done
-Creating edgex-core-command ... done
+Creating edgex-core-command ... done
Creating edgex-core-data ... done
-Creating edgex-app-service-configurable-rules ... done
-Creating edgex-sys-mgmt-agent ... done
-Creating edgex-kuiper ... done
+Creating edgex-device-rest ... done
+Creating edgex-device-virtual ... done
+Creating edgex-app-rules-engine ... done
+Creating edgex-sys-mgmt-agent ... done
+
# Once launching is done, let's check what are up and running. Some columns are removed.
-$ docker ps | less -ESX
-IMAGE STATUS
-emqx/kuiper:0.4.2-alpine Up 11 seconds
-edgexfoundry/docker-sys-mgmt-agent-go-arm64:1.2.1 Up 15 seconds
-edgexfoundry/docker-app-service-configurable-arm64:1.2.0 Up 14 seconds
-edgexfoundry/docker-core-command-go-arm64:1.2.1 Up 18 seconds
-edgexfoundry/docker-core-data-go-arm64:1.2.1 Up 19 seconds
-edgexfoundry/docker-core-metadata-go-arm64:1.2.1 Up 21 seconds
-edgexfoundry/docker-support-scheduler-go-arm64:1.2.1 Up 23 seconds
-edgexfoundry/docker-support-notifications-go-arm64:1.2.1 Up 22 seconds
-arm64v8/redis:5.0.8-alpine Up 26 seconds
-edgexfoundry/docker-edgex-consul-arm64:1.2.0 Up 26 seconds
-portainer/portainer
+$ docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}"
+CONTAINER ID IMAGE STATUS
+7d041dd2cde5 edgexfoundry/sys-mgmt-agent-arm64:2.1.0 Up 4 minutes
+d405bf5834f5 edgexfoundry/device-virtual-arm64:2.1.0 Up 4 minutes
+4eacedaff009 edgexfoundry/app-service-configurable-arm64:2.1.0 Up 4 minutes
+95a08253847e edgexfoundry/device-rest-arm64:2.1.0 Up 4 minutes
+23110c85c80c edgexfoundry/core-data-arm64:2.1.0 Up 4 minutes
+8b432e47ea9f edgexfoundry/core-command-arm64:2.1.0 Up 4 minutes
+183dec617f70 edgexfoundry/core-metadata-arm64:2.1.0 Up 4 minutes
+8d3945944f74 lfedge/ekuiper:1.3.1-alpine Up 4 minutes
+47e71a310782 edgexfoundry/support-notifications-arm64:2.1.0 Up 4 minutes
+404a9ee4f501 edgexfoundry/support-scheduler-arm64:2.1.0 Up 4 minutes
+2859afc4b612 consul:1.10.3 Up 4 minutes
+70d3a90e88a4 redis:6.2.6-alpine Up 4 minutes
+284ff5b3fa92 edgexfoundry/edgex-ui-arm64:2.1.0 Up 4 minutes
```
The EdgeX structure diagram clearly shows the purpose of each service:
-![EdgeX Architecture Diagram (Jun/12 2020)](./assets/EdgeX-Arch-Jun12-20.png.jpg)
+![EdgeX 2.1 Architecture Diagram](./assets/EdgeX_architecture.png)
There are the core services in the middle. Devices services will talk to the hardwares. Supporting services will inject rules and run actions scheduled. Application services will interact with frontend or external cloud services. All the well designed services are just launched with the one line of command!
@@ -85,19 +96,19 @@ Although the services are launched well, it is worth to test the servcies before
Curl is a command line tool of *nix systems to transfer data to a given URL and the basic tool to ping EdgeX services:
```
-$ curl http://localhost:48080/api/v1/ping
-pong
+$ curl http://localhost:59880/api/v2/ping
+{"apiVersion":"v2","timestamp":"Mon Jan 10 22:45:05 UTC 2022"}
-$ curl http://localhost:48081/api/v1/ping
-pong
+$ curl http://localhost:59881/api/v2/ping
+{"apiVersion":"v2","timestamp":"Mon Jan 10 22:45:32 UTC 2022"}
-$ curl http://localhost:48082/api/v1/ping
-pong
+$ curl http://localhost:59882/api/v2/ping
+{"apiVersion":"v2","timestamp":"Mon Jan 10 22:45:56 UTC 2022"}
```
Also docker-compose can be used to monitor logs:
```sh
-$ docker-compose -f docker-compose-geneva-redis-no-secty-arm64.yml logs -f {data|command|metadata}
+$ docker-compose -f docker-compose-no-secty-arm64.yml logs -f
```
@@ -128,4 +139,4 @@ EdgeX stack is up and running so that we can start making our own custom device
---
-Next: [How to develop custom device and app services](40_custom_services.md)
+Next: [How to develop custom device services](40_custom_device_services.md)
diff --git a/deployment/raspberry-pi-4/40_custom_device_services.md b/deployment/raspberry-pi-4/40_custom_device_services.md
new file mode 100644
index 00000000..461cfd80
--- /dev/null
+++ b/deployment/raspberry-pi-4/40_custom_device_services.md
@@ -0,0 +1,475 @@
+[To README](README.md)
+
+# 4. How to develop custom device service
+
+Now that we have got the core services for EdgeX running on our RPI we can look at how to implement our own device service.
+
+The EdgeX community offers C and go device service SDKs, we will use go for this exercise.
+
+
+## 4.1 How to use EdgeX device service SDK
+
+This chapter shows how to make an example device service based on EdgeX device service SDK, which offers basic capability to communicate with EdgeX core services. We will implement an "echo" feature so that it returns strings as same as it receives. With this example, readers can find API entries and handlers of EdgeX core services.
+
+Below resources can be used to learn more about the device service SDK:
+- https://docs.edgexfoundry.org/2.1/microservices/device/sdk/Ch-DeviceSDK/
+- https://docs.edgexfoundry.org/2.1/getting-started/Ch-GettingStartedSDK-Go/
+- https://github.com/edgexfoundry/device-sdk-go
+
+To make our own device service, readers should:
+- Clone EdgeX device service SDK
+- Relocate files
+- Edit configuration and Go files
+- Compile, launch, and test
+
+
+
+### 4.1.1 Create Stub Service
+
+For exercise purposes create folder called `repo` with a subfolder `device-echo` to hold our service. Other examples in this repository have better project structures to follow but for convenience and ease of discussion we will implement the entire service in main.go. But first some setup is needed.
+
+# Initialize Module and Install Device SDK
+
+This can be done with a few simple commands run in our service directory `device-echo`
+```shell
+$ go mod init main
+go: creating new go.mod: module main
+$ go get github.com/edgexfoundry/device-sdk-go/v2@v2.1.0
+go get: added github.com/edgexfoundry/device-sdk-go/v2 v2.1.0
+```
+
+# Create Service Stub
+
+In `main.go` add the following code using your favorite text editor. This will have build errors until the driver is implemented in the next step if you are using an IDE.
+
+```go
+package main
+
+import (
+ "fmt"
+ "github.com/edgexfoundry/device-sdk-go/v2"
+ sdkModels "github.com/edgexfoundry/device-sdk-go/v2/pkg/models"
+ "github.com/edgexfoundry/device-sdk-go/v2/pkg/startup"
+ "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger"
+ "github.com/edgexfoundry/go-mod-core-contracts/v2/common"
+ "github.com/edgexfoundry/go-mod-core-contracts/v2/models"
+ "time"
+)
+
+const (
+ serviceName string = "device-echo"
+)
+
+func main() {
+ sd := echoDriver{}
+ startup.Bootstrap(serviceName, device.Version, &sd)
+}
+
+```
+
+### 4.1.2 Add Driver
+
+We then need to add a driver as specified in the above links. This will be for a simple service that accepts string values in PUT commands, and returns the last value received on GETs. It will also send the current value along the EdgeX message bus every 5 seconds. The driver implementation is below, I placed it inline in main.go:
+
+```go
+type echoDriver struct {
+ lc logger.LoggingClient
+ asyncCh chan<- *sdkModels.AsyncValues
+ deviceCh chan<- []sdkModels.DiscoveredDevice
+ echoString string
+}
+
+// Initialize performs protocol-specific initialization for the device
+// service.
+func (ed *echoDriver) Initialize(
+ lc logger.LoggingClient,
+ asyncCh chan<- *sdkModels.AsyncValues,
+ deviceCh chan<- []sdkModels.DiscoveredDevice) error {
+
+ ed.lc = lc
+ ed.asyncCh = asyncCh
+ ed.deviceCh = deviceCh
+
+ ed.echoString = ""
+ go ed.Echo()
+
+ return nil
+}
+
+func (ed *echoDriver) Echo() {
+ tick := time.Tick(5000 * time.Millisecond)
+
+ for {
+ select {
+ case <-tick:
+ if ed.echoString != "" {
+ cValue, _ := sdkModels.NewCommandValue(
+ "echoString",
+ common.ValueTypeString,
+ ed.echoString)
+
+ cValueSlice := make([]*sdkModels.CommandValue, 0)
+ cValueSlice = append(cValueSlice, cValue)
+ d := sdkModels.AsyncValues{
+ DeviceName: "Echo-Device01",
+ CommandValues: cValueSlice,
+ }
+ ed.lc.Infof("sending command values from Echo: %+v", d)
+ ed.asyncCh <- &d
+ }
+ }
+ }
+}
+
+// HandleReadCommands triggers a protocol Read operation for the specified device.
+func (ed *echoDriver) HandleReadCommands(
+ deviceName string,
+ protocols map[string]models.ProtocolProperties,
+ reqs []sdkModels.CommandRequest) (res []*sdkModels.CommandValue, err error) {
+
+ ed.lc.Debug(fmt.Sprintf("echoDriver.HandleReadCommands: protocols: %v resource: %v attributes: %v", protocols, reqs[0].DeviceResourceName, reqs[0].Attributes))
+
+ if len(reqs) == 1 {
+ res = make([]*sdkModels.CommandValue, 1)
+ if reqs[0].DeviceResourceName == "echoString" {
+ cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeString, ed.echoString)
+ res[0] = cv
+ }
+ }
+
+ return
+}
+
+// HandleWriteCommands triggers a protocol Write operation for the specified device.
+func (ed *echoDriver) HandleWriteCommands(
+ deviceName string,
+ protocols map[string]models.ProtocolProperties,
+ reqs []sdkModels.CommandRequest,
+ params []*sdkModels.CommandValue) error {
+
+ var err error
+
+ for i, r := range reqs {
+ ed.lc.Info(fmt.Sprintf("echoDriver.HandleWriteCommands: protocols: %v, resource: %v, parameters: %v", protocols, reqs[i].DeviceResourceName, params[i]))
+
+ switch r.DeviceResourceName {
+ case "echoString":
+ if ed.echoString, err = params[i].StringValue(); err != nil {
+ err := fmt.Errorf("echoDriver.HandleWriteCommands; the data type of parameter should be string, parameter: %ed", params[0].String())
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// Stop the protocol-specific DS code to shutdown gracefully, or
+// if the force parameter is 'true', immediately. The driver is responsible
+// for closing any in-use channels, including the channel used to send async
+// readings (if supported).
+func (ed *echoDriver) Stop(force bool) error {
+ if ed.lc != nil {
+ ed.lc.Debugf("echoDriver.Stop called: force=%v", force)
+ }
+ return nil
+}
+
+// AddDevice is a callback function that is invoked
+// when a new Device associated with this Device Service is added
+func (ed *echoDriver) AddDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error {
+ ed.lc.Debugf("a new Device is added: %ed", deviceName)
+ return nil
+}
+
+// UpdateDevice is a callback function that is invoked
+// when a Device associated with this Device Service is updated
+func (ed *echoDriver) UpdateDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error {
+ ed.lc.Debugf("Device %ed is updated", deviceName)
+ return nil
+}
+
+// RemoveDevice is a callback function that is invoked
+// when a Device associated with this Device Service is removed
+func (ed *echoDriver) RemoveDevice(deviceName string, protocols map[string]models.ProtocolProperties) error {
+ ed.lc.Debugf("Device %ed is removed", deviceName)
+ return nil
+}
+```
+
+This is a fairly straightforward implementation of the device driver interface - the one wrinkle is our Echo method - this is run on a separate goroutine (in the background) by calling with `go ed.Echo()`
+
+### 4.1.3 Add Config files
+# res/configuration.toml
+```toml
+[Writable]
+ LogLevel = "INFO"
+ # Example InsecureSecrets configuration that simulates SecretStore for when EDGEX_SECURITY_SECRET_STORE=false
+ # InsecureSecrets are required for when Redis is used for message bus
+ [Writable.InsecureSecrets]
+ [Writable.InsecureSecrets.DB]
+ path = "redisdb"
+ [Writable.InsecureSecrets.DB.Secrets]
+ username = ""
+ password = ""
+
+[Service]
+ HealthCheckInterval = "10s"
+ Host = "localhost"
+ Port = 59999 # Device serivce are assigned the 599xx range
+ ServerBindAddr = "" # blank value defaults to Service.Host value
+ StartupMsg = "device echo started"
+ # MaxRequestSize limit the request body size in byte of put command
+ MaxRequestSize = 0 # value 0 unlimit the request size.
+ RequestTimeout = "20s"
+ [Service.CORSConfiguration]
+ EnableCORS = false
+ CORSAllowCredentials = false
+ CORSAllowedOrigin = "https://localhost"
+ CORSAllowedMethods = "GET, POST, PUT, PATCH, DELETE"
+ CORSAllowedHeaders = "Authorization, Accept, Accept-Language, Content-Language, Content-Type, X-Correlation-ID"
+ CORSExposeHeaders = "Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma, X-Correlation-ID"
+ CORSMaxAge = 3600
+
+[Registry]
+ Host = "localhost"
+ Port = 8500
+ Type = "consul"
+
+[Clients]
+ [Clients.core-data]
+ Protocol = "http"
+ Host = "localhost"
+ Port = 59880
+
+ [Clients.core-metadata]
+ Protocol = "http"
+ Host = "localhost"
+ Port = 59881
+
+[MessageQueue]
+ Protocol = "redis"
+ Host = "localhost"
+ Port = 6379
+ Type = "redis"
+ AuthMode = "usernamepassword" # required for redis messagebus (secure or insecure).
+ SecretName = "redisdb"
+ PublishTopicPrefix = "edgex/events/device" # /// will be added to this Publish Topic prefix
+ [MessageQueue.Optional]
+ # Default MQTT Specific options that need to be here to enable environment variable overrides of them
+ # Client Identifiers
+ ClientId = "device-echo"
+ # Connection information
+ Qos = "0" # Quality of Sevice 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" # Only used if Cert/Key file or Cert/Key PEMblock are specified
+
+# Example SecretStore configuration.
+# Only used when EDGEX_SECURITY_SECRET_STORE=true
+# Must also add `ADD_SECRETSTORE_TOKENS: "device-echo"` to vault-worker environment so it generates
+# the token and secret store in vault for "device-echo"
+[SecretStore]
+ Type = "vault"
+ Host = "localhost"
+ Port = 8200
+ Path = "device-echo/"
+ Protocol = "http"
+ RootCaCertPath = ""
+ ServerName = ""
+ SecretsFile = ""
+ DisableScrubSecretsFile = false
+ TokenFile = "/tmp/edgex/secrets/device-echo/secrets-token.json"
+ [SecretStore.Authentication]
+ AuthType = "X-Vault-Token"
+
+[Device]
+ DataTransform = true
+ MaxCmdOps = 128
+ MaxCmdValueLen = 256
+ ProfilesDir = "./res/profiles"
+ DevicesDir = "./res/devices"
+ UpdateLastConnected = false
+ AsyncBufferSize = 1
+ EnableAsyncReadings = true
+ Labels = []
+ UseMessageBus = true
+ [Device.Discovery]
+ Enabled = false
+ Interval = "30s"
+
+```
+
+# res/profiles/Echo-Driver.yaml
+```yaml
+apiVersion: "v2"
+name: "Echo-Device"
+manufacturer: "Simple Corp."
+model: "ED-01"
+labels:
+ - "sample"
+description: "Example of Echo Device"
+
+deviceResources:
+ -
+ name: "echoString"
+ isHidden: false
+ description: "Echo String"
+ properties:
+ valueType: "String"
+ readWrite: "RW"
+ defaultValue: ""
+
+deviceCommands:
+ -
+ name: "echoString"
+ isHidden: false
+ readWrite: "RW"
+ resourceOperations:
+ - { deviceResource: "echoString", defaultValue: "" }
+```
+
+# res/devices/echo-device.toml
+```toml
+[[DeviceList]]
+ Name = "Echo-Device01"
+ ProfileName = "Echo-Device"
+ Description = "Example of Echo Device"
+ Labels = [ "industrial" ]
+ [DeviceList.Protocols]
+ [DeviceList.Protocols.other]
+ Address = "echo01"
+ Port = "300"
+```
+
+
+### 4.1.4 Add Dockerfile
+
+Example below:
+
+```dockerfile
+FROM golang:1.17-alpine3.14 AS builder
+
+WORKDIR /temp
+
+LABEL license='SPDX-License-Identifier: Apache-2.0'
+
+RUN apk add --update --no-cache make git gcc libc-dev zeromq-dev libsodium-dev
+
+COPY go.mod .
+RUN go mod download
+
+COPY . .
+
+RUN go build -o ./device-echo
+
+FROM alpine:3.14 as final
+
+RUN apk add --update --no-cache zeromq
+
+WORKDIR /
+COPY --from=builder /temp/device-echo /device-echo
+COPY ./res/ /res
+
+EXPOSE 59999
+
+ENTRYPOINT ["/device-echo", "-cp=consul.http://edgex-core-consul:8500", "--registry", "--confdir=/res"]
+```
+### 4.1.5 Launch and test
+
+Our `device-echo` directory should now look like this:
+
+```shell
+$ tree
+.
+├── Dockerfile
+├── go.mod
+├── main.go
+└── res
+ ├── configuration.toml
+ ├── devices
+ │ └── echo-device.toml
+ ├── profiles
+ │ └── Echo-Driver.yaml
+
+3 directories, 6 files
+```
+
+Once this is done we can test building using `docker build` but to easily join it with our running services from previous steps we can create a file with the following compose snippet in `device-echo`
+
+```yaml
+networks:
+ edgex-network:
+ external: true
+services:
+ device-echo:
+ container_name: edgex-device-echo
+ depends_on:
+ - consul
+ - data
+ - metadata
+ environment:
+ CLIENTS_CORE_COMMAND_HOST: edgex-core-command
+ CLIENTS_CORE_DATA_HOST: edgex-core-data
+ CLIENTS_CORE_METADATA_HOST: edgex-core-metadata
+ CLIENTS_SUPPORT_NOTIFICATIONS_HOST: edgex-support-notifications
+ CLIENTS_SUPPORT_SCHEDULER_HOST: edgex-support-scheduler
+ DATABASES_PRIMARY_HOST: edgex-redis
+ EDGEX_SECURITY_SECRET_STORE: "false"
+ MESSAGEQUEUE_HOST: edgex-redis
+ REGISTRY_HOST: edgex-core-consul
+ SERVICE_HOST: edgex-device-echo
+ hostname: edgex-device-echo
+ build:
+ context: device-echo/.
+ networks:
+ edgex-network: { }
+ ports:
+ - 127.0.0.1:59999:59999/tcp
+ read_only: true
+ restart: always
+ security_opt:
+ - no-new-privileges:true
+ user: 2002:2001
+```
+From there we can attempt to run the service:
+
+```sh
+$ docker-compose up --build
+```
+
+Please open a new terminal, login to the RPI, and use **curl** to check the state of the device service:
+```sh
+# check device registration via metadata service
+$ curl localhost:59881/api/v2/device/name/Echo-Device01
+{"apiVersion":"v2","statusCode":200,"device":{"created":1642904140586,"modified":1642904140586,"id":"fed088a9-95d4-449c-b651-8681aacdb1ae","name":"Echo-Device01","description":"Example of Echo Device","adminState":"UNLOCKED","operatingState":"UP","labels":["industrial"],"serviceName":"device-echo","profileName":"Echo-Device","protocols":{"other":{"Address":"echo01","Port":"300"}}}}
+
+# check current value of echoString directly via device service
+$ curl localhost:59999/api/v2/device/name/Echo-Device01/echoString
+{"apiVersion":"v2","statusCode":200,"event":{"apiVersion":"v2","id":"b68134fb-5a64-4f90-92a6-b1c25a2458e2","deviceName":"Echo-Device01","profileName":"Echo-Device","sourceName":"echoString","origin":1642907383831644371,"readings":[{"id":"4aa8810f-be8e-4354-8330-58a60ce560ad","origin":1642907383831644371,"deviceName":"Echo-Device01","resourceName":"echoString","profileName":"Echo-Device","valueType":"String"}]}}
+
+# check current value of echoString via command service
+$ curl localhost:59882/api/v2/device/name/Echo-Device01/echoString
+{"apiVersion":"v2","statusCode":200,"event":{"apiVersion":"v2","id":"b68134fb-5a64-4f90-92a6-b1c25a2458e2","deviceName":"Echo-Device01","profileName":"Echo-Device","sourceName":"echoString","origin":1642907383831644371,"readings":[{"id":"4aa8810f-be8e-4354-8330-58a60ce560ad","origin":1642907383831644371,"deviceName":"Echo-Device01","resourceName":"echoString","profileName":"Echo-Device","valueType":"String"}]}}
+
+# set value for echoString via command service
+$ curl localhost:59882/api/v2/device/name/Echo-Device01/echoString -X PUT -d '{"echoString":"test completed"}'
+{"apiVersion":"v2","statusCode":200}
+
+# check value is set via command service
+$ curl localhost:59882/api/v2/device/name/Echo-Device01/echoString
+{"apiVersion":"v2","statusCode":200,"event":{"apiVersion":"v2","id":"e434eafe-345a-458b-96bc-9ab78a0d6035","deviceName":"Echo-Device01","profileName":"Echo-Device","sourceName":"echoString","origin":1642907593844892877,"readings":[{"id":"e53312e8-4d21-47ad-8614-20e07cca940c","origin":1642907593844892877,"deviceName":"Echo-Device01","resourceName":"echoString","profileName":"Echo-Device","valueType":"String","value":"test completed"}]}}
+
+```
+
+More info on the command service and various EdgeX apis can be found at the below links:
+- https://docs.edgexfoundry.org/2.1/api/core/Ch-APICoreCommand/
+- https://app.swaggerhub.com/search?type=API&query=%20edgex
+
+
+
+---
+
+Next: [How to develop custom app services](50_custom_app_services.md)
\ No newline at end of file
diff --git a/deployment/raspberry-pi-4/40_custom_services.md b/deployment/raspberry-pi-4/40_custom_services.md
deleted file mode 100644
index 379b7e94..00000000
--- a/deployment/raspberry-pi-4/40_custom_services.md
+++ /dev/null
@@ -1,591 +0,0 @@
-[To README](README.md)
-
-# 4. How to develop custom device and app services
-
-Launching the core services is the first step of EdgeX based development. However, how can we connect our own device and app services to the core services? How can our services communicate with the core services? EdgeX team offers SDKs for custom device and app services so that we can start from the SDKs.
-
-
-
-## 4.1 How to use EdgeX device service SDK
-
-This chapter shows how to make an example device service based on EdgeX device service SDK, which offers basic capability to communicate with EdgeX core services. We will implement an "echo" feature so that it returns strings as same as it receives. With this example, readers can find API entries and handlers of EdgeX core services.
-
-Below resources can be used to learn more about the device service SDK:
-- https://docs.edgexfoundry.org/1.2/microservices/device/sdk/Ch-DeviceSDK/
-- https://docs.edgexfoundry.org/1.2/getting-started/Ch-GettingStartedSDK-Go/
-- https://github.com/edgexfoundry/device-sdk-go
-
-To make our own device service, readers should:
-- Clone EdgeX device service SDK
-- Relocate files
-- Edit configuration and Go files
-- Compile, launch, and test
-
-
-
-### 4.1.1 Clone and config SDK
-
-The device service SDK can be cloned and configured so that we can build/test the service as follow:
-```sh
-# Clone SDK
-$ mkdir ~/go/src/github.com/edgexfoundry -p
-$ cd ~/go/src/github.com/edgexfoundry
-$ git clone --depth 1 \
- --branch v1.2.2 https://github.com/edgexfoundry/device-sdk-go.git
-
-# Relocate files
-$ mkdir ~/repo/device-simple -p
-$ cp -rf device-sdk-go/example/* ~/repo/device-simple/
-$ cp device-sdk-go/Makefile ~/repo/device-simple/
-$ cp device-sdk-go/version.go ~/repo/device-simple/
-$ cd ~/repo/device-simple
-
-# Check contents
-$ tree
-.
-├── cmd
-│ └── device-simple
-│ ├── Attribution.txt
-│ ├── Dockerfile
-│ ├── main.go
-│ └── res
-│ ├── configuration.toml
-│ ├── off.jpg
-│ ├── on.png
-│ ├── provisionwatcher.json
-│ └── Simple-Driver.yaml
-├── driver
-│ └── simpledriver.go
-├── Makefile
-├── README.md
-└── version.go
-
-4 directories, 12 files
-
-# Edit main.go: this command removes "example/"
-$ sed -i '/\"github.com\/edgexfoundry\/device-sdk-go\/example\/driver\"/c\\t\"main\/driver\"' ./cmd/device-simple/main.go
-
-# Edit Makefile: this command replaces "device-sdk-go" to "device-simple"
-$ sed -i '/GOFLAGS=-ldflags \"-X github.com\/edgexfoundry\/device-sdk-go.Version=$(VERSION)\"/c\GOFLAGS=-ldflags \"-X github.com\/edgexfoundry\/device-simple.Version=$(VERSION)\"' ./Makefile
-
-# Edit Makefile: these commands remove "example/"
-$ sed -i '/MICROSERVICES=example\/cmd\/device-simple\/device-simple/c\MICROSERVICES=cmd\/device-simple\/device-simple' ./Makefile
-$ sed -i '/example\/cmd\/device-simple\/device-simple:/c\cmd\/device-simple\/device-simple:' ./Makefile
-$ sed -i '/$(GO) build $(GOFLAGS) -o $@ .\/example\/cmd\/device-simple/c\\t$(GO) build $(GOFLAGS) -o $@ .\/cmd\/device-simple' ./Makefile
-
-# Enable Go module
-$ go mod init main
-$ echo "require (
- github.com/edgexfoundry/device-sdk-go v1.2.2
- github.com/edgexfoundry/go-mod-core-contracts v0.1.58
-)" >> go.mod
-
-# Test build
-$ make build
-$ file cmd/device-simple/device-simple
-cmd/device-simple/device-simple: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=azNP-8ouXJ3WjHlZkFSb/XRFJnCq3Yoz8hQqzObnB/Q1HqdTQy74eFvb6rvbrv/d3hOwwcqL_RurDo7Aesj, not stripped
-```
-
-
-
-### 4.1.2 Update handlers
-
-There is a file, which has all the handlers for us so that we can edit the code to interact with the core services:
-```sh
-# Update go file for handlers
-$ vi driver/simpledriver.go
-```
-
-In the go file, we need to change:
-- SimpleDriver struct
-- Initialize method
-- HandleReadCommands method
-- HandleWriteCommands method
-- HandleEcho method (new!)
-- Other parts shouldn't be changed
-
-```go
-type SimpleDriver struct {
- lc logger.LoggingClient
- asyncCh chan<- *dsModels.AsyncValues
- deviceCh chan<- []dsModels.DiscoveredDevice
- switchButton bool
- xRotation int32
- yRotation int32
- zRotation int32
- echoString string // Added
-}
-
-func (s *SimpleDriver) Initialize(
- lc logger.LoggingClient,
- asyncCh chan<- *dsModels.AsyncValues,
- deviceCh chan<- []dsModels.DiscoveredDevice) error {
-
- s.lc = lc
- s.asyncCh = asyncCh
- s.deviceCh = deviceCh
-
- s.echoString = "" // Added
- go s.Echo() // Added
-
- return nil
-}
-
-// A new function
-func (s *SimpleDriver) Echo(){
- tick := time.Tick(5000 * time.Millisecond)
-
- for {
- select{
- case <-tick:
- if s.echoString != "" {
- cValue := dsModels.NewStringValue(
- "echoString",
- int64(time.Now().Unix()), s.echoString)
-
- cValueSlice := make([]*dsModels.CommandValue, 0)
- cValueSlice = append(cValueSlice, cValue)
- d := dsModels.AsyncValues{
- DeviceName: "Simple-Device02",
- CommandValues: cValueSlice,
- }
- s.asyncCh <- &d
- }
- }
- }
-}
-
-func (s *SimpleDriver) HandleReadCommands(
- deviceName string,
- protocols map[string]contract.ProtocolProperties,
- reqs []dsModels.CommandRequest) (res []*dsModels.CommandValue, err error) {
-
- s.lc.Debug(fmt.Sprintf("SimpleDriver.HandleReadCommands: protocols: %v resource: %v attributes: %v", protocols, reqs[0].DeviceResourceName, reqs[0].Attributes))
-
- // Replaced the contents of this if block
- if len(reqs) == 1 {
- res = make([]*dsModels.CommandValue, 1)
- now := time.Now().UnixNano()
- if reqs[0].DeviceResourceName == "echoString" {
- cv := dsModels.NewStringValue(reqs[0].DeviceResourceName, now, s.echoString)
- res[0] = cv
- }
- }
-
- return
-}
-
-func (s *SimpleDriver) HandleWriteCommands(
- deviceName string,
- protocols map[string]contract.ProtocolProperties,
- reqs []dsModels.CommandRequest,
- params []*dsModels.CommandValue) error {
-
- var err error
-
- // Replaced the contents of this for block
- for i, r := range reqs {
- s.lc.Info(fmt.Sprintf("SimpleDriver.HandleWriteCommands: protocols: %v, resource: %v, parameters: %v", protocols, reqs[i].DeviceResourceName, params[i]))
-
- switch r.DeviceResourceName {
- case "echoString":
- if s.echoString, err = params[i].StringValue(); err != nil {
- err := fmt.Errorf("SimpleDriver.HandleWriteCommands; the data type of parameter should be Boolean, parameter: %s", params[0].String())
- return err
- }
- }
- }
-
- return nil
-}
-```
-
-Code formatting and test build:
-```sh
-$ gofmt -s -w .
-$ make build
-```
-
-
-
-### 4.1.3 Update config files
-
-As the code got updated, this device service can handle read and write requests from core services. The device service also needs to register itself to the core services and these are the files to be used for the registration:
-- ~/repo/device-simple/cmd/device-simple/res/**configuration.toml**
-- ~/repo/device-simple/cmd/device-simple/res/**Simple-Driver.yaml**
-
-For configuration.toml:
-```toml
-[Writable]
-LogLevel = 'INFO'
-
-[Service]
-BootTimeout = 30000
-CheckInterval = '10s'
-ClientMonitor = 15000
-Host = '172.17.0.1' # If the core services run as Docker containers
-Port = 49980 # Don't use a port being used!
-Protocol = 'http'
-StartupMsg = 'device simple started'
-Timeout = 20000
-ConnectRetries = 20
-Labels = []
-EnableAsyncReadings = true
-AsyncBufferSize = 16
-
-[Registry]
-Host = 'localhost'
-Port = 8500
-Type = 'consul'
-
-[Clients]
- [Clients.Data]
- Protocol = 'http'
- Host = 'localhost'
- Port = 48080
-
- [Clients.Metadata]
- Protocol = 'http'
- Host = 'localhost'
- Port = 48081
-
- [Clients.Logging]
- Protocol = 'http'
- Host = 'localhost'
- Port = 48061
-
-[Device]
- DataTransform = true
- InitCmd = ''
- InitCmdArgs = ''
- MaxCmdOps = 128
- MaxCmdValueLen = 256
- RemoveCmd = ''
- RemoveCmdArgs = ''
- ProfilesDir = './res'
- UpdateLastConnected = false
- [Device.Discovery]
- Enabled = false
- Interval = '30s'
-
-# Remote and file logging disabled so only stdout logging is used
-[Logging]
-EnableRemote = false
-File = ''
-
-# Pre-define Devices
-[[DeviceList]]
- Name = 'Simple-Device02'
- Profile = 'Simple-Device'
- Description = 'Example of Simple Device'
- Labels = [ 'industrial' ]
- [DeviceList.Protocols]
- [DeviceList.Protocols.other]
- Address = 'simple01'
- Port = '300'
-
-# Auto events are removed for the purpose of this tutorial
-```
-
-For Simple-Driver.yaml (be careful about the indentation!):
-```yaml
-name: "Simple-Device"
-manufacturer: "HP Corp."
-model: "ED-01"
-description: "Example of Simple Echo Device"
-
-deviceResources:
- -
- name: "echoString"
- description: "Echo String"
- properties:
- value:
- { type: "String", readWrite: "RW", defaultValue: "" }
- units:
- { type: "String", readWrite: "R", defaultValue: "" }
-
-deviceCommands:
- -
- name: "echoString"
- get:
- - { operation: "get", deviceResource: "echoString" }
- set:
- - { operation: "set", deviceResource: "echoString", parameter: "false" }
-
-coreCommands:
- -
- name: "echoString"
- get:
- path: "/api/v1/device/{deviceId}/echoString"
- responses:
- -
- code: "200"
- description: ""
- expectedValues: ["echoString"]
- -
- code: "503"
- description: "echo string unavailable"
- expectedValues: []
- put:
- path: "/api/v1/device/{deviceId}/echoString"
- parameterNames: ["echoString"]
- responses:
- -
- code: "200"
- description: ""
- -
- code: "503"
- description: "echo string unavailable"
- expectedValues: []
-```
-
-
-
-### 4.1.4 Launch and test
-
-The code was compiled well and the files for registration are ready. When the binary of this device service is executed, it does bootstrapping for the communication with the core services and uses the files to tell what it is and available commands. To excute:
-```sh
-$ cd cmd/device-simple
-$ ./device-simple
-...
-level=INFO ts=2020-09-15T10:48:51.813731783Z app=device-simple source=init.go:42 msg="Service clients initialize successful."
-level=INFO ts=2020-09-15T10:48:51.814815574Z app=device-simple source=service.go:83 msg="Device Service device-simple doesn't exist, creating a new one"
-...
-```
-
-Please open a new terminal, login to the RPI, and use **curl** to check the state of the device service:
-```sh
-# Basic info of the device service.
-$ curl http://localhost:48081/api/v1/addressable -X GET -s | jq '.[] | {name,address,port}'
-{
- "name": "device-simple",
- "address": "172.17.0.1",
- "port": 49980
-}
-
-# Returned IDs of our device service may vary.
-$ curl http://localhost:48081/api/v1/device/name/Simple-Device02 -X GET -s -S | jq '.name,.id,.service.id'
-"Simple-Device02"
-"9be2790a-dab9-447f-ac59-74505527252f"
-"20a1fec0-de09-4af8-bb20-000b33573f9e"
-
-# Returned ID of the value descriptor may vary.
-$ curl http://localhost:48080/api/v1/valuedescriptor/name/echoString -X GET -s -S | jq '.id'
-"0242e148-a5d3-40a5-a850-c5af5dd8456f"
-
-# To find out the available device name and id. The retured URL may vary.
-$ curl http://localhost:48082/api/v1/device -s | jq '.[] | select(.name == "Simple-Device02").id,.name'
-"9be2790a-dab9-447f-ac59-74505527252f"
-"Simple-Device02"
-
-# To find out the available commands id. The returned URL may vary.
-$ curl http://localhost:48082/api/v1/device -s | jq '.[] | select(.name == "Simple-Device02").commands | .[] | .id'
-"80f2ba5e-73d1-4130-b039-c158a2f43388"
-
-# With the commands above, we could find the ID of the device/command
-# Device ID: "9be2790a-dab9-447f-ac59-74505527252f"
-# Command ID: "80f2ba5e-73d1-4130-b039-c158a2f43388"
-
-# To change echoString value, let's store the IDs first:
-$ DEVICE_ID=$(curl http://localhost:48082/api/v1/device -s | jq -r '.[] | select(.name == "Simple-Device02").id')
-$ COMMAND_ID=$(curl http://localhost:48082/api/v1/device -s | jq -r '.[] | select(.name == "Simple-Device02").commands | .[] | .id')
-
-# Query with the gathered IDs:
-$ curl http://localhost:48082/api/v1/device/$DEVICE_ID/command/$COMMAND_ID \
- -s \
- -X PUT \
- -H 'Content-Type: application/json' \
- -H 'cache-control: no-cache' \
- -d '{"echoString": "HELLO"}'
-
-# To check the value changed in 3 different ways
-
-# Asking to the core data service
-$ curl http://localhost:48080/api/v1/reading/device/Simple-Device02/1 -X GET -s -S | json_pp
-
-# Asking to the core command service
-$ curl http://localhost:48082/api/v1/device/name/Simple-Device02/command/echoString -X GET | json_pp
-
-# Asking to the device service itself
-$ curl http://localhost:49980/api/v1/device/name/Simple-Device02/echoString -X GET -s -S | json_pp
-
-...
-"name" : "echoString",
-"value" : "HELLO",
-...
-
-# To check the latest 10 async events/readings of the device service via the core data service
-$ curl -s http://localhost:48080/api/v1/event/device/Simple-Device02/10 | json_pp
-```
-
-More APIs can be found from:
-- https://docs.edgexfoundry.org/1.2/api/core/Ch-APICoreCommand/
-- https://app.swaggerhub.com/search?type=API&query=%20edgex
-
-
-
-## 4.2 How to use EdgeX app functions SDK
-
-Our first custom device service works good with the EdgeX services. Now, it is the time to create our own custom app service, which gets messages from the device service via the core data.
-
-To make our own app service, readers should:
-- Clone EdgeX app functions SDK
-- Edit the configuration file
-- Compile, launch, and test
-
-EdgeX foundry offers plenty of documents as well:
-- https://docs.edgexfoundry.org/1.2/getting-started/ApplicationFunctionsSDK/
-- https://docs.edgexfoundry.org/1.2/microservices/application/ApplicationServices/
-- https://docs.edgexfoundry.org/1.2/examples/AppServiceExamples/
-- https://docs.edgexfoundry.org/1.2/getting-started/ApplicationFunctionsSDK/
-- https://github.com/edgexfoundry/app-functions-sdk-go
-- https://github.com/edgexfoundry/edgex-examples/blob/master/application-services/custom/simple-filter-xml/main.go
-
-
-
-### 4.2.1 Build app functions SDK example
-
-Now, we can clone and build one of the app functions SDK examples:
-```sh
-# Originally, the app functions SDK requires the libzmq library and sometimes we need to build it from the source code if we use OS doesn't deliver the library package out of the box. However, Ubuntu server 20.10 comes with the libzmq3-dev package and it is already installed in the previous chapter.
-
-$ cd ~/repo
-$ git clone https://github.com/edgexfoundry/edgex-examples
-$ cp -rf edgex-examples/application-services/custom/simple-filter-xml .
-$ cd simple-filter-xml
-$ tree
-.
-├── Dockerfile
-├── EdgeX Application Function SDK Device Name.postman_collection.json
-├── EdgeX Applications Function SDK.postman_collection.json
-├── go.mod
-├── main.go
-├── Makefile
-└── res
- └── configuration.toml
-
-1 directory, 7 files
-```
-
-Change go.mod of simple-filter-xml to be:
-```go
-go 1.15
-
-require (
- github.com/edgexfoundry/app-functions-sdk-go v1.2.0
-)
-```
-
-Test build:
-```sh
-$ make build
-CGO_ENABLED=1 GO111MODULE=on go build -o app-service
-```
-
-If test build fails, try this and build again:
-```sh
-$ go get github.com/rjeczalik/pkgconfig/cmd/pkg-config
-```
-
-
-
-### 4.2.2 Edit the configuration file
-
-The app functions SDK offers handlers and filters for the message stream of EdgeX core data service. Examples of app functions SDK show various use cases but we can start from the simplest one. In the previous step, the example is already compiled but we need to take a look into the main.go and res/configuration.toml files.
-
-The **res/configuration.toml** is the configuration file for this app function. Target message source can be specified as long as other settings. The sub section **ApplicationSettings** should have device names as target message sources. Since our device service has the name as **Simple-Device02** in its configuration.toml, we need to write the same name for DeviceNames as below.
-
-```toml
-[ApplicationSettings]
-DeviceNames = "Simple-Device02"
-```
-
-The **main.go** is the place where the actual handlers and filters can be written. The structure and flow are straightforward. In the main function, it initializes the app SDK with a secret key. Then it reads the configuration.toml file and DeviceNames variable. Pipeline is configured with chained functions for message handling and filtering. The printXMLToConsole function is specified at the end of the chained functions so that we can write some code there to use the data filtered from the pipeline so that the incoming messages can be passed to other go routines as we normally write Go code.
-
-```go
-func main() {
- // turn off secure mode for examples. Not recommended for production
- os.Setenv("EDGEX_SECURITY_SECRET_STORE", "false")
-
- // 1) First thing to do is to create an instance of the EdgeX SDK and initialize it.
- edgexSdk := &appsdk.AppFunctionsSDK{ServiceKey: serviceKey}
- if err := edgexSdk.Initialize(); err != nil {
- edgexSdk.LoggingClient.Error(fmt.Sprintf("SDK initialization failed: %v\n", err))
- os.Exit(-1)
- }
-
- // 2) shows how to access the application's specific configuration settings.
- deviceNames, err := edgexSdk.GetAppSettingStrings("DeviceNames")
- if err != nil {
- edgexSdk.LoggingClient.Error(err.Error())
- os.Exit(-1)
- }
- edgexSdk.LoggingClient.Info(fmt.Sprintf("Filtering for devices %v", deviceNames))
-
- // 3) This is our pipeline configuration, the collection of functions to
- // execute every time an event is triggered.
- // Also, "TransformToXML" can be edited as "TransformToJSON" for JSON format.
- edgexSdk.SetFunctionsPipeline(
- transforms.NewFilter(deviceNames).FilterByDeviceName,
- transforms.NewConversion().TransformToXML,
- printXMLToConsole,
- )
-
- // 4) Lastly, we'll go ahead and tell the SDK to "start" and begin listening for events
- // to trigger the pipeline.
- err = edgexSdk.MakeItRun()
- if err != nil {
- edgexSdk.LoggingClient.Error("MakeItRun returned error: ", err.Error())
- os.Exit(-1)
- }
-
- // Do any required cleanup here
-
- os.Exit(0)
-}
-```
-
-### 4.2.3 Customize app functions SDK example
-
-Since we already compiled this example (and main.go is not changed), we can just launch it:
-```sh
-$ ./app-service
-...
-level=INFO ts=2020-09-18T10:10:22.624535012Z app=sampleFilterXml source=server.go:350 msg="Starting HTTP Web Server on port :48095"
-...
-
-b53ae300-6bcc-42a4-bf3b-f58165d890f30Simple-Device021600422912988016004229129866953200305fccf-e5bd-45ba-a0f5-5fe9002447510016004229120Simple-Device02echoStringHELLOString
-```
-
-
-
-As our device service keeps sending events with the value "HELLO" once every 5 seconds, we can see the XML messages. To test how the message changes, we can send commands to the device service via the core command service.
-
-Let's open a new terminal and:
-```sh
-# Let's store the IDs as same as earlier
-$ DEVICE_ID=$(curl http://localhost:48082/api/v1/device -s | jq -r '.[] | select(.name == "Simple-Device02").id')
-$ COMMAND_ID=$(curl http://localhost:48082/api/v1/device -s | jq -r '.[] | select(.name == "Simple-Device02").commands | .[] | .id')
-
-# Query with the gathered IDs (the DEVICE_ID and COMMAND_ID variables are defined earlier):
-$ curl http://localhost:48082/api/v1/device/$DEVICE_ID/command/$COMMAND_ID \
- -s \
- -X PUT \
- -H 'Content-Type: application/json' \
- -H 'cache-control: no-cache' \
- -d '{"echoString": "HELLO, WORLD"}'
-```
-
-Then, the app service will print out XML message with the new string "HELLO, WORLD" and this shows the new string went throught the services well.
-
-
-
-## Conclusion
-
-In this tutorial, we prepared Ubuntu server 20.10 on RPI, launched EdgeX services, and created the custom device and app services. Although there are many unwritten details to keep it simple, now we know about the flow of EdgeX service development - where the important files are and how to build/test. To me, the benefit of using EdgeX is that all the queries get stored without concern of DB management and that is a huge plus if we deploy tons of edge devices everywhere. As we could see, running EdgeX on RPI is not difficult at all. Everything is ready there for us and the next exciting IoT projects!
-
-
-
----
-
-[To README](README.md)
diff --git a/deployment/raspberry-pi-4/50_custom_app_services.md b/deployment/raspberry-pi-4/50_custom_app_services.md
new file mode 100644
index 00000000..5a8ec4a9
--- /dev/null
+++ b/deployment/raspberry-pi-4/50_custom_app_services.md
@@ -0,0 +1,330 @@
+[To README](README.md)
+
+## 5 How to use EdgeX app functions SDK
+
+Our first custom device service works good with the EdgeX services. Now, it is the time to create our own custom app service, which gets messages from the device service via the core data.
+
+To make our own app service, readers should:
+- Clone EdgeX app functions SDK
+- Edit the configuration file
+- Compile, launch, and test
+
+EdgeX foundry offers plenty of documents as well:
+- https://docs.edgexfoundry.org/2.1/getting-started/ApplicationFunctionsSDK/
+- https://docs.edgexfoundry.org/2.1/microservices/application/ApplicationServices/
+- https://docs.edgexfoundry.org/2.1/examples/AppServiceExamples/
+- https://docs.edgexfoundry.org/2.1/getting-started/ApplicationFunctionsSDK/
+- https://github.com/edgexfoundry/app-functions-sdk-go
+- https://github.com/edgexfoundry/edgex-examples/blob/master/application-services/custom/simple-filter-xml/main.go
+
+
+
+### 5.1 Build app functions SDK example
+
+For exercise purposes we will work in the folder `repo` with a new subfolder `app-echo` to hold our service. Other examples in this repository have better project structures to follow but for convenience and ease of discussion we will implement the entire service in main.go. But first some setup is needed. We are using the `simple-filter-xml` example as the basis for our service.
+
+# Initialize Module and Install App Function SDK
+
+This can be done with a few simple commands run in our service directory `app-echo`
+```shell
+$ go mod init main
+go: creating new go.mod: module main
+$ go get github.com/edgexfoundry/app-functions-sdk-go/v2@v2.1.0
+go get: added github.com/edgexfoundry/app-functions-sdk-go/v2 v2.1.0
+```
+
+# Add Service
+
+Create a new file `main.go` with the following content:
+
+```go
+package main
+
+import (
+ "errors"
+ "fmt"
+ "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg"
+ "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces"
+ "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms"
+ "os"
+)
+
+const (
+ serviceKey = "app-echo"
+)
+
+func main() {
+ // turn off secure mode for examples. Not recommended for production
+ _ = os.Setenv("EDGEX_SECURITY_SECRET_STORE", "false")
+
+ // 1) First thing to do is to create an new instance of an EdgeX Application Service.
+ service, ok := pkg.NewAppService(serviceKey)
+ if !ok {
+ os.Exit(-1)
+ }
+
+ // Leverage the built in logging service in EdgeX
+ lc := service.LoggingClient()
+
+ // 2) shows how to access the application's specific configuration settings.
+ deviceNames, err := service.GetAppSettingStrings("DeviceNames")
+ if err != nil {
+ lc.Error(err.Error())
+ os.Exit(-1)
+ }
+ lc.Info(fmt.Sprintf("Filtering for devices %v", deviceNames))
+
+ // 3) This is our pipeline configuration, the collection of functions to
+ // execute every time an event is triggered.
+ if err := service.SetFunctionsPipeline(
+ transforms.NewFilterFor(deviceNames).FilterByDeviceName,
+ transforms.NewConversion().TransformToXML,
+ printXMLToConsole,
+ ); err != nil {
+ lc.Errorf("SetFunctionsPipeline returned error: %s", err.Error())
+ os.Exit(-1)
+ }
+
+ // 4) Lastly, we'll go ahead and tell the SDK to "start" and begin listening for events
+ // to trigger the pipeline.
+ err = service.MakeItRun()
+ if err != nil {
+ lc.Errorf("MakeItRun returned error: %s", err.Error())
+ os.Exit(-1)
+ }
+
+ // Do any required cleanup here
+
+ os.Exit(0)
+}
+
+func printXMLToConsole(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) {
+ // Leverage the built in logging service in EdgeX
+ lc := ctx.LoggingClient()
+
+ if data == nil {
+ return false, errors.New("printXMLToConsole: No data received")
+ }
+
+ xml, ok := data.(string)
+ if !ok {
+ return false, errors.New("printXMLToConsole: Data received is not the expected 'string' type")
+ }
+
+ lc.Debug(xml)
+ ctx.SetResponseData([]byte(xml))
+ return true, xml
+}
+
+```
+
+
+
+### 5.2 Update Configuration
+
+The app functions SDK offers handlers and filters for the message stream of EdgeX core data service. Examples of app functions SDK show various use cases but we will start with a very simple one - echoing events coming through the message bus for our new device `Device-Echo01` only.
+
+The **res/configuration.toml** is the configuration file for this app function. The sub section **ApplicationSettings** should have device names as target message sources. Since our device service has the name **Echo-Device01** registered, we need to write the same name for DeviceNames as below.
+
+```toml
+[Writable]
+ LogLevel = "INFO"
+ [Writable.StoreAndForward]
+ Enabled = false
+ RetryInterval = "5m"
+ MaxRetryCount = 10
+ [Writable.InsecureSecrets]
+ [Writable.InsecureSecrets.DB]
+ path = "redisdb"
+ [Writable.InsecureSecrets.DB.Secrets]
+ username = ""
+ password = ""
+
+[Service]
+ HealthCheckInterval = "10s"
+ Host = "localhost"
+ Port = 59998 # Adjust if running multiple examples at the same time to avoid duplicate port conflicts
+ ServerBindAddr = "" # if blank, uses default Go behavior https://golang.org/pkg/net/#Listen
+ StartupMsg = "This is a sample Filter/XML Transform Application Service"
+ RequestTimeout = "30s"
+ MaxRequestSize = 0
+ MaxResultCount = 0
+
+[Registry]
+ Host = "localhost"
+ Port = 8500
+ Type = "consul"
+
+# Database is require when Store and Forward is enabled
+[Database]
+ Type = "redisdb"
+ Host = "localhost"
+ Port = 6379
+ Timeout = "30s"
+
+# SecretStore is required when Store and Forward is enabled and running with security
+# so Database credentials can be pulled from Vault.
+# Note when running in docker from compose file set the following environment variables:
+# - SecretStore_Host: edgex-vault
+[SecretStore]
+ Type = "vault"
+ Host = "localhost"
+ Port = 8200
+ Path = "app-simple-filter-xml/"
+ Protocol = "http"
+ TokenFile = "/tmp/edgex/secrets/app-simple-filter-xml/secrets-token.json"
+ RootCaCertPath = ""
+ ServerName = ""
+ [SecretStore.Authentication]
+ AuthType = "X-Vault-Token"
+
+[Clients]
+ [Clients.core-metadata]
+ Protocol = "http"
+ Host = "localhost"
+ Port = 59881
+
+# Choose either the http trigger or edgex-messagebus trigger
+
+#[Trigger]
+#Type="http"
+
+[Trigger]
+ Type="edgex-messagebus"
+ [Trigger.EdgexMessageBus]
+ Type = "redis"
+ [Trigger.EdgexMessageBus.SubscribeHost]
+ Host = "localhost"
+ Port = 6379
+ Protocol = "redis"
+ SubscribeTopics="edgex/events/#"
+ [Trigger.EdgexMessageBus.PublishHost]
+ Host = "localhost"
+ Port = 6379
+ Protocol = "redis"
+ PublishTopic="example"
+
+# App Service specifc simple settings
+# Great for single string settings. For more complex structured custom configuration
+# See https://docs.edgexfoundry.org/2.0/microservices/application/AdvancedTopics/#custom-configuration
+[ApplicationSettings]
+ DeviceNames = "Echo-Device01"
+```
+
+### 5.3 Add Dockerfile
+
+We can use the same dockerfile as with device services (both require ZeroMQ present to build/run). Just need to specify a different port.
+
+```dockerfile
+FROM golang:1.17-alpine3.14 AS builder
+
+WORKDIR /temp
+
+LABEL license='SPDX-License-Identifier: Apache-2.0'
+
+RUN apk add --update --no-cache make git gcc libc-dev zeromq-dev libsodium-dev
+
+COPY go.mod .
+RUN go mod download
+
+COPY . .
+
+RUN go build -o ./app-echo
+
+FROM alpine:3.14 as final
+
+RUN apk add --update --no-cache zeromq
+
+WORKDIR /
+COPY --from=builder /temp/app-echo /app-echo
+COPY ./res/ /res
+
+EXPOSE 59798
+
+ENTRYPOINT ["/app-echo", "-cp=consul.http://edgex-core-consul:8500", "--registry", "--confdir=/res"]
+```
+
+### 5.4 Launch and test
+
+Our `app-echo` directory should now look like this:
+
+```shell
+$ tree
+.
+├── Dockerfile
+├── go.mod
+├── go.sum
+├── main.go
+└── res
+ └── configuration.toml
+
+1 directory, 5 files
+
+```
+
+Once this is done we can test building using `docker build` but to easily join it with our running services from previous steps we can create a file with the following compose snippet in `device-echo`
+
+```yaml
+networks:
+ edgex-network:
+ external: true
+services:
+ app-echo:
+ container_name: edgex-app-echo
+ depends_on:
+ - consul
+ - data
+ - metadata
+ environment:
+ CLIENTS_CORE_COMMAND_HOST: edgex-core-command
+ CLIENTS_CORE_DATA_HOST: edgex-core-data
+ CLIENTS_CORE_METADATA_HOST: edgex-core-metadata
+ CLIENTS_SUPPORT_NOTIFICATIONS_HOST: edgex-support-notifications
+ CLIENTS_SUPPORT_SCHEDULER_HOST: edgex-support-scheduler
+ DATABASES_PRIMARY_HOST: edgex-redis
+ EDGEX_SECURITY_SECRET_STORE: "false"
+ TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST: edgex-redis
+ TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST: edgex-redis
+ REGISTRY_HOST: edgex-core-consul
+ SERVICE_HOST: edgex-app-echo
+ hostname: edgex-app-echo
+ build:
+ context: app-echo/.
+ networks:
+ edgex-network: { }
+ ports:
+ - 127.0.0.1:59798:59798/tcp
+ read_only: true
+ restart: always
+ security_opt:
+ - no-new-privileges:true
+ user: 2002:2001
+```
+From there we can attempt to run the service:
+
+```sh
+$ docker-compose up --build
+```
+
+Once services start the logs should be fairly quiet - we will need to set an echo value before the events start sending:
+
+```shell
+$ curl localhost:59882/api/v2/device/name/Echo-Device01/echoString -X PUT -d '{"echoString":"test completed"}'
+{"apiVersion":"v2","statusCode":200}
+```
+
+After this is set we should start seeing pairs of log messages like this:
+
+```
+edgex-device-echo | level=INFO ts=2022-01-23T13:25:02.457868491Z app=device-echo source=main.go:66 msg="sending command values from Echo: {DeviceName:Echo-Device01 SourceName: CommandValues:[DeviceResource: echoString, String: test completed]}"
+edgex-app-echo | level=INFO ts=2022-01-23T13:25:02.458789589Z app=app-echo source=main.go:74 msg="v2fb9ecfe1-57bb-4f1b-8d9b-408526e41951Echo-Device01Echo-DeviceechoString1642944302457960155d7365a0c-9c91-40a5-9333-a4ab7fe5307d1642944302457960155Echo-Device01echoStringEcho-DeviceStringtest completed"
+```
+## Conclusion
+
+I hope you have enjoyed this introduction. For a recap, we prepared Ubuntu server 21.10 on RPI, launched EdgeX services, and created custom device and app services. While these are simplistic examples they should demonstrate how custom services can be connected to the EdgeX ecosystem and the way data flows between them on the message bus. Feel free to explore the other examples in this repository for further inspiration.
+
+
+
+---
+
+[To README](README.md)
diff --git a/deployment/raspberry-pi-4/README.md b/deployment/raspberry-pi-4/README.md
index 0e537ce3..2974069c 100644
--- a/deployment/raspberry-pi-4/README.md
+++ b/deployment/raspberry-pi-4/README.md
@@ -23,7 +23,7 @@ Ubuntu on ARM:
EdgeX:
- https://www.edgexfoundry.org/
- https://github.com/edgexfoundry
-- https://docs.edgexfoundry.org/1.2/examples/LinuxTutorial/LinuxTutorial/
+- https://docs.edgexfoundry.org/2.1/walk-through/Ch-Walkthrough/
@@ -42,5 +42,5 @@ EdgeX:
- [How to install Ubuntu on RPI](10_install_ubuntu.md)
- [How to install packages required for EdgeX development](20_install_packages.md)
- [How to install and launch EdgeX](30_install_edgex.md)
-- [How to develop custom device and app services](40_custom_services.md)
+- [How to develop custom device and app services](40_custom_device_services.md)
diff --git a/deployment/raspberry-pi-4/assets/01_download.png b/deployment/raspberry-pi-4/assets/01_download.png
index fc71125e..6df31c11 100644
Binary files a/deployment/raspberry-pi-4/assets/01_download.png and b/deployment/raspberry-pi-4/assets/01_download.png differ
diff --git a/deployment/raspberry-pi-4/assets/EdgeX-Arch-Jun12-20.png.jpg b/deployment/raspberry-pi-4/assets/EdgeX-Arch-Jun12-20.png.jpg
deleted file mode 100644
index 04a2e3a3..00000000
Binary files a/deployment/raspberry-pi-4/assets/EdgeX-Arch-Jun12-20.png.jpg and /dev/null differ
diff --git a/deployment/raspberry-pi-4/assets/EdgeX_architecture.png b/deployment/raspberry-pi-4/assets/EdgeX_architecture.png
new file mode 100644
index 00000000..f87eb65d
Binary files /dev/null and b/deployment/raspberry-pi-4/assets/EdgeX_architecture.png differ
diff --git a/deployment/raspberry-pi-4/assets/configuration.toml b/deployment/raspberry-pi-4/assets/configuration.toml
deleted file mode 100644
index 8f2d72c7..00000000
--- a/deployment/raspberry-pi-4/assets/configuration.toml
+++ /dev/null
@@ -1,75 +0,0 @@
-[Writable]
-LogLevel = 'INFO'
-
-[Service]
-BootTimeout = 30000
-CheckInterval = '10s'
-ClientMonitor = 15000
-Host = 'localhost'
-Port = 49990
-Protocol = 'http'
-StartupMsg = 'device simple started'
-Timeout = 20000
-ConnectRetries = 20
-Labels = []
-EnableAsyncReadings = true
-AsyncBufferSize = 16
-
-[Registry]
-Host = 'localhost'
-Port = 8500
-Type = 'consul'
-
-[Clients]
- [Clients.Data]
- Protocol = 'http'
- Host = 'localhost'
- Port = 48080
-
- [Clients.Metadata]
- Protocol = 'http'
- Host = 'localhost'
- Port = 48081
-
- [Clients.Logging]
- Protocol = 'http'
- Host = 'localhost'
- Port = 48061
-
-[Device]
- DataTransform = true
- InitCmd = ''
- InitCmdArgs = ''
- MaxCmdOps = 128
- MaxCmdValueLen = 256
- RemoveCmd = ''
- RemoveCmdArgs = ''
- ProfilesDir = './res'
- UpdateLastConnected = false
- [Device.Discovery]
- Enabled = false
- Interval = '30s'
-
-# Remote and file logging disabled so only stdout logging is used
-[Logging]
-EnableRemote = false
-File = ''
-
-# Pre-define Devices
-[[DeviceList]]
- Name = 'Simple-Device01'
- Profile = 'Simple-Device'
- Description = 'Example of Simple Device'
- Labels = [ 'industrial' ]
- [DeviceList.Protocols]
- [DeviceList.Protocols.other]
- Address = 'simple01'
- Port = '300'
- [[DeviceList.AutoEvents]]
- Frequency = '10s'
- OnChange = false
- Resource = 'Switch'
- [[DeviceList.AutoEvents]]
- Frequency = '30s'
- OnChange = false
- Resource = 'Image'
diff --git a/device-services/device-random/cmd/res/configuration.toml b/device-services/device-random/cmd/res/configuration.toml
index d0afedba..96149bc5 100644
--- a/device-services/device-random/cmd/res/configuration.toml
+++ b/device-services/device-random/cmd/res/configuration.toml
@@ -56,8 +56,8 @@ PublishTopicPrefix = 'edgex/events/device' # //