Please enable Javascript to view the contents

EdgeX Foundry 心得 之二 -- 设备服务(数据获取和推送到EdgeX)

 ·  ☕ 7 分钟

在EdgeX Foundry中有南向和北向的概念

在数据获取的过程中我自己大略画了一个流程图

在数据获取的过程中 南向就是数据的产生方,北向就是最终数据的利用方

在这个系列的文章中,我没有用官方的虚拟设备服务(device-virtual),因为我个人觉得那样的描述不能很好的体会EdgeX中的各种概念,

这个图可能没开发过的也是看不懂的,接下来一点一点的把上图的内容尽可能的说清楚。

从南向北说

最南端是设备,比如温度传感器,门禁控制器,智能电表,摄像头等等的可以产生数据的装置甚至是软件

我们先来三台机器(用三台的原因是这样更容易把概念区分开)

192.168.1.11(简称host1) 设备

192.168.1.12(简称host2) 运行EdgeX的主机

192.168.1.13 (简称host3)运行deviceService的主机

本文中讲用简单的PHP程序当做数据的产生器也就是设备, 原因很简单, Ubuntu安装Apache + PHP 的环境非常简单

我也尽可能的同步的用go语言来实现相同的功能

先实现最简单的功能, 没隔10秒产生一个[1-127] 的随机数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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同一个目录, 并手动创建

这不是必要的内容, 只是用来验证上面产生数字并能争取发送出来而已

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
}

到现在一个产生随机数的设备就已经有了, 你可以当它是温度或者湿度,或者其他的产生数字的设备就可以了

接下来是设备服务

设备服务主要参考

git clone https://github.com/edgexfoundry/device-rest-go
cd device-rest-go
make build
注:在windows环境中, make命令好像挺麻烦的,用Windows系统的同学自己查一下把, 我也不太会, Linux比较简单

现在我们可以体验一下数据上传到EdgeX了, (注意到目前还没有往北向发送, 只是数据能正常的发送到EdgeX)
启动虚拟机运行起来EdgeX服务
例如 在host2 执行命令 sudo docker-compose up -d 请参考第一节的内容
并停掉其中两个服务(如果启动了的话)sudo docker-compose stop device-virtual device-random 停掉的原因是为了不要让这两个服务产生的数据影响之后的判断

启动以后我们来修改device-rest-go(这个项目是没有fuji发行的,直接用master就好了)
cd device-rest-go/cmd/res
里面有一些文件|目录

-- docker
-- configuration.toml
--sample-image-device.yaml
--sample-json-device.yaml
--sample-numeric-device.yaml

我们的设备是产生随机数所以和我们最相似的是sample-numeric-device.yaml
cp sample-numeric-device.yaml rand-numeric-device.yaml
我们删掉一些内容在下面的代码中用删除线(其实也就是删除float的部分), 修改的内容把原来的内容在#号后的注释中写名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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"}

下面来一个清爽的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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结尾的接口更具有观赏性

可以随着设备数据的产生从而看到数据是跟着设备数据变化二变化的

下面我们来分析代码

打开这个项目, 最好用一个编辑器

先打开device-rest-go/cmd/main.go的文件

 跟进到

Bootstrap

NewService

 s.Start(errChan)

这里初始化了自己的驱动中的Initialize() 传了参数 s.asyncCh

这个时候我们回过头来看 device-rest-go/driver/restdriver.go中的Initialize方法

handler := NewRestHandler(sdk.RunningService(), logger, asyncValues)

return handler.Start()

——»

handler.service.AddRoute(apiResourceRoute, handler.addContext(deviceHandler), http.MethodPost)

——»

deviceHandler是一个方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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方法中

1
2
3
4
5
6
7
8
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

分享

Koala An
作者
Koala An
一个大自然的搬运工