在EdgeX Foundry中有南向和北向的概念
在數據獲取的過程中我自己大略畫了一個流程圖
在數據獲取的過程中 南向就是數據的產生方,北向就是最終數據的利用方
在這個系列的文章中,我沒有用官方的虛擬設備服務(device-virtual),因為我個人覺得那樣的描述不能很好的體會EdgeX中的各種概念,

這個圖可能沒開發過的也是看不懂的,接下來一點一點的把上圖的內容盡可能的說清楚。
從南向北說
最南端是設備,比如溫度傳感器,門禁控制器,智能電表,攝像頭等等的可以產生數據的裝置甚至是軟件
我們先來三台機器(用三台的原因是這樣更容易把概念區分開)
192.168.1.11(簡稱host1) 設備
192.168.1.12(簡稱host2) 運行EdgeX的主機
192.168.1.13 (簡稱host3)運行deviceService的主機
有小伙伴留言問我這幾個主機的區別,為什么要分開:
在此澄清一下, 整個edgexFoundry的部署方式是可以有很多方式的, 之所以我選擇在這里都分開部署, 是為了寫文章的時候更加清晰
那么先講一下edgexfoundry的組成
1 edgexfoundry的核心微服務,官方已經打包成docker鏡像了,並且有一個docker-compose.yml可以用來啟動所有的edgexfoundry核心服務,
這些核心服務,每個都是一個微服務,是用httpAPI的方式相互調用的,那么這些微服務在生產中,你懂得。。。 你怎么弄都可以咯
本文中講用簡單的PHP程序當做數據的產生器也就是設備, 原因很簡單, Ubuntu安裝Apache + PHP 的環境非常簡單
我也盡可能的同步的用go語言來實現相同的功能
先實現最簡單的功能, 沒隔10秒產生一個[1-127] 的隨機數
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for {
time.Sleep(time.Second * 3)
fmt.Println(rand.Intn(128) + 1)
}
}
先不要糾結這個小程序的功能,先有在慢慢改
從根據圖上說, 要 能用http請求或者是mqtt的方式把產生的隨機數發送上來,我們的案例就以一個http的方式來實現發送產生的數據到設備服務(deviceService)
修改上面的代碼, 主要功能是把產生的隨機數POST到一個指定的url
package main import ( "fmt" "math/rand" "net/http" "strings" "time" ) func main() { for { time.Sleep(time.Second * 3) i := rand.Intn(128) + 1 fmt.Println(i) url := "http://host1:1210/bye" //請求地址 contentType := "application/json" //參數,多個用&隔開 data := strings.NewReader(fmt.Sprintf("%d", i)) resp, _ := http.Post(url, contentType, data) fmt.Println(resp) } }
因為現在還么有開始寫deviceService 所以寫了一個簡單的服務端用來驗證, 就是把接收到的隨機數追加到一個文件中, 代碼中的context.txt文件是要和main.go同一個目錄, 並手動創建
這不是必要的內容, 只是用來驗證上面產生數字並能爭取發送出來而已
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/bye", sayBye)
server := &http.Server{
Addr: ":1210",
WriteTimeout: time.Second * 3, //設置3秒的寫超時
Handler: mux,
}
log.Println("Starting v3 httpserver")
log.Fatal(server.ListenAndServe())
}
func sayBye(w http.ResponseWriter, r *http.Request) {
str, _ := readBodyAsString(w, r)
fmt.Println(str)
currentPath, _ := os.Getwd()
//這里要自己建一個context.txt文件
appendToFile(currentPath+"/context.txt", str+"\n")
}
func readBodyAsString(writer http.ResponseWriter, request *http.Request) (string, error) {
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return "", err
}
if len(body) == 0 {
return "", fmt.Errorf("no request body provided")
}
return string(body), nil
}
func appendToFile(fileName string, content string) error {
// 以只寫的模式,打開文件
f, err := os.OpenFile(fileName, os.O_WRONLY, 0644)
if err != nil {
fmt.Println("cacheFileList.yml file create failed. err: " + err.Error())
} else {
// 查找文件末尾的偏移量
n, _ := f.Seek(0, os.SEEK_END)
// 從末尾的偏移量開始寫入內容
_, err = f.WriteAt([]byte(content), n)
}
defer f.Close()
return err
}
到現在一個產生隨機數的設備就已經有了, 你可以當它是溫度或者濕度,或者其他的產生數字的設備就可以了
接下來是設備服務
設備服務主要參考
name: "rand-numeric" #舊的值是 sample-numeric manufacturer: "Intel Corp." model: "Some 3rd party App sending int & floats" labels: - "rest"
- "float64"
- "int64" description: "REST Device that sends in ints and floats" deviceResources: - name: int description: "Int64 value" properties: value: { type: "Int64", readWrite: "W" , mediaType : "application/json" } #舊的值是 text/plain, 寫成json的原因是我們設備上傳的數據使用json頭,我沒有嘗試不一樣會怎么樣, 反正我是改了 units: { type: "String", readWrite: "R"} - name: float description: "Float64 value" properties: value: { type: "Float64", readWrite: "W" , mediaType : "text/plain" } units: { type: "String", readWrite: "R" } deviceCommands: - name: PostInt get: - { operation: "get", deviceResource: "int"} - name: PostFloat get: - { operation: "get", deviceResource: "float"}
下面來一個清爽的
name: "rand-numeric"
manufacturer: "Intel Corp."
model: "Some 3rd party App sending int & floats"
labels:
- "rest"
- "int64"
description: "REST Device that sends in ints and floats"
deviceResources:
- name: int
description: "Int64 value"
properties:
value:
{ type: "Int64", readWrite: "W" , mediaType : "application/json" }
units:
{ type: "String", readWrite: "R"}
deviceCommands:
- name: PostInt
get:
- { operation: "get", deviceResource: "int"}
接下來我們要修改 configuration.toml 文件
打開它找到最后DeviceList的部分,把 name=sample-json 和 name=sample-image的都刪掉
把name=sample-numeric 改成 name=rand-numeric 這是定義了設備名改不改都行
Profile = ""雙引號中要寫成rand-numeric ,注意這個屬性的值和上面rand-numeric-device.yaml文件中的name屬性值必須一樣
修改文件中所有目前的Host = "localhost" 改為運行EdgeX 服務的虛擬機的ip 即 host2
這里要注意上面修改的host不包括[Service]中的Host,[Service]-->Host的值是將要啟動device-rest-go(deviceService)服務的ip地址 host3
如果你是在物理機來運行device-rest-go服務的話, 那也不要寫localhost,因為這個信息是要注冊給docker的
所以說, 這里一定要寫ip而且是一個和運行docker服務主機能互通的ip
修改完把device-rest-go/cmd目錄中的 二進制文件device-rest-go 和res目錄拷貝到192.168.1.10任意目錄, 本機的話直接啟動即可
./device-rest-go
Init: useRegistry: profile: confDir: Loading configuration from: /home/koala/project/EdgeX/device-rest-go/cmd/res/configuration.toml Load configuration from local file and bypassing registration in Registry... Calling service.Start. EnableRemote is false, using local log file level=INFO ts=2020-03-06T09:40:19.996638031Z app=edgex-device-rest source=init.go:138 msg="Check Metadata service's status ..." level=INFO ts=2020-03-06T09:40:19.996638037Z app=edgex-device-rest source=init.go:138 msg="Check Data service's status ..." level=INFO ts=2020-03-06T09:40:20.005574323Z app=edgex-device-rest source=init.go:48 msg="Service clients initialize successful." level=INFO ts=2020-03-06T09:40:20.010404424Z app=edgex-device-rest source=service.go:153 msg="Device Service edgex-device-rest exists" level=INFO ts=2020-03-06T09:40:20.019431937Z app=edgex-device-rest source=service.go:96 msg="*Service Start() called, name=edgex-device-rest, version=to be replaced by makefile" level=INFO ts=2020-03-06T09:40:20.022447427Z app=edgex-device-rest source=service.go:100 msg="Listening on port: 49986" level=INFO ts=2020-03-06T09:40:20.025506267Z app=edgex-device-rest source=resthandler.go:64 msg="Route /api/v1/resource/{deviceName}/{resourceName} added." level=INFO ts=2020-03-06T09:40:20.033438364Z app=edgex-device-rest source=service.go:127 msg="Service started in: 37.360847ms"
啟動后可以看到監聽端口是49986 uri /api/v1/resource/{deviceName}/{resourceName}
所以拼接url是 http://host3:49986/api/v1/resource/{deviceName}/{resourceName}
deviceName 根據configuration.toml文件 [DeviceList][Name]屬性的值
resourceName 對應rand-numeric-device.yaml


