[TOC]
一、device-service项目目录结构
通过device-sdk-go提取的simple的目录结构
~/project/Go/workspace/src/github.com/edgexfoundry$ tree device-simple/
device-simple/
├── cmd
│ └── device-simple
│ ├── Dockerfile
│ ├── main.go
│ └── res
│ ├── configuration.toml
│ ├── docker
│ │ └── configuration.toml
│ ├── off.jpg
│ ├── on.png
│ └── Simple-Driver.yaml
├── driver
│ └── simpledriver.go
├── go.mod
├── go.sum
├── Makefile
├── VERSION
└── version.go
以温度传感器device-temperature-sensor项目为例,分析项目工程结构和代码
root@J0633788c6:/home/admin/edgex/device-temperature-sensor# tree
.
├── .gitlab-ci.yml # Gitlab自动化ci/cd所需配置文件
├── .gitmodules # git submodules配置文件
├── Dockerfile # 打docker镜像所需文件
├── Makefile # 项目打包命令集合
├── README.md
├── VERSION
├── cmd # 启动命令及相关配置文件
│ └── device-service
│ ├── main.go # 项目启动main函数
│ └── res # 项目所需配置文件
│ ├── TemperatureProfile.yaml # 设备http接口定义
│ ├── configuration.toml # 裸机启动所需配置文件
│ └── docker
│ └── configuration.toml # docker启动配置文件
├── docker-compose.yml
├── go.mod # gomod项目配置文件,通过go mod init生成,手动修改配置内部依赖库
├── go.sum # gomod依赖版本管理,无需手动创建
├── internal
│ ├── driver # 主要的驱动封装逻辑
│ │ ├── driver.go # Driver结构主体,需在此实现相关callback方法
│ │ └── temperaturesensor # 自定义设备相关结构与方法
│ │ └── temperaturesensor.go
│ └── mod # 包含一些内部依赖库,添加submodule即可
│ ├── device-sdk-go # 官方 device-sdk
│ ├── go-mod-core-contracts
│ └── go-mod-registry
├── iotedge.yml
├── sensor.md
└── version.go
1.cmd文件夹
1)main.go
device-service项目程序入口
package main
# 导入包
import (
"github.com/edgexfoundry/device-sdk-go/pkg/jxstartup"
"gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor" # 包含version.go
"gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor/internal/driver" # 包含driver.go
)
const (
serviceName string = "device-temperature-sensor" # 定义字符串变量
)
func main() {
d := driver.Driver{} # driver.go下的Driver结构体
jxstartup.StartService(serviceName, device.Version, &d) # 启动device-service
}
2)configuration.toml
- 用于描述当前dervice-service自身ip、端口以及所调用的edgex 微服务ip和端口信息。
- 系统启动时由main函数执行
- 项目通常有很多配置信息需要存储,比如数据库的账号密码、Redis的连接地址、各种环境参数,这些配置信息通常都是写在配置文件中。常用的配置文件存储格式有JSON文件、INI文件、YAML文件和TOML文件等等。
- TOML被设计成可以无歧义地被映射为哈希表,从而很容易的被解析成各种语言中的数据结构。
[Writable]
LogLevel = 'INFO'
[Service]
Host = "172.17.0.1"
Port = 49990
ConnectRetries = 20
Labels = []
OpenMsg = "device service started"
Timeout = 5000
EnableAsyncReadings = true
AsyncBufferSize = 16
[Registry]
Host = "localhost"
Port = 8500
Type = "consul"
CheckInterval = "10s"
FailLimit = 3
FailWaitTime = 10
[Clients]
[Clients.Data]
Name = "edgex-core-data"
Protocol = "http"
Host = "localhost"
Port = 48080
Timeout = 5000
[Clients.Metadata]
Name = "edgex-core-metadata"
Protocol = "http"
Host = "localhost"
Port = 48081
Timeout = 5000
[Clients.Logging]
Name = "edgex-support-logging"
Protocol = "http"
Host = "localhost"
Port = 48061
[Device]
DataTransform = true
InitCmd = ""
InitCmdArgs = ""
MaxCmdOps = 128
MaxCmdValueLen = 256
RemoveCmd = ""
RemoveCmdArgs = ""
ProfilesDir = "./res"
[Logging]
EnableRemote = false
File = "./device-service.log"
# Pre-define Devices
[[DeviceList]]
# 这里DeviceList的Name和Profile的值对应TemperatureProfile.yaml的name: "temperature-sensor"
Name = "temperature-sensor"
Profile = "temperature-sensor"
Description = "温度传感器"
Labels = []
[DeviceList.Protocols]
[DeviceList.Protocols.other]
Address = "/api/v1/device/temperature-sensor"
3)裸机与docker的configuration.toml 区别
colordiff configuration.toml docker/configuration.toml
5,6c5,6
< Host = "172.17.0.1"
< Port = 49990
---
> Host = "edgex-device-temperature-sensor"
> Port = 80
9c9
< OpenMsg = "device service started"
---
> OpenMsg = "device service temperature-sensor started"
15c15
< Host = "localhost"
---
> Host = "edgex-core-consul"
26c26
< Host = "localhost"
---
> Host = "edgex-core-data"
33c33
< Host = "localhost"
---
> Host = "edgex-core-metadata"
40c40
< Host = "localhost"
---
> Host = "edgex-support-logging"
54c54
< EnableRemote = false
---
> EnableRemote = true
4)TemperatureProfile.yaml
- 设备微服务命令请求描述文件,提供给core-service的command层处理
- 定义北向应用发往南向设备的命令请求。
- 系统启动时由main函数执行
name: "temperature-sensor"
manufacturer: "xxx"
model: "SP-01"
labels:
- "temperature-sensor"
description: "temperature sensor"
deviceResources:
-
name: "temperature"
description: "获取温度"
properties:
value:
{ type: "String", readWrite: "R", defaultValue: "{}" }
deviceCommands:
-
name: "temperature"
get:
- { operation: "get", object: "temperature" }
coreCommands:
-
name: "temperature"
get:
path: "/api/v1/device/{deviceId}/temperature"
responses:
-
code: "200"
description: ""
expectedValues: ["temperature"]
-
code: "503"
description: "service unavailable"
expectedValues: []
注意:ymal文件不能出现tab键
2.internal文件夹
1)driver.go
- edgex的驱动协议接口,设备微服务驱动初始化及对北向命令请求进行处理
- Initialize
- HandleReadCommands
- HandleWriteCommands
- Stop
- AddDevice
- UpdateDevice
- RemoveDevice
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018 Canonical Ltd
// Copyright (C) 2018-2019 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0
// Package driver this package provides a simple example implementation of
// ProtocolDriver interface.
//
package driver
import (
"fmt"
"time"
dsModels "github.com/edgexfoundry/device-sdk-go/pkg/models"
"github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
contract "github.com/edgexfoundry/go-mod-core-contracts/models"
"gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor/internal/driver/temperaturesensor" //包含temperaturesensor包
)
// Driver 是对device的操作
type Driver struct {
lc logger.LoggingClient
asyncCh chan<- *dsModels.AsyncValues
temperatureSensor *temperaturesensor.TemperatureSensor
}
// Initialize performs protocol-specific initialization for the device
// service.
func (s *Driver) Initialize(lc logger.LoggingClient, asyncCh chan<- *dsModels.AsyncValues) error {
s.lc = lc
s.asyncCh = asyncCh
s.temperatureSensor = temperaturesensor.NewTemperatureSensor(lc)
return nil
}
// HandleReadCommands triggers a protocol Read operation for the specified device.
func (s *Driver) HandleReadCommands(deviceName string, protocols map[string]contract.ProtocolProperties, reqs []dsModels.CommandRequest) (res []*dsModels.CommandValue, err error) {
// 查看该handle的信息
s.lc.Debug(fmt.Sprintf("protocols: %v resource: %v attributes: %v", protocols, reqs[0].DeviceResourceName, reqs[0].Attributes))
now := time.Now().UnixNano()
for _, req := range reqs {
switch req.DeviceResourceName {
case "temperature":
{
temperature, err := s.temperatureSensor.GetTemperature()
if err != nil {
return nil, err
}
cv := dsModels.NewStringValue(reqs[0].DeviceResourceName, now, temperature)
res = append(res, cv)
}
}
}
return res, nil
}
// HandleWriteCommands passes a slice of CommandRequest struct each representing
// a ResourceOperation for a specific device resource.
// Since the commands are actuation commands, params provide parameters for the individual
// command.
func (s *Driver) HandleWriteCommands(deviceName string, protocols map[string]contract.ProtocolProperties, reqs []dsModels.CommandRequest,
params []*dsModels.CommandValue) error {
s.lc.Info(fmt.Sprintf("Driver.HandleWriteCommands: protocols: %v, resource: %v, parameters: %v", protocols, reqs[0].DeviceResourceName, params))
for _, param := range params {
switch param.DeviceResourceName {
}
}
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 (s *Driver) Stop(force bool) error {
// Then Logging Client might not be initialized
if s.lc != nil {
s.lc.Debug(fmt.Sprintf("Driver.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 (s *Driver) AddDevice(deviceName string, protocols map[string]contract.ProtocolProperties, adminState contract.AdminState) error {
s.lc.Debug(fmt.Sprintf("a new Device is added: %s", deviceName))
return nil
}
// UpdateDevice is a callback function that is invoked
// when a Device associated with this Device Service is updated
func (s *Driver) UpdateDevice(deviceName string, protocols map[string]contract.ProtocolProperties, adminState contract.AdminState) error {
s.lc.Debug(fmt.Sprintf("Device %s is updated", deviceName))
return nil
}
// RemoveDevice is a callback function that is invoked
// when a Device associated with this Device Service is removed
func (s *Driver) RemoveDevice(deviceName string, protocols map[string]contract.ProtocolProperties) error {
s.lc.Debug(fmt.Sprintf("Device %s is removed", deviceName))
return nil
}
2)temperaturesensor.go
- Go Library,提供传感器控制或数据提取相关的一些子方法
package temperaturesensor
import (
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
)
// Temperature Sensor 定义
type TemperatureSensor struct {
lc logger.LoggingClient // 用来发送日志
enabled bool // 是否使能
temperature float64
}
// NewTemperatureSensor 创建一个新的TemperatureSensor
func NewTemperatureSensor(lc logger.LoggingClient) *TemperatureSensor {
return &TemperatureSensor{
lc: lc,
enabled: false,
}
}
// Enable 用来让传感器工作
func (t *TemperatureSensor) Enable() {
if t.enabled {
t.lc.Warn("temperature sensor already enabled")
return
}
t.enabled = true
t.lc.Info("temperature sensor enabled")
}
// Disable 用来让传感器工作
func (t *TemperatureSensor) Disable() {
if !t.enabled {
t.lc.Warn("temperature sensor already disabled")
return
}
t.enabled = false
t.lc.Info("temperature sensor disabled")
}
// GetTemperature 获取当前传感器的温度数据
func (t *TemperatureSensor) GetTemperature() (string, error) {
//out, err := runCmd()
//if err != nil {
// return "", err
//} else {
// return strings.TrimSuffix(out, "\n"), nil
//}
files, err := ioutil.ReadDir("/sys/bus/w1/devices")
if err != nil {
return "", err
}
var sensorId string
for _, file := range files {
if strings.HasPrefix(file.Name(), "28") {
sensorId = file.Name()
}
}
path := fmt.Sprintf("/sys/bus/w1/devices/%s/w1_slave", sensorId)
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
b, err := ioutil.ReadAll(file)
outs := strings.Split(string(b), " ")
res := strings.Split(outs[len(outs)-1], "=")
return strings.TrimSuffix(res[len(res)-1], "\n"), nil
}
3.go.mod
module gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor //指定go项目包的名字
go 1.13
//映射路径,在import 包时,将后面的路径替换前面的路径
replace github.com/edgexfoundry/device-sdk-go => ./internal/mod/device-sdk-go
replace github.com/edgexfoundry/go-mod-core-contracts => ./internal/mod/go-mod-core-contracts
replace github.com/edgexfoundry/go-mod-registry => ./internal/mod/go-mod-registry
//指定仓库版本
require (
github.com/edgexfoundry/device-sdk-go v1.0.0
github.com/edgexfoundry/go-mod-core-contracts v0.1.30
github.com/go-stack/stack v1.8.0 // indirect //go get命令从github拉取仓库到$GOPATH/src/github.com/go-stack/stack
github.com/pkg/errors v0.8.1
gopkg.in/evanphx/json-patch.v4 v4.5.0
)
replace github.com/satori/go.uuid v1.2.0 => github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
4. .gitmodules
submodules配置文件,git的submodules功能允许将一个Git存储库保留为另一个Git存储库的子目录。方便在一个项目中调用另一个项目。
执行:git submodule update --init --remote 会自动把依赖仓库拉取到对应位置。
[submodule "internal/mod/device-sdk-go"]
path = internal/mod/device-sdk-go
url = http://gitlab.xxx.com/applications/edgex/device-sdk-go.git
branch = jx_edinburgh
[submodule "internal/mod/go-mod-core-contracts"]
path = internal/mod/go-mod-core-contracts
url = http://gitlab.xxx.com/applications/edgex/go-mod-core-contracts.git
[submodule "internal/mod/go-mod-registry"]
path = internal/mod/go-mod-registry
url = http://gitlab.xxx.com/applications/edgex/go-mod-registry.git
5.Makefile
BUILDDIR=build/
CHANGELOG=$(BUILDDIR)/CHANGELOG.md
# Go的代理服务器,加速go包的拉取
GOPROXY=https://mirrors.aliyun.com/goproxy/
# 拉取submodule配置文件下的仓库到本项目
UPDATE_SUBMODULE:=$(shell git submodule update --init --remote)
# 存在交叉编译的情况时,cgo 工具是不可用的。
GO=CGO_ENABLED=0 GO111MODULE=on go #CGO_ENABLED=0 声明cgo 工具不可用
# 定义在各平台上的镜像名称
DOCKER_IMAGE_NAME=registry.xxx.com:5000/edgex/device-service/temperature-sensor
HARBOR_IMAGE_NAME=harbor.xxx.iotedge/library/edgex-temperature-sensor/$(ARCHTAG)/others
HARBOR_IOTEDGE_IMAGE_NAME=harbor.xxx.com/library/edgex-temperature-sensor/$(ARCHTAG)/others
GOARCH=arm64
ARCHTAG=arm64v8
# 源程序路径
SRC_PATH=gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor
# 使用最新的git tag作为镜像版本号
VERSION=$(shell git tag -l "v*" --points-at HEAD | tail -n 1 | tail -c +2)
GIT_SHA=$(shell git rev-parse HEAD)
GOFLAGS=-ldflags "-X $(SRC_PATH).Version=$(VERSION)"
MICROSERVICES=device-service
.PHONY: $(MICROSERVICES) # 避免参数与文件重名
check_version:
ifeq ($(VERSION),)
$(error No version tag found)
endif
ifeq ($(shell git cat-file -t v$(VERSION)),commit)
$(error Changelog should be in tag message)
endif
# 执行build会编译check_version、$(MICROSERVICES)、changelog三个步骤
build: check_version $(MICROSERVICES) changelog
build-amd64: GOARCH=amd64
build-amd64: build
build-arm64: GOARCH=arm64
build-arm64: build
device-service: | $(BUILDDIR)
GOARCH=$(GOARCH) $(GO) build $(GOFLAGS) -o $(BUILDDIR)/bin/$@-$(GOARCH) ./cmd/device-service
cp -r cmd/device-service/res $(BUILDDIR)/bin/
# 记录修改版本,如果check_version或 $(BUILDDIR) 步骤执行成功,则执行以下操作
changelog: check_version | $(BUILDDIR)
echo "# Changelog\n" > $(CHANGELOG)
git tag --sort=-taggerdate --format='## %(tag) - %(taggerdate:short)%0a### Author: %(taggername) %(taggeremail)%0a%(contents)%0a' >> $(CHANGELOG)
$(BUILDDIR):
mkdir -p $(BUILDDIR)
docker-amd64: GOARCH=amd64
docker-amd64: ARCHTAG=x8664
docker-amd64: docker
docker-arm64: GOARCH=arm64
docker-arm64: ARCHTAG=arm64v8
docker-arm64: docker
docker: build
docker build \
--label "git_sha=$(GIT_SHA)" \
--build-arg GOARCH=$(GOARCH) \
-f Dockerfile \
-t $(DOCKER_IMAGE_NAME):$(GIT_SHA) \
-t $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) \
-t $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-latest \
build
deploy: check_version
docker push $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION)
docker push $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-latest
deploy-arm64: ARCHTAG=arm64v8
deploy-arm64: deploy
deploy-harbor:
docker login harbor.xxx.iotedge --username $(DOCKER_USERNAME) --password $(DOCKER_PASSWORD)
docker tag $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) $(HARBOR_IMAGE_NAME):$(VERSION)
docker push $(HARBOR_IMAGE_NAME):$(VERSION)
deploy-harbor-arm64: ARCHTAG=arm64v8
deploy-harbor-arm64: deploy-harbor
deploy-harbor-iotedge:
docker login harbor.xxx.com --username $(IOTEDGE_DOCKER_USERNAME) --password $(IOTEDGE_DOCKER_PASSWORD)
docker tag $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) $(HARBOR_IOTEDGE_IMAGE_NAME):$(VERSION)
docker push $(HARBOR_IOTEDGE_IMAGE_NAME):$(VERSION)
deploy-harbor-arm64-iotedge: ARCHTAG=arm64v8
deploy-harbor-arm64-iotedge: deploy-harbor-iotedge
test:
$(GO) vet ./...
gofmt -l .
$(GO) test -coverprofile=coverage.out ./...
clean:
rm -rf build/
使用方法
# 编译main,go 测试
cd cmd/device-service
GOARCH=arm64 go build main.go
# 编译go工程
make build-arm64
# 编译go工程并制作docker镜像
make docker-arm64
# 部署docker镜像到registry
make deploy-arm64
# 部署docker镜像到harbor
make deploy-harbor-arm64
# 部署docker镜像到iotedge
make deploy-harbor-arm64-iotedge
6.docker-compose.yml
- 设备微服务的docker-compose描述文件,是docker服务的清单,指定了所需的容器以及如何运行这些容器。
version: '3'
services:
edgex-device-termperature-sensor:
image: registry.xxx.com:5000/edgex/device-service/temperature-sensor:arm64v8-cpu-0.0.4 # Makefile编译并提交registry到的docker image
container_name: edgex-device-temperature-sensor
hostname: edgex-device-temperature-sensor
restart: always
privileged: true
volumes:
- "/sys/bus/w1/devices:/sys/bus/w1/devices" #把裸机的设备映射到docker中
networks:
default:
external:
name: edgex
7.iotedge.yml
- 在IOTEdge上将device-service镜像注册成应用时所需文件
worker_node:
service_name: app-jx-temperature-sensor
portname: port9995
arch: ["arm64v8"]
engine: "docker-compose"
yaml: # 对docker-compose.yml的拷贝
version: '3'
services:
edgex-device-temperature-sensor:
image: edgex-temperature-sensor
container_name: edgex-device-temperature-sensor
hostname: edgex-device-temperature-sensor
restart: always
privileged: true
volumes:
- "/sys/bus/w1/devices:/sys/bus/w1/devices"
networks:
default:
external:
name: edgex
8.Dockerfile
- 用于构建镜像,文本内容包含了一条条构建镜像所需的指令和说明。
- Dockerfile文件详解
#
# Copyright (c) 2018, 2019 Intel
#
# SPDX-License-Identifier: Apache-2.0
#
# FROM golang:1.11-alpine AS builder
# ARG BUILD_PATH
# ARG GOARCH
# RUN sed -e 's/dl-cdn[.]alpinelinux.org/nl.alpinelinux.org/g' -i~ /etc/apk/repositories
# # add git for go modules
# RUN apk update && apk add make git
# WORKDIR $BUILD_PATH
# COPY . .
# RUN GOPROXY="https://mirrors.aliyun.com/goproxy/" go get -d -v ./...
# RUN make build-$GOARCH
# Next image - Copy built Go binary into new workspace
FROM scratch
# ARG BUILD_PATH
ARG GOARCH
ENV APP_PORT=80
#expose command data port
EXPOSE $APP_PORT
WORKDIR /app
# COPY --from=builder $BUILD_PATH/bin/device-service-$GOARCH /app/device-service
# COPY --from=builder $BUILD_PATH/cmd/device-service/res /app/res
# COPY --from=builder $BUILD_PATH/cmd/device-service/res/docker/configuration.toml /app/res/configuration.toml
COPY bin/device-service-$GOARCH /app/device-service
COPY bin/res /app/res
COPY bin/res/docker/configuration.toml /app/res/configuration.toml
COPY CHANGELOG.md /app
ENTRYPOINT [ "/app/device-service"]
9. .gitlab-ci.yml
- gitlab ci自动化部署所需描述文件
image: golang-dind:latest
variables:
GO111MODULE: "on"
GOPROXY: "https://mirrors.aliyun.com/goproxy/"
# 指明分以下4个阶段
stages:
- build
- deploy
- deploy-harbor-review
- deploy-harbor-iotedge
build: # 1.编译
tags:
- docker-builder
cache:
paths:
- .cache
stage: build
before_script: # 编译前的处理
- sed -i 's/http:\/\/gitlab\.xxx\.com/..\/..\/..\/../' .gitmodules
- git submodule sync --recursive
- git submodule update --init --recursive
# cache needs to be inside $CI_PROJECT_DIR
- mkdir -p .cache
- export GOPATH="$CI_PROJECT_DIR/.cache"
script: # 执行脚本
- make docker-arm64 # 编译go工程并制作docker-arm64镜像
artifacts:
paths:
- build/*
expire_in: 1 day
only:
- /^v[0-9](?:\.[0-9]){2,3}/
deploy: # 2.部署镜像到registry
tags:
- docker-builder
stage: deploy
environment:
name: registry
url: http://registry.xxx.com:8080
script: # 执行脚本
- make deploy-arm64 # 部署docker镜像
only:
- /^v[0-9](?:\.[0-9]){2,3}/
deploy-harbor-review: # 3.部署镜像到harbor
tags:
- docker-builder
stage: deploy-harbor-review
environment:
name: harbor
url: http://harbor.xxx.iotedge
script: # 执行脚本
- make deploy-harbor-arm64
only:
- /^v[0-9](?:\.[0-9]){2,3}/
deploy-harbor-iotedge: # 4.部署镜像到harbor-iotedge
tags:
- docker-builder
stage: deploy-harbor-iotedge
environment:
name: harbor
url: http://harbor.xxx.com
script: # 执行脚本
- make deploy-harbor-arm64-iotedge
when: manual # 指定手动执行脚本,非指定手动的就会自动执行
only:
- /^v[0-9](?:\.[0-9]){2,3}/