DeviceService简介
DeviceService
在Edgex Foundry中用于连接设备,他们直接与设备打交道,可以看作是设备的驱动吧。DeviceService
作用的作用有:获取设备的状态;接收处理设备发过来的数据并发送到EdgeX;变更设备配置;设备发现。下面是基于Golang
版本的Device Service SDK
做一个主体流程的梳理说明。
核心流程节点
流程图
核心节点说明
首先说明一下,EdgeX Foundry中message bus下面涉及的相关的topic中使用的BaseTopic
默认为edgex
,是可以自定义的。
message bus其实很简单,消息体都是MessageEnvelope
,大家看一下结构:
// MessageEnvelope is the data structure for messages. It wraps the generic message payload with attributes.
type MessageEnvelope struct {
// ApiVersion (from Versionable) shows the API version for the message envelope.
commonDTO.Versionable
// ReceivedTopic is the topic that the message was received on.
ReceivedTopic string `json:"receivedTopic"`
// CorrelationID is an object id to identify the envelope.
CorrelationID string `json:"correlationID"`
// RequestID is an object id to identify the request.
RequestID string `json:"requestID"`
// ErrorCode provides the indication of error. '0' indicates no error, '1' indicates error.
// Additional codes may be added in the future. If non-0, the payload will contain the error.
ErrorCode int `json:"errorCode"`
// Payload is byte representation of the data being transferred.
Payload []byte `json:"payload"`
// ContentType is the marshaled type of payload, i.e. application/json, application/xml, application/cbor, etc
ContentType string `json:"contentType"`
// QueryParams is optionally provided key/value pairs.
QueryParams map[string]string `json:"queryParams,omitempty"`
}
读取配置,默认读取configuration.yaml
// loadConfigYamlFromFile attempts to read the specified configuration yaml file
func (cp *Processor) loadConfigYamlFromFile(yamlFile string) (map[string]any, error) {
cp.lc.Infof("Loading configuration file from %s", yamlFile)
contents, err := os.ReadFile(yamlFile)
if err != nil {
return nil, fmt.Errorf("failed to read configuration file %s: %s", yamlFile, err.Error())
}
data := make(map[string]any)
err = yaml.Unmarshal(contents, &data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall configuration file %s: %s", yamlFile, err.Error())
}
return data, nil
}
这段代码是sdk中会从对应的yaml文件中读取配置为一个map结构。
如果需要,注册到注册中心
EdgeX Foundry使用的默认注册中心为Consul
。
初始化messageBus
messageBus默认提供四种实现方式:mqtt、redis、nats-core、nats-jetstream,默认使用的是redis,可以看一下对应的创建客户端代码片段:
// NewMessageClient is a factory function to instantiate different message client depending on
// the "Type" from the configuration
func NewMessageClient(msgConfig types.MessageBusConfig) (MessageClient, error) {
if msgConfig.Broker.IsHostInfoEmpty() {
return nil, fmt.Errorf("unable to create messageClient: Broker info not set")
}
switch lowerMsgType := strings.ToLower(msgConfig.Type); lowerMsgType {
case MQTT:
return mqtt.NewMQTTClient(msgConfig)
case Redis:
return redis.NewClient(msgConfig)
case NatsCore:
return nats.NewClient(msgConfig)
case NatsJetStream:
return jetstream.NewClient(msgConfig)
default:
return nil, fmt.Errorf("unknown message type '%s' requested", msgConfig.Type)
}
}
订阅command核心模块的请求
DeviceService与command核心服务通信,大部分情况下是使用Message BUS进行交互的,可以看一下对应的TOPIC:
1)DeviceService订阅Command模块请求的TOPIC:{BaseTopic}/device/command/request/{deviceServiceName}/#
,其中#
的值为get
或set
。
2)DeviceService响应Command模块请求的TOPIC:{BaseTopic}/response/{deviceServiceName}/{requestId}
在处理逻辑中会根据TOPIC中最后一级标记的方法(get
或set
)调用自定义驱动Driver
的HandleReadCommands
或HandleWriteCommands
方法。
放一段对应的处理逻辑代码片段:
// expected command response topic scheme: #/<service-name>/<device-name>/<command-name>/<method>
deviceName := topicLevels[length-3]
commandName, err := url.PathUnescape(topicLevels[length-2])
if err != nil {
lc.Errorf("Failed to unescape command name '%s'", commandName)
continue
}
method := topicLevels[length-1]
responsePublishTopic := common.BuildTopic(responsePublishTopicPrefix, msgEnvelope.RequestID)
switch strings.ToUpper(method) {
case "GET":
getCommand(ctx, msgEnvelope, responsePublishTopic, deviceName, commandName, dic)
case "SET":
setCommand(ctx, msgEnvelope, responsePublishTopic, deviceName, commandName, dic)
default:
lc.Errorf("unknown command method '%s', only 'get' or 'set' is allowed", method)
continue
}
订阅metadata核心模块事件
1)DeviceService订阅Metadata模块请求的TOPIC:{BaseTopic}/system-events/core-metadata/+/+/{deviceServiceName}/#
,如果使用了-i flag
指定了不同的InstanceName,还会订阅:{BaseTopic}/system-events/core-metadata/provisionwatcher/+/{serviceBaseName}/#
。如果不使用provisionwatcher
这个特性的话,也是可以不订阅的,下面是关于针对这个topic生成的代码:
// Must subscribe to provision watcher System Events separately when the service has an instance name. i.e. -i flag was used.
// This is because Provision Watchers apply to all instances of the device service, i.e. the service base name (without the instance portion).
// The above subscribe topic is for the specific instance name which is appropriate for Devices, Profiles and Devices service System Events
// and when the -i flag is not used.
if serviceBaseName != deviceService.Name {
// Must replace the first wildcard with the type for Provision Watchers
baseSubscribeTopic := strings.Replace(common.MetadataSystemEventSubscribeTopic, "+", common.ProvisionWatcherSystemEventType, 1)
provisionWatcherSystemEventSubscribeTopic := common.BuildTopic(messageBusInfo.GetBaseTopicPrefix(),
baseSubscribeTopic, serviceBaseName, "#")
topics = append(topics, types.TopicChannel{
Topic: provisionWatcherSystemEventSubscribeTopic,
Messages: messages,
})
lc.Infof("Subscribing additionally to Provision Watcher System Events on topic: %s", provisionWatcherSystemEventSubscribeTopic)
}
我们再来看一下SystemEvent的定义,EdgeX Foundry中关于事件基本上都是使用这个结构体的:
// SystemEvent defines the data for a system event
type SystemEvent struct {
common.Versionable `json:",inline"`
Type string `json:"type"`
Action string `json:"action"`
Source string `json:"source"`
Owner string `json:"owner"`
Tags map[string]string `json:"tags"`
Details any `json:"details"`
Timestamp int64 `json:"timestamp"`
}
根据SystemEvent
中的Type
、Action
确定用途:
1)设备相关(Device):新增设备(会调用Driver
的AddDevice
方法)、更新设备(会调用Driver
的UpdateDevice
方法)、删除设备(会调用Driver
的RemoveDevice
方法)
2)设备模型相关(Device Profile):新增、更新、删除
3)驱动相关(Device Service):新增、更新、删除
4)provisionwatcher:新增、更新、删除
订阅设备验证事件
1)DeviceService订阅设备验证的请求TOPIC:{BaseTopic}/{deviceServiceName}/validate/device
2)DeviceService响应设备验证的请求的TOPIC:{BaseTopic}/response/{deviceServiceName}/{requestId}
。
实际调用Driver
中的ValidateDevice
方法。
初始化各个核心服务客户端实例
主要是各个核心服务的客户端,方便后继的调用,比如:CommandClient、DeviceClient、DeviceServiceClient、DeviceProfileClient等等。
// BootstrapHandler fulfills the BootstrapHandler contract.
// It creates instances of each of the EdgeX clients that are in the service's configuration and place them in the DIC.
// If the registry is enabled it will be used to get the URL for client otherwise it will use configuration for the url.
// This handler will fail if an unknown client is specified.
func (cb *ClientsBootstrap) BootstrapHandler(
_ context.Context,
_ *sync.WaitGroup,
startupTimer startup.Timer,
dic *di.Container) bool {
lc := container.LoggingClientFrom(dic.Get)
config := container.ConfigurationFrom(dic.Get)
cb.registry = container.RegistryFrom(dic.Get)
jwtSecretProvider := secret.NewJWTSecretProvider(container.SecretProviderExtFrom(dic.Get))
if config.GetBootstrap().Clients != nil {
for serviceKey, serviceInfo := range *config.GetBootstrap().Clients {
var url string
var err error
if !serviceInfo.UseMessageBus {
url, err = cb.getClientUrl(serviceKey, serviceInfo.Url(), startupTimer, lc)
if err != nil {
lc.Error(err.Error())
return false
}
}
switch serviceKey {
case common.CoreDataServiceKey:
dic.Update(di.ServiceConstructorMap{
container.EventClientName: func(get di.Get) interface{} {
return clients.NewEventClient(url, jwtSecretProvider)
},
})
case common.CoreMetaDataServiceKey:
dic.Update(di.ServiceConstructorMap{
container.DeviceClientName: func(get di.Get) interface{} {
return clients.NewDeviceClient(url, jwtSecretProvider)
},
container.DeviceServiceClientName: func(get di.Get) interface{} {
return clients.NewDeviceServiceClient(url, jwtSecretProvider)
},
container.DeviceProfileClientName: func(get di.Get) interface{} {
return clients.NewDeviceProfileClient(url, jwtSecretProvider)
},
container.ProvisionWatcherClientName: func(get di.Get) interface{} {
return clients.NewProvisionWatcherClient(url, jwtSecretProvider)
},
})
case common.CoreCommandServiceKey:
var client interfaces.CommandClient
if serviceInfo.UseMessageBus {
// TODO: Move following outside loop when multiple messaging based clients exist
messageClient := container.MessagingClientFrom(dic.Get)
if messageClient == nil {
lc.Errorf("Unable to create Command client using MessageBus: %s", "MessageBus Client was not created")
return false
}
// TODO: Move following outside loop when multiple messaging based clients exist
timeout, err := time.ParseDuration(config.GetBootstrap().Service.RequestTimeout)
if err != nil {
lc.Errorf("Unable to parse Service.RequestTimeout as a time duration: %v", err)
return false
}
baseTopic := config.GetBootstrap().MessageBus.GetBaseTopicPrefix()
client = clientsMessaging.NewCommandClient(messageClient, baseTopic, timeout)
lc.Infof("Using messaging for '%s' clients", serviceKey)
} else {
client = clients.NewCommandClient(url, jwtSecretProvider)
}
dic.Update(di.ServiceConstructorMap{
container.CommandClientName: func(get di.Get) interface{} {
return client
},
})
case common.SupportNotificationsServiceKey:
dic.Update(di.ServiceConstructorMap{
container.NotificationClientName: func(get di.Get) interface{} {
return clients.NewNotificationClient(url, jwtSecretProvider)
},
container.SubscriptionClientName: func(get di.Get) interface{} {
return clients.NewSubscriptionClient(url, jwtSecretProvider)
},
})
case common.SupportSchedulerServiceKey:
dic.Update(di.ServiceConstructorMap{
container.IntervalClientName: func(get di.Get) interface{} {
return clients.NewIntervalClient(url, jwtSecretProvider)
},
container.IntervalActionClientName: func(get di.Get) interface{} {
return clients.NewIntervalActionClient(url, jwtSecretProvider)
},
})
default:
}
}
}
return true
}
创建AutoEventManager
这个其实就是驱动相关设备属性可以自动上报的机制,比如你在配置设备的时候配置了属性autoEvents
,配置的属性就会在这里进行执行。
deviceList:
- name: virtualDevice001
profileName: 1698998764550291457
description: "虚拟设备Justin001"
tags:
productId: "1691704696665341954"
protocols:
other:
address: 127.0.0.1
port: 0000
autoEvents:
- interval: 10s
onChange: false
sourceName: WorkTemperature
- interval: 10s
onChange: false
sourceName: EnvTemperature
初始化Rest API
其实就是初始化相关的HTTP接口服务,目前在EdgeX Foundry中的通信,大部分都转到了message Bus中了,不过在极个别的情况下,还是会使用HTTP的接口服务,比如在与Command模块会通过HTTP协议进行交互,对应的请求如下:
1)setCommand PUT /api/v3/device/name/{name}/{command}
2)getCommand GET /api/v3/device/name/{name}/{command}
func (c *RestController) InitRestRoutes() {
c.lc.Info("Registering v2 routes...")
// router.UseEncodedPath() tells the router to match the encoded original path to the routes
c.router.UseEncodedPath()
lc := container.LoggingClientFrom(c.dic.Get)
secretProvider := container.SecretProviderExtFrom(c.dic.Get)
authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc)
// common
c.addReservedRoute(common.ApiPingRoute, c.Ping).Methods(http.MethodGet)
c.addReservedRoute(common.ApiVersionRoute, authenticationHook(c.Version)).Methods(http.MethodGet)
c.addReservedRoute(common.ApiConfigRoute, authenticationHook(c.Config)).Methods(http.MethodGet)
// secret
c.addReservedRoute(common.ApiSecretRoute, authenticationHook(c.Secret)).Methods(http.MethodPost)
// discovery
c.addReservedRoute(common.ApiDiscoveryRoute, authenticationHook(c.Discovery)).Methods(http.MethodPost)
// device command
c.addReservedRoute(common.ApiDeviceNameCommandNameRoute, authenticationHook(c.GetCommand)).Methods(http.MethodGet)
c.addReservedRoute(common.ApiDeviceNameCommandNameRoute, authenticationHook(c.SetCommand)).Methods(http.MethodPut)
c.router.Use(correlation.ManageHeader)
c.router.Use(correlation.LoggingMiddleware(c.lc))
c.router.Use(correlation.UrlDecodeMiddleware(c.lc))
}
驱动初始化
调用驱动服务方法Initialize
进行驱动的初始化操作。
type ProtocolDriver interface {
// Initialize performs protocol-specific initialization for the device service.
// The given *AsyncValues channel can be used to push asynchronous events and
// readings to Core Data. The given []DiscoveredDevice channel is used to send
// discovered devices that will be filtered and added to Core Metadata asynchronously.
Initialize(sdk DeviceServiceSDK) error
......
}
注册到edgex foundry的Metadata模块
设备服务自注册到EdgeX Foundry中。
edgexErr = s.selfRegister()
if edgexErr != nil {
s.lc.Errorf("Failed to register %s on Metadata: %s", s.serviceKey, edgexErr.Error())
return false
}
加载本地device profile配置文件
加载本地配置的device profile文件:
package provision
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
)
const (
jsonExt = ".json"
yamlExt = ".yaml"
ymlExt = ".yml"
)
func LoadProfiles(path string, dic *di.Container) errors.EdgeX {
if path == "" {
return nil
}
absPath, err := filepath.Abs(path)
if err != nil {
return errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path", err)
}
files, err := os.ReadDir(absPath)
if err != nil {
return errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory", err)
}
if len(files) == 0 {
return nil
}
lc := bootstrapContainer.LoggingClientFrom(dic.Get)
lc.Infof("Loading pre-defined profiles from %s(%d files found)", absPath, len(files))
var addProfilesReq []requests.DeviceProfileRequest
dpc := bootstrapContainer.DeviceProfileClientFrom(dic.Get)
for _, file := range files {
var profile dtos.DeviceProfile
fullPath := filepath.Join(absPath, file.Name())
if strings.HasSuffix(fullPath, yamlExt) || strings.HasSuffix(fullPath, ymlExt) {
content, err := os.ReadFile(fullPath)
if err != nil {
lc.Errorf("Failed to read %s: %v", fullPath, err)
continue
}
err = yaml.Unmarshal(content, &profile)
if err != nil {
lc.Errorf("Failed to YAML decode profile %s: %v", file.Name(), err)
continue
}
} else if strings.HasSuffix(fullPath, jsonExt) {
content, err := os.ReadFile(fullPath)
if err != nil {
lc.Errorf("Failed to read %s: %v", fullPath, err)
continue
}
err = json.Unmarshal(content, &profile)
if err != nil {
lc.Errorf("Failed to JSON decode profile %s: %v", file.Name(), err)
continue
}
} else {
continue
}
res, err := dpc.DeviceProfileByName(context.Background(), profile.Name)
if err == nil {
lc.Infof("Profile %s exists, using the existing one", profile.Name)
_, exist := cache.Profiles().ForName(profile.Name)
if !exist {
err = cache.Profiles().Add(dtos.ToDeviceProfileModel(res.Profile))
if err != nil {
return errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("failed to cache the profile %s", res.Profile.Name), err)
}
}
} else {
lc.Infof("Profile %s not found in Metadata, adding it ...", profile.Name)
req := requests.NewDeviceProfileRequest(profile)
addProfilesReq = append(addProfilesReq, req)
}
}
if len(addProfilesReq) == 0 {
return nil
}
ctx := context.WithValue(context.Background(), common.CorrelationHeader, uuid.NewString()) // nolint:staticcheck
_, edgexErr := dpc.Add(ctx, addProfilesReq)
return edgexErr
}
加载本地设备配置文件
加载本地配置的设备文件:
package provision
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
"github.com/edgexfoundry/go-mod-core-contracts/v3/models"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
"gopkg.in/yaml.v3"
"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
)
func LoadDevices(path string, dic *di.Container) errors.EdgeX {
if path == "" {
return nil
}
absPath, err := filepath.Abs(path)
if err != nil {
return errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path", err)
}
files, err := os.ReadDir(absPath)
if err != nil {
return errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory", err)
}
if len(files) == 0 {
return nil
}
lc := bootstrapContainer.LoggingClientFrom(dic.Get)
lc.Infof("Loading pre-defined devices from %s(%d files found)", absPath, len(files))
var addDevicesReq []requests.AddDeviceRequest
serviceName := container.DeviceServiceFrom(dic.Get).Name
for _, file := range files {
var devices []dtos.Device
fullPath := filepath.Join(absPath, file.Name())
if strings.HasSuffix(fullPath, yamlExt) || strings.HasSuffix(fullPath, ymlExt) {
content, err := os.ReadFile(fullPath)
if err != nil {
lc.Errorf("Failed to read %s: %v", fullPath, err)
continue
}
d := struct {
DeviceList []dtos.Device `yaml:"deviceList"`
}{}
err = yaml.Unmarshal(content, &d)
if err != nil {
lc.Errorf("Failed to YAML decode %s: %v", fullPath, err)
continue
}
devices = d.DeviceList
} else if strings.HasSuffix(fullPath, ".json") {
content, err := os.ReadFile(fullPath)
if err != nil {
lc.Errorf("Failed to read %s: %v", fullPath, err)
continue
}
err = json.Unmarshal(content, &devices)
if err != nil {
lc.Errorf("Failed to JSON decode %s: %v", fullPath, err)
continue
}
} else {
continue
}
for _, device := range devices {
if _, ok := cache.Devices().ForName(device.Name); ok {
lc.Infof("Device %s exists, using the existing one", device.Name)
} else {
lc.Infof("Device %s not found in Metadata, adding it ...", device.Name)
device.ServiceName = serviceName
device.AdminState = models.Unlocked
device.OperatingState = models.Up
req := requests.NewAddDeviceRequest(device)
addDevicesReq = append(addDevicesReq, req)
}
}
}
if len(addDevicesReq) == 0 {
return nil
}
dc := bootstrapContainer.DeviceClientFrom(dic.Get)
ctx := context.WithValue(context.Background(), common.CorrelationHeader, uuid.NewString()) //nolint: staticcheck
responses, edgexErr := dc.Add(ctx, addDevicesReq)
if edgexErr != nil {
return edgexErr
}
err = nil
for _, response := range responses {
if response.StatusCode != http.StatusCreated {
if response.StatusCode == http.StatusConflict {
lc.Warnf("%s. Device may be owned by other device service instance.", response.Message)
continue
}
err = multierror.Append(err, fmt.Errorf("add device failed: %s", response.Message))
}
}
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}
return nil
}
启动自动上报事件的定时器
对应的是AutoEventManager
s.autoEventManager.StartAutoEvents()
驱动启动
err := s.driver.Start()
if err != nil {
cancel()
return fmt.Errorf("failed to Start ProtocolDriver: %v", err)
}
驱动定义
上面提到了很多次的驱动,我们看一看驱动定义文件ProtocolDriver:
package interfaces
import (
"github.com/edgexfoundry/go-mod-core-contracts/v3/models"
sdkModels "github.com/edgexfoundry/device-sdk-go/v3/pkg/models"
)
// ProtocolDriver is a low-level device-specific interface used by
// other components of an EdgeX Device Service to interact with
// a specific class of devices.
type ProtocolDriver interface {
// Initialize performs protocol-specific initialization for the device service.
// The given *AsyncValues channel can be used to push asynchronous events and
// readings to Core Data. The given []DiscoveredDevice channel is used to send
// discovered devices that will be filtered and added to Core Metadata asynchronously.
Initialize(sdk DeviceServiceSDK) error
// HandleReadCommands passes a slice of CommandRequest struct each representing
// a ResourceOperation for a specific device resource.
HandleReadCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []sdkModels.CommandRequest) ([]*sdkModels.CommandValue, error)
// 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.
HandleWriteCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []sdkModels.CommandRequest, params []*sdkModels.CommandValue) error
// Stop instructs 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).
Stop(force bool) error
// Start runs Device Service startup tasks after the SDK has been completely initialized.
// This allows Device Service to safely use DeviceServiceSDK interface features in this function call.
Start() error
// AddDevice is a callback function that is invoked
// when a new Device associated with this Device Service is added
AddDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error
// UpdateDevice is a callback function that is invoked
// when a Device associated with this Device Service is updated
UpdateDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error
// RemoveDevice is a callback function that is invoked
// when a Device associated with this Device Service is removed
RemoveDevice(deviceName string, protocols map[string]models.ProtocolProperties) error
// Discover triggers protocol specific device discovery, asynchronously
// writes the results to the channel which is passed to the implementation
// via ProtocolDriver.Initialize(). The results may be added to the device service
// based on a set of acceptance criteria (i.e. Provision Watchers).
Discover() error
// ValidateDevice triggers device's protocol properties validation, returns error
// if validation failed and the incoming device will not be added into EdgeX.
ValidateDevice(device models.Device) error
}
我们再基于Device Service SDK开发对应设备驱动的时候,是需要实现这个接口的,后面会再介绍一下如果来定义一个Device Service。