http://host3:49986/api/v1/resource/rand-numeric/int
發送post數據 任意整數(忽略我的IP)

在這里沒有消息就是好消息 ,官方的代碼中沒有寫返回的內容
在瀏覽器中打開下面的鏈接(ip是運行EdgeX 的docker服務的主機IP
http://host2:48080/api/v1/event/count
每發送一次, 刷新頁面 ,返回的數字就+1
如果是這樣,那么我們就可以修改我們的設備代碼把里面的url替換成
http://host3:49986/api/v1/resource/rand-numeric/int
func main() {
for {
time.Sleep(time.Second * 3)
i := rand.Intn(128) + 1
fmt.Println(i)
url := "http://host3:49986/api/v1/resource/rand-numeric/int" //請求地址
contentType := "application/json"
//參數,多個用&隔開
data := strings.NewReader(fmt.Sprintf("%d", i))
resp, _ := http.Post(url, contentType, data)
fmt.Println(resp)
}
}
http://host2:48080/api/v1/reading/name/int/3 (http://host2:48080/api/v1/reading/name/:name/:limit)
name是resourceName limit就是看最新的幾條
這個接口比上面count結尾的接口更具有觀賞性
可以隨着設備數據的產生從而看到數據是跟着設備數據變化二變化的

下面我們來分析代碼
跟進到

NewService

s.Start(errChan)

這里初始化了自己的驅動中的Initialize() 傳了參數s.asyncCh
這個時候我們回過頭來看 device-rest-go/driver/restdriver.go中的Initialize方法
func deviceHandler(writer http.ResponseWriter, request *http.Request) { handler, ok := request.Context().Value(handlerContextKey).(RestHandler) if !ok { writer.WriteHeader(http.StatusBadRequest) writer.Write([]byte("Bad context pass to handler")) return } handler.processAsyncRequest(writer, request) resStr := "success" writer.Write([]byte(resStr)) }
processAsyncRequest方法中
asyncValues := &models.AsyncValues{ DeviceName: deviceName, CommandValues: []*models.CommandValue{value}, } handler.logger.Debug(fmt.Sprintf("Incoming reading received: Device=%s Resource=%s", deviceName, resourceName)) handler.asyncValues <- asyncValues
asyncValues發送到管道中也是初始化驅動時的傳參 s.asyncCh
