在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