From 7773e4ef87c710e98024a2f3fbe634ff0da12ffe Mon Sep 17 00:00:00 2001 From: xufangyou Date: Mon, 1 Nov 2021 15:38:19 +0800 Subject: [PATCH 01/12] init for syncing --- go.mod | 7 +++ go.sum | 9 ++++ internal/di/container.go | 75 ++++++++++++++++++++++++++++++ internal/di/type.go | 20 ++++++++ internal/logger/logger.go | 73 +++++++++++++++++++++++++++++ internal/service/device_service.go | 35 ++++++++++++++ pkg/bootstrap/bootstrap.go | 12 +++++ pkg/bootstrap/handler.go | 15 ++++++ pkg/bootstrap/timer.go | 4 ++ pkg/models/define.go | 12 +++++ pkg/models/device_data.go | 7 +++ pkg/models/protocol_driver.go | 5 ++ 12 files changed, 274 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/di/container.go create mode 100644 internal/di/type.go create mode 100644 internal/logger/logger.go create mode 100644 internal/service/device_service.go create mode 100644 pkg/bootstrap/bootstrap.go create mode 100644 pkg/bootstrap/handler.go create mode 100644 pkg/bootstrap/timer.go create mode 100644 pkg/models/define.go create mode 100644 pkg/models/device_data.go create mode 100644 pkg/models/protocol_driver.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..10a2436 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/thingio/edge-device-sdk + +go 1.16 + +require ( + github.com/sirupsen/logrus v1.8.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2b4c8b1 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/di/container.go b/internal/di/container.go new file mode 100644 index 0000000..68809c8 --- /dev/null +++ b/internal/di/container.go @@ -0,0 +1,75 @@ +package di + +import "sync" + +// NewContainer is a factory method that returns an initialized Container receiver struct. +func NewContainer(serviceConstructors ServiceConstructorMap) *Container { + c := &Container{ + serviceMap: map[string]service{}, + mutex: sync.RWMutex{}, + } + + if serviceConstructors != nil { + c.Update(serviceConstructors) + } + + return c +} + +type Get func(serviceName string) interface{} + +// ServiceConstructor defines the contract for a function/closure to create a service. +type ServiceConstructor func(get Get) interface{} + +// ServiceConstructorMap maps a service name to a function/closure to create that service. +type ServiceConstructorMap map[string]ServiceConstructor + +// service is an internal structure used to track a specific service's constructor and constructed instance. +type service struct { + constructor ServiceConstructor + instance interface{} +} + +// Container is a receiver that maintains a list of services, their constructors, and their constructed instances +// in a thread-safe manner. +type Container struct { + serviceMap map[string]service + mutex sync.RWMutex +} + +// Update updates the container's internal services included in serviceConstructors. +func (c *Container) Update(serviceConstructors ServiceConstructorMap) { + c.mutex.Lock() + defer c.mutex.Unlock() + for serviceName, constructor := range serviceConstructors { + c.serviceMap[serviceName] = service{ + constructor: constructor, + instance: nil, + } + } +} + +// get looks up the requested serviceName and, if it exists, returns a constructed instance. +// If the requested service does not exist, it returns nil. +// Get wraps instance construction in a singleton; the implementation assumes an instance, +// once constructed, will be reused and returned for all subsequent get(serviceName) calls. +func (c *Container) get(serviceName string) interface{} { + service, ok := c.serviceMap[serviceName] + if !ok { + return nil + } + + if service.instance == nil { + service.instance = service.constructor(c.get) + c.serviceMap[serviceName] = service + } + + return service.instance +} + +// Get wraps get to make it thread-safe. +func (c *Container) Get(serviceName string) interface{} { + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.get(serviceName) +} diff --git a/internal/di/type.go b/internal/di/type.go new file mode 100644 index 0000000..4b145cc --- /dev/null +++ b/internal/di/type.go @@ -0,0 +1,20 @@ +package di + +import ( + "fmt" + "reflect" +) + +// TypeInstanceToName converts an instance of a type to a unique name. +func TypeInstanceToName(v interface{}) string { + t := reflect.TypeOf(v) + + if name := t.Name(); name != "" { + // non-interface types + return fmt.Sprintf("%s.%s", t.PkgPath(), name) + } + + // interface types + e := t.Elem() + return fmt.Sprintf("%s.%s", e.PkgPath(), e.Name()) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..c6917ed --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,73 @@ +package logger + +import "github.com/sirupsen/logrus" + +type LogLevel string + +const ( + DebugLevel LogLevel = "debug" + InfoLevel LogLevel = "info" + WarnLevel LogLevel = "warn" + ErrorLevel LogLevel = "error" + FatalLevel LogLevel = "fatal" + PanicLevel LogLevel = "panic" +) + +func NewLogger() *Logger { + logger := &Logger{ + logger: logrus.New(), + } + _ = logger.SetLevel(InfoLevel) + return logger +} + +type Logger struct { + logger *logrus.Logger +} + +func (l Logger) SetLevel(level LogLevel) error { + lvl, err := logrus.ParseLevel(string(level)) + if err != nil { + return err + } + l.logger.SetLevel(lvl) + return nil +} + +func (l Logger) Debugf(format string, args ...interface{}) { + l.logger.Debugf(format, args) +} +func (l Logger) Infof(format string, args ...interface{}) { + l.logger.Infof(format, args) +} +func (l Logger) Warnf(format string, args ...interface{}) { + l.logger.Infof(format, args) +} +func (l Logger) Errorf(format string, args ...interface{}) { + l.logger.Errorf(format, args) +} +func (l Logger) Fatalf(format string, args ...interface{}) { + l.logger.Fatalf(format, args) +} +func (l Logger) Panicf(format string, args ...interface{}) { + l.logger.Panicf(format, args) +} + +func (l Logger) Debug(args ...interface{}) { + l.logger.Debug(args) +} +func (l Logger) Info(args ...interface{}) { + l.logger.Info(args) +} +func (l Logger) Warn(args ...interface{}) { + l.logger.Info(args) +} +func (l Logger) Error(format string, args ...interface{}) { + l.logger.Error(args) +} +func (l Logger) Fatal(args ...interface{}) { + l.logger.Fatal(args) +} +func (l Logger) Panic(args ...interface{}) { + l.logger.Panic(args) +} diff --git a/internal/service/device_service.go b/internal/service/device_service.go new file mode 100644 index 0000000..ca9b61b --- /dev/null +++ b/internal/service/device_service.go @@ -0,0 +1,35 @@ +package service + +import ( + "context" + "github.com/thingio/edge-device-sdk/internal/logger" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func Run(ctx context.Context, cancel context.CancelFunc, + serviceName string, serviceVersion string, driver models.ProtocolDriver) { + + ds := &DeviceService{ + Name: serviceName, + driver: driver, + logger: logger.NewLogger(), + } + + ds.Stop(false) +} + +type DeviceService struct { + ID string + Name string + Version string + driver models.ProtocolDriver + + logger *logger.Logger +} + +// Stop shuts down the service. +func (s *DeviceService) Stop(force bool) { + if err := s.driver.Stop(force); err != nil { + s.logger.Error(err.Error()) + } +} diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go new file mode 100644 index 0000000..fa96a44 --- /dev/null +++ b/pkg/bootstrap/bootstrap.go @@ -0,0 +1,12 @@ +package bootstrap + +import ( + "context" + "github.com/thingio/edge-device-sdk/internal/service" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func Bootstrap(serviceName string, serviceVersion string, driver models.ProtocolDriver) { + ctx, cancel := context.WithCancel(context.Background()) + service.Run(ctx, cancel, serviceName, serviceVersion, driver) +} diff --git a/pkg/bootstrap/handler.go b/pkg/bootstrap/handler.go new file mode 100644 index 0000000..3fe3d19 --- /dev/null +++ b/pkg/bootstrap/handler.go @@ -0,0 +1,15 @@ +package bootstrap + +import ( + "context" + "github.com/thingio/edge-device-sdk/internal/di" + "sync" +) + +// Handler defines the contract that each bootstrap handler must fulfill. +// The 1st argument ctx is responsible for shutting down handler gracefully after receiving ctx.Done(). +// The 2nd argument wg is used to let the main goroutine exit after all handlers have finished. +// The 3rd argument dic maintains all services, their constructors and their constructed instances. +// The 4th argument startupTimer is responsible for setting and monitoring the startup time of each handler. +// Implementation returns true if the handler completed successfully, false if it did not. +type Handler func(ctx context.Context, wg *sync.WaitGroup, dic *di.Container, startupTimer Timer) (success bool) diff --git a/pkg/bootstrap/timer.go b/pkg/bootstrap/timer.go new file mode 100644 index 0000000..b508639 --- /dev/null +++ b/pkg/bootstrap/timer.go @@ -0,0 +1,4 @@ +package bootstrap + +type Timer struct { +} diff --git a/pkg/models/define.go b/pkg/models/define.go new file mode 100644 index 0000000..c65bca1 --- /dev/null +++ b/pkg/models/define.go @@ -0,0 +1,12 @@ +package models + +type DeviceOperation string + +const ( + DeviceRead DeviceOperation = "read" // Device Property Read + DeviceWrite DeviceOperation = "write" // Device Property Write + DeviceEvent DeviceOperation = "event" // Device Event Receive + DeviceRequest DeviceOperation = "request" // Device Service Request + DeviceResponse DeviceOperation = "response" // Device Service Response + DeviceOperationError DeviceOperation = "error" // Device Operation Error +) diff --git a/pkg/models/device_data.go b/pkg/models/device_data.go new file mode 100644 index 0000000..301d255 --- /dev/null +++ b/pkg/models/device_data.go @@ -0,0 +1,7 @@ +package models + +type DeviceData struct { + DeviceID string + ProductID string + OptType DeviceOperation +} diff --git a/pkg/models/protocol_driver.go b/pkg/models/protocol_driver.go new file mode 100644 index 0000000..c0ecec8 --- /dev/null +++ b/pkg/models/protocol_driver.go @@ -0,0 +1,5 @@ +package models + +type ProtocolDriver interface { + Stop(force bool) error +} From d0a4f6eb739fed0c8904570e1f59ac40c963716b Mon Sep 17 00:00:00 2001 From: xufangyou Date: Wed, 10 Nov 2021 20:58:21 +0800 Subject: [PATCH 02/12] update zh-readme --- docs/zh/README.md | 77 ++++++++++++++++++++++++++++++ go.mod | 4 -- internal/di/container.go | 75 ----------------------------- internal/di/type.go | 20 -------- internal/logger/logger.go | 73 ---------------------------- internal/service/device_service.go | 35 -------------- pkg/bootstrap/bootstrap.go | 12 ----- pkg/bootstrap/handler.go | 15 ------ pkg/bootstrap/timer.go | 4 -- pkg/models/define.go | 12 ----- pkg/models/device_data.go | 7 --- pkg/models/protocol_driver.go | 5 -- 12 files changed, 77 insertions(+), 262 deletions(-) create mode 100644 docs/zh/README.md delete mode 100644 internal/di/container.go delete mode 100644 internal/di/type.go delete mode 100644 internal/logger/logger.go delete mode 100644 internal/service/device_service.go delete mode 100644 pkg/bootstrap/bootstrap.go delete mode 100644 pkg/bootstrap/handler.go delete mode 100644 pkg/bootstrap/timer.go delete mode 100644 pkg/models/define.go delete mode 100644 pkg/models/device_data.go delete mode 100644 pkg/models/protocol_driver.go diff --git a/docs/zh/README.md b/docs/zh/README.md new file mode 100644 index 0000000..53af045 --- /dev/null +++ b/docs/zh/README.md @@ -0,0 +1,77 @@ +# EDS + +**EDS**(Edge Device SDK)是为 IOT 场景设计并开发的设备接入层 SDK,提供快速将多种协议的设备接入平台的能力。 + +## 特性 + +1. 轻量:只使用 MQ 作为设备服务与设备中心之间数据交换的中间件,无需引入多余组件; +2. 通用: + 1. 将 MQ 封装为 MessageBus,包括常见配置与操作,因此可以很方便地替换所依赖的 MQ 组件; + 2. 基于 MessageBus 定义和实现了通用的**元数据**与**物模型**操作规范; + +## 术语 + +### 物模型 + +[物模型](https://blog.csdn.net/zjccoder/article/details/107050046),作为一类设备的抽象,描述了设备的: + +- 属性(property):用于描述设备状态,支持读取和写入; +- 方法(method):设备可被外部调用的能力或方法,可设置输入和输出参数,参数必须是某个“属性”,相比与属性,方法可通过一条指令实现更复杂的业务逻辑; +- 事件(event):用于描述设备主动上报的事件,可包含多个输入参数,参数必须是某个“属性”。 + +产品(Product):即物模型。 + +设备(Device):与真实设备一一对应,必须属于并且仅属于某一个 Product。 + +### 元数据 + +元数据包括协议(Protocol)、产品(Product)及设备(Device): + +- 协议对应了一个特定协议的描述信息; +- 产品对应了一个特定产品的描述信息; +- 设备对应了一个特定设备的描述信息。 + +### 设备服务 + +设备协议驱动服务的简称,负责一种特定的设备协议的接入实现。主要功能包括: + +- 协议注册:将当前设备协议注册到设备管理中心; +- 设备初始化:从设备重新拉取产品 & 设备等元数据,加载驱动; +- 元数据监听:监听设备中心产品 & 设备等元数据的变更,加载/卸载/重加载驱动; +- 数据接入:与实际设备进行连接,向 MessageBus 发送采集的设备数据(设备中心负责从 MessageBus 中读取出数据); +- 命令执行:与实际设备进行连接,从 MessageBus 获取调用的方法执行(设备中心负责将执行命令写入到 MessageBus); + +### 设备中心 + +设备管理中心的简称,由 SDK 提供基础服务,嵌入到平台中使用。主要功能包括: + +- 协议管理:接收设备服务的注册请求 & 设备服务探活(失活则更新设备元数据); +- 产品管理:基于特定协议定义产品 & 产品 CURD; +- 设备管理:基于特定产品定义设备 & 设备 CURD; + +## Topic 约定 + +因为 EDS 基于 MQ 实现数据交换,所以我们基于 MQ 的 Topic & Payload 概念定义了我们自己的数据格式及通信规范。 + +### 物模型 + +对于物模型来说,Topic 格式为 `DATA/{ProductID}/{DeviceID}/{OptType}/{DataID}`: + +- `ProductID`:设备元数据中产品的 UUID,产品唯一; +- `DeviceID`:设备元数据中设备的 UUID,设备唯一; +- `OptType`,产生当前数据的操作类型: + - 对于 `property`,可选 `read | write`,分别对应于属性的读取和写入; + - 对于 `method`,可选 `request | response | error`,分别对应于方法的请求、响应和出错; + - 对于 `event`,可选 `event`,分别对应于事件上报。 +- `DataID`,当前数据的 UUID: + - 对于隶属于同一个方法调用 `method call`,其 `request` 与 `response` 的 `DataID` 是相同的; + - 对于非方法调用的其它类型的数据,其 `DataID` 都是不同的。 + +### 元数据 + +对于元数据来说,Topic 格式为 `META/{MetaType}/{Method}/{OptType}/{DataID}`: + +- `MetaType`:元数据类型,可选 `protocol | product | device`; +- `MethodType`:调用方法,可选 `create | update | delete | get | list`,对于不同的元数据类型,可选范围是不同的; +- `DataType`:数据类型,可选 `request | response | error`; +- `DataID`:当前数据的 UUID,特别地,对于同一个方法,其 `request` 与 `response` 的 `DataID` 是相同的; \ No newline at end of file diff --git a/go.mod b/go.mod index 10a2436..8779f5c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,3 @@ module github.com/thingio/edge-device-sdk go 1.16 - -require ( - github.com/sirupsen/logrus v1.8.1 -) diff --git a/internal/di/container.go b/internal/di/container.go deleted file mode 100644 index 68809c8..0000000 --- a/internal/di/container.go +++ /dev/null @@ -1,75 +0,0 @@ -package di - -import "sync" - -// NewContainer is a factory method that returns an initialized Container receiver struct. -func NewContainer(serviceConstructors ServiceConstructorMap) *Container { - c := &Container{ - serviceMap: map[string]service{}, - mutex: sync.RWMutex{}, - } - - if serviceConstructors != nil { - c.Update(serviceConstructors) - } - - return c -} - -type Get func(serviceName string) interface{} - -// ServiceConstructor defines the contract for a function/closure to create a service. -type ServiceConstructor func(get Get) interface{} - -// ServiceConstructorMap maps a service name to a function/closure to create that service. -type ServiceConstructorMap map[string]ServiceConstructor - -// service is an internal structure used to track a specific service's constructor and constructed instance. -type service struct { - constructor ServiceConstructor - instance interface{} -} - -// Container is a receiver that maintains a list of services, their constructors, and their constructed instances -// in a thread-safe manner. -type Container struct { - serviceMap map[string]service - mutex sync.RWMutex -} - -// Update updates the container's internal services included in serviceConstructors. -func (c *Container) Update(serviceConstructors ServiceConstructorMap) { - c.mutex.Lock() - defer c.mutex.Unlock() - for serviceName, constructor := range serviceConstructors { - c.serviceMap[serviceName] = service{ - constructor: constructor, - instance: nil, - } - } -} - -// get looks up the requested serviceName and, if it exists, returns a constructed instance. -// If the requested service does not exist, it returns nil. -// Get wraps instance construction in a singleton; the implementation assumes an instance, -// once constructed, will be reused and returned for all subsequent get(serviceName) calls. -func (c *Container) get(serviceName string) interface{} { - service, ok := c.serviceMap[serviceName] - if !ok { - return nil - } - - if service.instance == nil { - service.instance = service.constructor(c.get) - c.serviceMap[serviceName] = service - } - - return service.instance -} - -// Get wraps get to make it thread-safe. -func (c *Container) Get(serviceName string) interface{} { - c.mutex.RLock() - defer c.mutex.RUnlock() - return c.get(serviceName) -} diff --git a/internal/di/type.go b/internal/di/type.go deleted file mode 100644 index 4b145cc..0000000 --- a/internal/di/type.go +++ /dev/null @@ -1,20 +0,0 @@ -package di - -import ( - "fmt" - "reflect" -) - -// TypeInstanceToName converts an instance of a type to a unique name. -func TypeInstanceToName(v interface{}) string { - t := reflect.TypeOf(v) - - if name := t.Name(); name != "" { - // non-interface types - return fmt.Sprintf("%s.%s", t.PkgPath(), name) - } - - // interface types - e := t.Elem() - return fmt.Sprintf("%s.%s", e.PkgPath(), e.Name()) -} diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index c6917ed..0000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,73 +0,0 @@ -package logger - -import "github.com/sirupsen/logrus" - -type LogLevel string - -const ( - DebugLevel LogLevel = "debug" - InfoLevel LogLevel = "info" - WarnLevel LogLevel = "warn" - ErrorLevel LogLevel = "error" - FatalLevel LogLevel = "fatal" - PanicLevel LogLevel = "panic" -) - -func NewLogger() *Logger { - logger := &Logger{ - logger: logrus.New(), - } - _ = logger.SetLevel(InfoLevel) - return logger -} - -type Logger struct { - logger *logrus.Logger -} - -func (l Logger) SetLevel(level LogLevel) error { - lvl, err := logrus.ParseLevel(string(level)) - if err != nil { - return err - } - l.logger.SetLevel(lvl) - return nil -} - -func (l Logger) Debugf(format string, args ...interface{}) { - l.logger.Debugf(format, args) -} -func (l Logger) Infof(format string, args ...interface{}) { - l.logger.Infof(format, args) -} -func (l Logger) Warnf(format string, args ...interface{}) { - l.logger.Infof(format, args) -} -func (l Logger) Errorf(format string, args ...interface{}) { - l.logger.Errorf(format, args) -} -func (l Logger) Fatalf(format string, args ...interface{}) { - l.logger.Fatalf(format, args) -} -func (l Logger) Panicf(format string, args ...interface{}) { - l.logger.Panicf(format, args) -} - -func (l Logger) Debug(args ...interface{}) { - l.logger.Debug(args) -} -func (l Logger) Info(args ...interface{}) { - l.logger.Info(args) -} -func (l Logger) Warn(args ...interface{}) { - l.logger.Info(args) -} -func (l Logger) Error(format string, args ...interface{}) { - l.logger.Error(args) -} -func (l Logger) Fatal(args ...interface{}) { - l.logger.Fatal(args) -} -func (l Logger) Panic(args ...interface{}) { - l.logger.Panic(args) -} diff --git a/internal/service/device_service.go b/internal/service/device_service.go deleted file mode 100644 index ca9b61b..0000000 --- a/internal/service/device_service.go +++ /dev/null @@ -1,35 +0,0 @@ -package service - -import ( - "context" - "github.com/thingio/edge-device-sdk/internal/logger" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func Run(ctx context.Context, cancel context.CancelFunc, - serviceName string, serviceVersion string, driver models.ProtocolDriver) { - - ds := &DeviceService{ - Name: serviceName, - driver: driver, - logger: logger.NewLogger(), - } - - ds.Stop(false) -} - -type DeviceService struct { - ID string - Name string - Version string - driver models.ProtocolDriver - - logger *logger.Logger -} - -// Stop shuts down the service. -func (s *DeviceService) Stop(force bool) { - if err := s.driver.Stop(force); err != nil { - s.logger.Error(err.Error()) - } -} diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go deleted file mode 100644 index fa96a44..0000000 --- a/pkg/bootstrap/bootstrap.go +++ /dev/null @@ -1,12 +0,0 @@ -package bootstrap - -import ( - "context" - "github.com/thingio/edge-device-sdk/internal/service" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func Bootstrap(serviceName string, serviceVersion string, driver models.ProtocolDriver) { - ctx, cancel := context.WithCancel(context.Background()) - service.Run(ctx, cancel, serviceName, serviceVersion, driver) -} diff --git a/pkg/bootstrap/handler.go b/pkg/bootstrap/handler.go deleted file mode 100644 index 3fe3d19..0000000 --- a/pkg/bootstrap/handler.go +++ /dev/null @@ -1,15 +0,0 @@ -package bootstrap - -import ( - "context" - "github.com/thingio/edge-device-sdk/internal/di" - "sync" -) - -// Handler defines the contract that each bootstrap handler must fulfill. -// The 1st argument ctx is responsible for shutting down handler gracefully after receiving ctx.Done(). -// The 2nd argument wg is used to let the main goroutine exit after all handlers have finished. -// The 3rd argument dic maintains all services, their constructors and their constructed instances. -// The 4th argument startupTimer is responsible for setting and monitoring the startup time of each handler. -// Implementation returns true if the handler completed successfully, false if it did not. -type Handler func(ctx context.Context, wg *sync.WaitGroup, dic *di.Container, startupTimer Timer) (success bool) diff --git a/pkg/bootstrap/timer.go b/pkg/bootstrap/timer.go deleted file mode 100644 index b508639..0000000 --- a/pkg/bootstrap/timer.go +++ /dev/null @@ -1,4 +0,0 @@ -package bootstrap - -type Timer struct { -} diff --git a/pkg/models/define.go b/pkg/models/define.go deleted file mode 100644 index c65bca1..0000000 --- a/pkg/models/define.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -type DeviceOperation string - -const ( - DeviceRead DeviceOperation = "read" // Device Property Read - DeviceWrite DeviceOperation = "write" // Device Property Write - DeviceEvent DeviceOperation = "event" // Device Event Receive - DeviceRequest DeviceOperation = "request" // Device Service Request - DeviceResponse DeviceOperation = "response" // Device Service Response - DeviceOperationError DeviceOperation = "error" // Device Operation Error -) diff --git a/pkg/models/device_data.go b/pkg/models/device_data.go deleted file mode 100644 index 301d255..0000000 --- a/pkg/models/device_data.go +++ /dev/null @@ -1,7 +0,0 @@ -package models - -type DeviceData struct { - DeviceID string - ProductID string - OptType DeviceOperation -} diff --git a/pkg/models/protocol_driver.go b/pkg/models/protocol_driver.go deleted file mode 100644 index c0ecec8..0000000 --- a/pkg/models/protocol_driver.go +++ /dev/null @@ -1,5 +0,0 @@ -package models - -type ProtocolDriver interface { - Stop(force bool) error -} From 7a10ef5c187f8090d96589c51da518a70e0e7601 Mon Sep 17 00:00:00 2001 From: xufangyou Date: Wed, 10 Nov 2021 20:58:39 +0800 Subject: [PATCH 03/12] update zh-readme --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 66fd13c..69a17e0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ + +# IDEA +.idea \ No newline at end of file From c816f49a7bdd7eee140c9cf80038a2b4ae8268bc Mon Sep 17 00:00:00 2001 From: xufangyou Date: Wed, 10 Nov 2021 20:58:39 +0800 Subject: [PATCH 04/12] update zh-readme --- .gitignore | 5 ++++- docs/zh/README.md | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 66fd13c..69a17e0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ + +# IDEA +.idea \ No newline at end of file diff --git a/docs/zh/README.md b/docs/zh/README.md index 53af045..7ed17bd 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -4,10 +4,10 @@ ## 特性 -1. 轻量:只使用 MQ 作为设备服务与设备中心之间数据交换的中间件,无需引入多余组件; +1. 轻量:只使用 MQTT 作为设备服务与设备中心之间数据交换的中间件,无需引入多余组件; 2. 通用: - 1. 将 MQ 封装为 MessageBus,包括常见配置与操作,因此可以很方便地替换所依赖的 MQ 组件; - 2. 基于 MessageBus 定义和实现了通用的**元数据**与**物模型**操作规范; + 1. 将 MQTT 封装为 MessageBus,向上层模块提供支持; + 2. 基于 MessageBus 定义和实现了通用的**元数据操作**与**物模型**操作规范。 ## 术语 @@ -38,20 +38,20 @@ - 协议注册:将当前设备协议注册到设备管理中心; - 设备初始化:从设备重新拉取产品 & 设备等元数据,加载驱动; - 元数据监听:监听设备中心产品 & 设备等元数据的变更,加载/卸载/重加载驱动; -- 数据接入:与实际设备进行连接,向 MessageBus 发送采集的设备数据(设备中心负责从 MessageBus 中读取出数据); -- 命令执行:与实际设备进行连接,从 MessageBus 获取调用的方法执行(设备中心负责将执行命令写入到 MessageBus); +- 数据接入:与实际设备进行连接,向 MessageBus 发送采集的设备数据(设备中心负责从 MessageBus 中读取出数据); +- 命令执行:与实际设备进行连接,从 MessageBus 获取调用的方法执行(设备中心负责将执行命令写入到 MessageBus)。 ### 设备中心 设备管理中心的简称,由 SDK 提供基础服务,嵌入到平台中使用。主要功能包括: - 协议管理:接收设备服务的注册请求 & 设备服务探活(失活则更新设备元数据); -- 产品管理:基于特定协议定义产品 & 产品 CURD; -- 设备管理:基于特定产品定义设备 & 设备 CURD; +- 产品管理:基于特定协议定义产品 & 产品 CRUD; +- 设备管理:基于特定产品定义设备 & 设备 CRUD。 ## Topic 约定 -因为 EDS 基于 MQ 实现数据交换,所以我们基于 MQ 的 Topic & Payload 概念定义了我们自己的数据格式及通信规范。 +因为 EDS 基于 MQTT 实现数据交换,所以我们基于 MQTT 的 Topic & Payload 概念定义了我们自己的数据格式及通信规范。 ### 物模型 @@ -67,11 +67,11 @@ - 对于隶属于同一个方法调用 `method call`,其 `request` 与 `response` 的 `DataID` 是相同的; - 对于非方法调用的其它类型的数据,其 `DataID` 都是不同的。 -### 元数据 +### 元数据操作 -对于元数据来说,Topic 格式为 `META/{MetaType}/{Method}/{OptType}/{DataID}`: +对于元数据增删改查等操作来说,Topic 格式为 `META/{MetaType}/{Method}/{OptType}/{DataID}`: - `MetaType`:元数据类型,可选 `protocol | product | device`; - `MethodType`:调用方法,可选 `create | update | delete | get | list`,对于不同的元数据类型,可选范围是不同的; - `DataType`:数据类型,可选 `request | response | error`; -- `DataID`:当前数据的 UUID,特别地,对于同一个方法,其 `request` 与 `response` 的 `DataID` 是相同的; \ No newline at end of file +- `DataID`:当前数据的 UUID,特别地,对于同一个方法,其 `request` 与 `response` 的 `DataID` 是相同的。 \ No newline at end of file From 45089ae15ac6255f9a37f714fd7bad38662da0d2 Mon Sep 17 00:00:00 2001 From: xufangyou Date: Fri, 12 Nov 2021 14:47:31 +0800 Subject: [PATCH 05/12] define metadata --- pkg/models/device.go | 19 +++++++++++++++ pkg/models/product.go | 55 ++++++++++++++++++++++++++++++++++++++++++ pkg/models/property.go | 37 ++++++++++++++++++++++++++++ pkg/models/protocol.go | 12 +++++++++ 4 files changed, 123 insertions(+) create mode 100644 pkg/models/device.go create mode 100644 pkg/models/product.go create mode 100644 pkg/models/property.go create mode 100644 pkg/models/protocol.go diff --git a/pkg/models/device.go b/pkg/models/device.go new file mode 100644 index 0000000..4d8b719 --- /dev/null +++ b/pkg/models/device.go @@ -0,0 +1,19 @@ +package models + +type Device struct { + ID string `json:"id"` // 设备 ID + Name string `json:"name"` // 设备名称 + Desc string `json:"desc"` // 设备描述 + ProductID string `json:"product_id"` // 设备所属产品 ID, 不可更新 + ProductName string `json:"product_name"` // 设备所属产品名称 + Category string `json:"category"` // 设备类型(多媒体, 时序), 不可更新 + Recording bool `json:"recording"` // 是否正在录制 + DeviceStatus string `json:"device_status"` // 设备状态 + DeviceProps map[string]string `json:"device_props"` // 设备动态属性, 取决于具体的设备协议 + DeviceLabels map[string]string `json:"device_labels"` // 设备标签 + DeviceMeta map[string]string `json:"device_meta"` // 视频流元信息 +} + +func (d *Device) GetProperty(key string) string { + return d.DeviceProps[key] +} diff --git a/pkg/models/product.go b/pkg/models/product.go new file mode 100644 index 0000000..cbd6a53 --- /dev/null +++ b/pkg/models/product.go @@ -0,0 +1,55 @@ +package models + +type Product struct { + ID string `json:"id"` // 产品 ID + Name string `json:"name"` // 产品名称 + Desc string `json:"desc"` // 产品描述 + Protocol string `json:"protocol"` // 产品协议 + DataFormat string `json:"data_format,omitempty"` // 数据格式 + Properties []*ProductProperty `json:"properties,omitempty"` // 属性功能列表 + Events []*ProductEvent `json:"events,omitempty"` // 事件功能列表 + Methods []*ProductMethod `json:"methods,omitempty"` // 方法功能列表 + Topics []*ProductTopic `json:"topics,omitempty"` // 各功能对应的消息主题 +} + +type ProductProperty struct { + Id string `json:"id"` + Name string `json:"name"` + Desc string `json:"desc"` + Interval string `json:"interval"` + Unit string `json:"unit"` + FieldType string `json:"field_type"` + ReportMode string `json:"report_mode"` + Writeable bool `json:"writeable"` + AuxProps map[string]string `json:"aux_props"` +} + +type ProductEvent struct { + Id string `json:"id"` + Name string `json:"name"` + Desc string `json:"desc"` + Outs []*ProductField `json:"outs"` + AuxProps map[string]string `json:"aux_props"` +} + +type ProductMethod struct { + Id string `json:"id"` + Name string `json:"name"` + Desc string `json:"desc"` + Ins []*ProductField `json:"ins"` + Outs []*ProductField `json:"outs"` + AuxProps map[string]string `json:"aux_props"` +} + +type ProductTopic struct { + Topic string `json:"topic"` + OptType string `json:"opt_type"` + Desc string `json:"desc"` +} + +type ProductField struct { + Id string `json:"id"` + Name string `json:"name"` + FieldType string `json:"field_type"` + Desc string `json:"desc"` +} diff --git a/pkg/models/property.go b/pkg/models/property.go new file mode 100644 index 0000000..69e186e --- /dev/null +++ b/pkg/models/property.go @@ -0,0 +1,37 @@ +package models + +type ( + PropertyType = string + PropertyStyle = string // UI Representation Style +) + +const ( + PropertyTypeInt PropertyType = "int" + PropertyTypeUint PropertyType = "uint" + PropertyTypeFloat PropertyType = "float" + PropertyTypeBool PropertyType = "bool" + PropertyTypeString PropertyType = "string" +) + +var ( + DeviceDataPropertyTypes = map[PropertyType]struct{}{ + PropertyTypeInt: {}, + PropertyTypeUint: {}, + PropertyTypeFloat: {}, + PropertyTypeBool: {}, + PropertyTypeString: {}, + } +) + +type Property struct { + Name string `json:"name"` // Name 为属性的展示名称 + Desc string `json:"desc"` // Desc 为属性的描述, 通常以名称旁的?形式进行展示 + Type PropertyType `json:"type"` // Type 为该属性的数据类型 + Style PropertyStyle `json:"style"` // Style 为该属性在前端的展示样式 + Default string `json:"default"` // Default 该属性默认的属性值 + Range string `json:"range"` // Range 为属性值的可选范围 + Precondition string `json:"precondition"` // Precondition 为当前属性展示的前置条件, 用来实现简单的动态依赖功能 + Required bool `json:"required"` // Required 表示该属性是否为必填项 + Multiple bool `json:"multiple"` // Multiple 表示是否支持多选(下拉框), 列表(输入), Map(K,V) + MaxLen int64 `json:"max_len"` // MaxLen 表示当Multiple为true时, 可选择的最大数量 +} diff --git a/pkg/models/protocol.go b/pkg/models/protocol.go new file mode 100644 index 0000000..ad2d544 --- /dev/null +++ b/pkg/models/protocol.go @@ -0,0 +1,12 @@ +package models + +type Protocol struct { + ID string `json:"id"` // 协议 ID + Name string `json:"name"` // 协议名称 + Desc string `json:"desc"` // 协议描述 + Category string `json:"category"` + Language string `json:"language"` + SupportFuncs []string `json:"support_funcs"` + AuxProps []*Property `json:"aux_props"` + DeviceProps []*Property `json:"device_props"` +} From 8347a28dc2bfeabe59e856a07fa16f1710e3663e Mon Sep 17 00:00:00 2001 From: xufangyou Date: Fri, 12 Nov 2021 18:31:19 +0800 Subject: [PATCH 06/12] define message bus & data --- config/message_bus_options.go | 19 +++ go.mod | 5 + go.sum | 17 +- internal/logger/logger.go | 110 +++++++++++++ internal/message_bus/bus.go | 236 ++++++++++++++++++++++++++++ internal/message_bus/data.go | 51 ++++++ internal/message_bus/data_device.go | 94 +++++++++++ internal/message_bus/data_meta.go | 81 ++++++++++ internal/message_bus/message.go | 29 ++++ internal/message_bus/topic.go | 136 ++++++++++++++++ pkg/models/product.go | 2 + 11 files changed, 777 insertions(+), 3 deletions(-) create mode 100644 config/message_bus_options.go create mode 100644 internal/logger/logger.go create mode 100644 internal/message_bus/bus.go create mode 100644 internal/message_bus/data.go create mode 100644 internal/message_bus/data_device.go create mode 100644 internal/message_bus/data_meta.go create mode 100644 internal/message_bus/message.go create mode 100644 internal/message_bus/topic.go diff --git a/config/message_bus_options.go b/config/message_bus_options.go new file mode 100644 index 0000000..05bac99 --- /dev/null +++ b/config/message_bus_options.go @@ -0,0 +1,19 @@ +package config + +type MessageBusOptions struct { + // Host is the hostname or IP address of the MQTT broker. + Host string `json:"host" yaml:"host"` + // Port is the port of the MQTT broker. + Port int `json:"port" yaml:"port"` + // Protocol is the protocol to use when communicating with the MQTT broker, such as "tcp". + Protocol string `json:"protocol" yaml:"protocol"` + + // ConnectTimoutMillisecond indicates the timeout of connecting to the MQTT broker. + ConnectTimoutMillisecond int `json:"connect_timout_millisecond" yaml:"connect_timout_millisecond"` + // TimeoutMillisecond indicates the timeout of manipulations. + TimeoutMillisecond int `json:"timeout_millisecond" yaml:"timeout_millisecond"` + // QoS is the abbreviation of MQTT Quality of Service. + QoS int `json:"qos" yaml:"qos"` + // CleanSession indicates whether retain messages after reconnecting for QoS1 and QoS2. + CleanSession bool `json:"clean_session" yaml:"clean_session"` +} diff --git a/go.mod b/go.mod index 8779f5c..1175ce2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module github.com/thingio/edge-device-sdk go 1.16 + +require ( + github.com/eclipse/paho.mqtt.golang v1.3.5 + github.com/sirupsen/logrus v1.8.1 +) diff --git a/go.sum b/go.sum index 2b4c8b1..29ad16c 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,20 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= +github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..42f5737 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,110 @@ +package logger + +import ( + "github.com/sirupsen/logrus" + "os" +) + +type LogLevel string + +const ( + DebugLevel LogLevel = "debug" + InfoLevel LogLevel = "info" + WarnLevel LogLevel = "warn" + ErrorLevel LogLevel = "error" + FatalLevel LogLevel = "fatal" + PanicLevel LogLevel = "panic" +) + +func NewLogger() *Logger { + logger := &Logger{ + logger: logrus.NewEntry(logrus.New()), + } + _ = logger.SetLevel(InfoLevel) + return logger +} + +type Logger struct { + logger *logrus.Entry +} + +func (l Logger) SetLevel(level LogLevel) error { + lvl, err := logrus.ParseLevel(string(level)) + if err != nil { + return err + } + l.logger.Logger.SetLevel(lvl) + l.logger.Logger.SetOutput(os.Stdout) + l.logger.Logger.SetFormatter(&logFormatter{logrus.TextFormatter{FullTimestamp: true, ForceColors: true}}) + return nil +} + +// WithFields adds a map of fields to the Entry. +func (l Logger) WithFields(vs ...string) *logrus.Entry { + // mutex.Lock() + // defer mutex.Unlock() + fs := logrus.Fields{} + for index := 0; index < len(vs)-1; index = index + 2 { + fs[vs[index]] = vs[index+1] + } + return l.logger.WithFields(fs) +} + +// WithError adds an error as single field (using the key defined in ErrorKey) to the Entry. +func (l Logger) WithError(err error) *logrus.Entry { + if err == nil { + return l.logger + } + + return l.logger.WithField(logrus.ErrorKey, err.Error()) +} + +func (l Logger) Debugf(format string, args ...interface{}) { + l.logger.Debugf(format, args...) +} +func (l Logger) Infof(format string, args ...interface{}) { + l.logger.Infof(format, args...) +} +func (l Logger) Warnf(format string, args ...interface{}) { + l.logger.Infof(format, args...) +} +func (l Logger) Errorf(format string, args ...interface{}) { + l.logger.Errorf(format, args...) +} +func (l Logger) Fatalf(format string, args ...interface{}) { + l.logger.Fatalf(format, args...) +} +func (l Logger) Panicf(format string, args ...interface{}) { + l.logger.Panicf(format, args...) +} + +func (l Logger) Debug(args ...interface{}) { + l.logger.Debug(args...) +} +func (l Logger) Info(args ...interface{}) { + l.logger.Info(args...) +} +func (l Logger) Warn(args ...interface{}) { + l.logger.Info(args...) +} +func (l Logger) Error(args ...interface{}) { + l.logger.Error(args...) +} +func (l Logger) Fatal(args ...interface{}) { + l.logger.Fatal(args...) +} +func (l Logger) Panic(args ...interface{}) { + l.logger.Panic(args) +} + +type logFormatter struct { + logrus.TextFormatter +} + +func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) { + data, err := f.TextFormatter.Format(entry) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/internal/message_bus/bus.go b/internal/message_bus/bus.go new file mode 100644 index 0000000..4f222b3 --- /dev/null +++ b/internal/message_bus/bus.go @@ -0,0 +1,236 @@ +package bus + +import ( + "fmt" + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/thingio/edge-device-sdk/config" + "github.com/thingio/edge-device-sdk/internal/logger" + "strconv" + "sync" + "time" +) + +func NewMessageBus(opts *config.MessageBusOptions, logger *logger.Logger) (MessageBus, error) { + mb := messageBus{ + timeout: time.Millisecond * time.Duration(opts.TimeoutMillisecond), + qos: opts.QoS, + routes: make(map[string]MessageHandler), + serves: make(map[string]chan Data), + mutex: sync.Mutex{}, + logger: logger, + } + if err := mb.setClient(opts); err != nil { + return nil, err + } + return &mb, nil +} + +// MessageBus encapsulates all common manipulations based on MQTT. +type MessageBus interface { + IsConnected() bool + + Connect() error + + Disconnected() error + + Publish(data Data) error + + Subscribe(handler MessageHandler, topics ...string) error + + Unsubscribe(topics ...string) error + + Call(request Data) (response Data, err error) + + Serve(request Data) (responses <-chan Data, err error) +} + +type messageBus struct { + client mqtt.Client + timeout time.Duration + qos int + + routes map[string]MessageHandler // topic -> handler + serves map[string]chan Data + + mutex sync.Mutex + logger *logger.Logger +} + +func (mb *messageBus) IsConnected() bool { + return mb.client.IsConnected() +} + +func (mb *messageBus) Connect() error { + if mb.IsConnected() { + return nil + } + + token := mb.client.Connect() + return mb.handleToken(token) +} + +func (mb *messageBus) Disconnected() error { + if mb.IsConnected() { + mb.client.Disconnect(2000) // waiting 2s + } + return nil +} + +func (mb *messageBus) Publish(data Data) error { + msg, err := data.ToMessage() + if err != nil { + return err + } + + token := mb.client.Publish(msg.Topic, byte(mb.qos), false, msg.Payload) + return mb.handleToken(token) +} + +func (mb *messageBus) Subscribe(handler MessageHandler, topics ...string) error { + mb.mutex.Lock() + defer mb.mutex.Unlock() + + filters := make(map[string]byte) + for _, topic := range topics { + mb.routes[topic] = handler + filters[topic] = byte(mb.qos) + } + callback := func(mc mqtt.Client, msg mqtt.Message) { + go handler(&Message{ + Topic: msg.Topic(), + Payload: msg.Payload(), + }) + } + + token := mb.client.SubscribeMultiple(filters, callback) + return mb.handleToken(token) +} + +func (mb *messageBus) Unsubscribe(topics ...string) error { + mb.mutex.Lock() + defer mb.mutex.Unlock() + + for _, topic := range topics { + delete(mb.routes, topic) + } + + token := mb.client.Unsubscribe(topics...) + return mb.handleToken(token) +} + +func (mb *messageBus) Call(request Data) (response Data, err error) { + response, err = request.Response() + if err != nil { + return nil, err + } + + // subscribe response + rspMsg, err := response.ToMessage() + if err != nil { + return nil, err + } + ch := make(chan *Message, 1) + rspTpc := rspMsg.Topic + if err = mb.Subscribe(func(msg *Message) { + ch <- msg + }, rspTpc); err != nil { + return nil, err + } + defer func() { + close(ch) + _ = mb.Unsubscribe(rspTpc) + }() + + // publish request + if err = mb.Publish(request); err != nil { + return nil, err + } + // waiting for the response + ticker := time.NewTicker(mb.timeout) + select { + case msg := <-ch: + _, fields, err := msg.Parse() + if err != nil { + return nil, fmt.Errorf("fail to parse message, got %s", err.Error()) + } + response.SetFields(fields) + return response, nil + case <-ticker.C: + ticker.Stop() + return nil, fmt.Errorf("call timeout: %dms", mb.timeout/time.Millisecond) + } +} + +func (mb *messageBus) Serve(request Data) (<-chan Data, error) { + // subscribe request + reqMsg, err := request.ToMessage() + if err != nil { + return nil, err + } + reqTpc := reqMsg.Topic + + mb.serves[reqTpc] = make(chan Data, 1) + if err = mb.Subscribe(func(msg *Message) { + // publish response + response, err := request.Response() + if err != nil { + mb.logger.WithError(err).Errorf("fail to parse the request and construct the response") + return + } + _, fields, err := msg.Parse() + if err != nil { + mb.logger.WithError(err).Error("fail to parse the message") + return + } + response.SetFields(fields) + mb.serves[reqTpc] <- response + }, reqTpc); err != nil { + return nil, err + } + defer func() { + close(mb.serves[reqTpc]) + _ = mb.Unsubscribe(reqTpc) + }() + return mb.serves[reqTpc], nil +} + +func (mb *messageBus) handleToken(token mqtt.Token) error { + if mb.timeout > 0 { + token.WaitTimeout(mb.timeout) + } else { + token.Wait() + } + return token.Error() +} + +func (mb *messageBus) setClient(options *config.MessageBusOptions) error { + opts := mqtt.NewClientOptions() + opts.SetClientID("edge-device-sub-" + strconv.FormatInt(time.Now().UnixNano(), 10)) + opts.AddBroker(fmt.Sprintf("%s://%s:%d", options.Protocol, options.Host, options.Port)) + opts.SetConnectTimeout(time.Duration(options.ConnectTimoutMillisecond) * time.Millisecond) + opts.SetKeepAlive(time.Minute) + opts.SetAutoReconnect(true) + opts.SetOnConnectHandler(mb.onConnect) + opts.SetConnectionLostHandler(mb.onConnectLost) + opts.SetCleanSession(options.CleanSession) + + mb.client = mqtt.NewClient(opts) + return nil +} + +func (mb *messageBus) onConnect(mc mqtt.Client) { + reader := mc.OptionsReader() + mb.logger.Infof("the connection with %s has been established.", reader.Servers()[0].String()) + + for tpc, hdl := range mb.routes { + if err := mb.Subscribe(hdl, tpc); err != nil { + mb.logger.WithError(err).Errorf("fail to resubscribe the topic: %s", tpc) + } + } +} + +func (mb *messageBus) onConnectLost(mc mqtt.Client, err error) { + reader := mc.OptionsReader() + mb.logger.WithError(err).Errorf("the connection with %s has host, trying to reconnect.", + reader.Servers()[0].String()) +} diff --git a/internal/message_bus/data.go b/internal/message_bus/data.go new file mode 100644 index 0000000..df0aed2 --- /dev/null +++ b/internal/message_bus/data.go @@ -0,0 +1,51 @@ +package bus + +import "errors" + +type Data interface { + SetFields(fields map[string]interface{}) + GetFields() map[string]interface{} + SetField(key string, value interface{}) + GetField(key string) interface{} + + ToMessage() (*Message, error) + + Response() (response Data, err error) +} + +type data struct { + fields map[string]interface{} +} + +func (d *data) SetFields(fields map[string]interface{}) { + if d.fields == nil { + d.fields = make(map[string]interface{}) + } + + for key, value := range fields { + d.fields[key] = value + } +} + +func (d *data) GetFields() map[string]interface{} { + return d.fields +} + +func (d *data) SetField(key string, value interface{}) { + if d.fields == nil { + d.fields = make(map[string]interface{}) + } + d.fields[key] = value +} + +func (d *data) GetField(key string) interface{} { + return d.fields[key] +} + +func (d *data) ToMessage() (*Message, error) { + return nil, errors.New("implement me") +} + +func (d *data) Response() (response Data, err error) { + return nil, errors.New("implement me") +} diff --git a/internal/message_bus/data_device.go b/internal/message_bus/data_device.go new file mode 100644 index 0000000..4104cbf --- /dev/null +++ b/internal/message_bus/data_device.go @@ -0,0 +1,94 @@ +package bus + +import ( + "encoding/json" + "fmt" +) + +type ( + DeviceDataOperation = string // the type of device data's operation + DeviceDataReportMode = string // the mode of device data's reporting + + ProductFuncID = string // product functionality ID + ProductFuncType = string // product functionality type + //ProductPropertyID = ProductFuncID // product property's functionality ID + //ProductEventID = ProductFuncID // product event's functionality ID + //ProductMethodID = ProductFuncID // product method's functionality ID +) + +const ( + PropertyFunc ProductFuncType = "props" // product property's functionality + EventFunc ProductFuncType = "events" // product event's functionality + MethodFunc ProductFuncType = "methods" // product method's functionality + + DeviceDataOperationRead DeviceDataOperation = "read" // Device Property Read + DeviceDataOperationWrite DeviceDataOperation = "write" // Device Property Write + DeviceDataOperationEvent DeviceDataOperation = "event" // Device Event + DeviceDataOperationRequest DeviceDataOperation = "request" // Device Method Request + DeviceDataOperationResponse DeviceDataOperation = "response" // Device Method Response + DeviceDataOperationError DeviceDataOperation = "error" // Device Method Error +) + +// Opts2FuncType maps operation upon device data as product's functionality. +var opts2FuncType = map[DeviceDataOperation]ProductFuncType{ + DeviceDataOperationEvent: EventFunc, + DeviceDataOperationRead: PropertyFunc, + DeviceDataOperationWrite: PropertyFunc, + DeviceDataOperationRequest: MethodFunc, + DeviceDataOperationResponse: MethodFunc, + DeviceDataOperationError: MethodFunc, +} + +type DeviceData struct { + data + + ProductID string `json:"product_id"` + DeviceID string `json:"device_id"` + OptType DeviceDataOperation `json:"opt_type"` + FuncID ProductFuncID `json:"func_id"` + FuncType ProductFuncType `json:"func_type"` +} + +func (d *DeviceData) ToMessage() (*Message, error) { + topic := NewDeviceDataTopic(d.ProductID, d.DeviceID, d.OptType, d.FuncID) + payload, err := json.Marshal(d.fields) + if err != nil { + return nil, err + } + + return &Message{ + Topic: topic.String(), + Payload: payload, + }, nil +} + +func (d *DeviceData) isRequest() bool { + return d.OptType == DeviceDataOperationRequest +} + +func (d *DeviceData) Response() (response Data, err error) { + if !d.isRequest() { + return nil, fmt.Errorf("the device data is not a request: %+v", *d) + } + return NewDeviceData(d.ProductID, d.DeviceID, DeviceDataOperationResponse, d.FuncID), nil +} + +func NewDeviceData(productID, deviceID string, optType DeviceDataOperation, dataID ProductFuncID) *DeviceData { + return &DeviceData{ + ProductID: productID, + DeviceID: deviceID, + OptType: optType, + FuncID: dataID, + FuncType: opts2FuncType[optType], + } +} + +func ParseDeviceData(msg *Message) (*DeviceData, error) { + tags, fields, err := msg.Parse() + if err != nil { + return nil, err + } + deviceData := NewDeviceData(tags[0], tags[1], tags[2], tags[3]) + deviceData.SetFields(fields) + return deviceData, nil +} diff --git a/internal/message_bus/data_meta.go b/internal/message_bus/data_meta.go new file mode 100644 index 0000000..22806f2 --- /dev/null +++ b/internal/message_bus/data_meta.go @@ -0,0 +1,81 @@ +package bus + +import ( + "encoding/json" + "fmt" +) + +type ( + MetaDataType = string + + MetaDataOperation = string + MetaDataOperationMode = string +) + +const ( + MetaDataTypeProtocol MetaDataType = "protocol" + MetaDataTypeProduct MetaDataType = "product" + MetaDataTypeDevice MetaDataType = "device" + + MetaDataOperationCreate MetaDataOperation = "create" + MetaDataOperationUpdate MetaDataOperation = "update" + MetaDataOperationDelete MetaDataOperation = "delete" + MetaDataOperationGet MetaDataOperation = "get" + MetaDataOperationList MetaDataOperation = "list" + + MetaDataOperationModeRequest MetaDataOperationMode = "request" + MetaDataOperationModeResponse MetaDataOperationMode = "response" +) + +type MetaData struct { + data + + MetaType MetaDataType `json:"meta_type"` + OptType MetaDataOperation `json:"opt_type"` + OptMode MetaDataOperationMode `json:"opt_mode"` + DataID string `json:"data_id"` +} + +func (d *MetaData) ToMessage() (*Message, error) { + topic := NewMetaDataTopic(d.MetaType, d.OptType, d.OptMode, d.DataID) + payload, err := json.Marshal(d.fields) + if err != nil { + return nil, err + } + + return &Message{ + Topic: topic.String(), + Payload: payload, + }, nil +} + +func (d *MetaData) isRequest() bool { + return d.OptMode == MetaDataOperationModeRequest +} + +func (d *MetaData) Response() (Data, error) { + if !d.isRequest() { + return nil, fmt.Errorf("the device data is not a request: %+v", *d) + } + return NewMetaData(d.MetaType, d.OptType, MetaDataOperationModeResponse, d.DataID), nil +} + +func NewMetaData(metaType MetaDataType, methodType MetaDataOperation, + optMode MetaDataOperationMode, dataID string) *MetaData { + return &MetaData{ + MetaType: metaType, + OptType: methodType, + OptMode: optMode, + DataID: dataID, + } +} + +func ParseMetaData(msg *Message) (*MetaData, error) { + tags, fields, err := msg.Parse() + if err != nil { + return nil, err + } + metaData := NewMetaData(tags[0], tags[1], tags[2], tags[3]) + metaData.SetFields(fields) + return metaData, nil +} diff --git a/internal/message_bus/message.go b/internal/message_bus/message.go new file mode 100644 index 0000000..ad14418 --- /dev/null +++ b/internal/message_bus/message.go @@ -0,0 +1,29 @@ +package bus + +import ( + "encoding/json" +) + +type MessageHandler func(msg *Message) + +// Message is an intermediate data format between MQTT and Data. +type Message struct { + Topic string + Payload []byte +} + +func (m *Message) Parse() ([]string, map[string]interface{}, error) { + // parse topic + topic, err := NewTopic(m.Topic) + if err != nil { + return nil, nil, err + } + tagValues := topic.TagValues() + + // parse payload + fields := make(map[string]interface{}) + if err := json.Unmarshal(m.Payload, &fields); err != nil { + return nil, nil, err + } + return tagValues, fields, nil +} diff --git a/internal/message_bus/topic.go b/internal/message_bus/topic.go new file mode 100644 index 0000000..e0b78e9 --- /dev/null +++ b/internal/message_bus/topic.go @@ -0,0 +1,136 @@ +package bus + +import ( + "fmt" + "strings" +) + +type TopicTagKey string + +type TopicTags map[TopicTagKey]string + +type TopicType string + +// Topic returns the topic that could subscribe all messages belong to this type. +func (t TopicType) Topic() string { + return string(t) + TopicWildcard +} + +const ( + TopicTagKeyMetaDataType TopicTagKey = "meta_type" + TopicTagKeyMetaDataMethodType TopicTagKey = "method_type" + TopicTagKeyMetaDataMethodMode TopicTagKey = "method_mode" + TopicTagKeyProductID TopicTagKey = "product_id" + TopicTagKeyDeviceID TopicTagKey = "device_id" + TopicTagKeyOptType TopicTagKey = "opt_type" + TopicTagKeyDataID TopicTagKey = "data_id" + + TopicTypeMetaData TopicType = "META" + TopicTypeDeviceData TopicType = "DATA" + + TopicSep = "/" + TopicWildcard = "#" +) + +// Schemas describes all topics' forms. Every topic is formed by //.../. +var Schemas = map[TopicType][]TopicTagKey{ + TopicTypeMetaData: {TopicTagKeyMetaDataType, TopicTagKeyMetaDataMethodType, TopicTagKeyMetaDataMethodMode, TopicTagKeyDataID}, + TopicTypeDeviceData: {TopicTagKeyProductID, TopicTagKeyDeviceID, TopicTagKeyOptType, TopicTagKeyDataID}, +} + +type Topic interface { + Type() TopicType + + String() string + + Tags() TopicTags + TagKeys() []TopicTagKey + TagValues() []string + TagValue(key TopicTagKey) (value string, ok bool) +} + +type commonTopic struct { + topicTags TopicTags + topicType TopicType +} + +func (c *commonTopic) Type() TopicType { + return c.topicType +} + +func (c *commonTopic) String() string { + topicType := string(c.topicType) + tagValues := c.TagValues() + return strings.Join(append([]string{topicType}, tagValues...), TopicSep) +} + +func (c *commonTopic) Tags() TopicTags { + return c.topicTags +} + +func (c *commonTopic) TagKeys() []TopicTagKey { + return Schemas[c.topicType] +} + +func (c *commonTopic) TagValues() []string { + tagKeys := c.TagKeys() + values := make([]string, len(tagKeys)) + for idx, topicTagKey := range tagKeys { + values[idx] = c.topicTags[topicTagKey] + } + return values +} + +func (c *commonTopic) TagValue(key TopicTagKey) (value string, ok bool) { + tags := c.Tags() + value, ok = tags[key] + return +} + +func NewTopic(topic string) (Topic, error) { + parts := strings.Split(topic, TopicSep) + if len(parts) == 0 { + return nil, fmt.Errorf("invalid topic: %s", topic) + } + topicType := TopicType(parts[0]) + keys, ok := Schemas[topicType] + if !ok { + return nil, fmt.Errorf("undefined topic type: %s", topicType) + } + if len(parts)-1 != len(keys) { + return nil, fmt.Errorf("invalid topic: %s, keys [%+v] are necessary", topic, keys) + } + + tags := make(map[TopicTagKey]string) + for i, key := range keys { + tags[key] = parts[i+1] + } + return &commonTopic{ + topicType: topicType, + topicTags: tags, + }, nil +} + +func NewMetaDataTopic(metaType, methodType, methodMode, dataID string) Topic { + return &commonTopic{ + topicTags: map[TopicTagKey]string{ + TopicTagKeyMetaDataType: metaType, + TopicTagKeyMetaDataMethodType: methodType, + TopicTagKeyMetaDataMethodMode: methodMode, + TopicTagKeyDataID: dataID, + }, + topicType: TopicTypeMetaData, + } +} + +func NewDeviceDataTopic(productID, deviceID, optType, dataID string) Topic { + return &commonTopic{ + topicTags: map[TopicTagKey]string{ + TopicTagKeyProductID: productID, + TopicTagKeyDeviceID: deviceID, + TopicTagKeyOptType: optType, + TopicTagKeyDataID: dataID, + }, + topicType: TopicTypeDeviceData, + } +} diff --git a/pkg/models/product.go b/pkg/models/product.go index cbd6a53..b8f1ae7 100644 --- a/pkg/models/product.go +++ b/pkg/models/product.go @@ -1,5 +1,7 @@ package models + + type Product struct { ID string `json:"id"` // 产品 ID Name string `json:"name"` // 产品名称 From 1351223cbda092b4592a1be66c77d020297bc4e8 Mon Sep 17 00:00:00 2001 From: xufangyou Date: Wed, 17 Nov 2021 20:01:42 +0800 Subject: [PATCH 07/12] implement the meta operations framework and register the protocol --- config/config.go | 6 + go.mod | 1 + go.sum | 3 + internal/message_bus/bus.go | 47 +------- internal/message_bus/data.go | 36 +++--- internal/message_bus/data_meta.go | 4 +- internal/operations/client_manager.go | 89 ++++++++++++++ internal/operations/client_service.go | 91 +++++++++++++++ internal/operations/operation_device_data.go | 1 + internal/operations/operation_device_list.go | 17 +++ internal/operations/operation_message.go | 31 +++++ internal/operations/operation_product_list.go | 13 +++ .../operations/operation_protocol_register.go | 99 ++++++++++++++++ {internal/logger => logger}/logger.go | 0 pkg/models/data_converter.go | 35 ++++++ .../message_bus => pkg/models}/data_device.go | 41 ++++--- pkg/models/device_connector.go | 20 ++++ pkg/models/meta_loader.go | 104 +++++++++++++++++ pkg/models/product.go | 2 - pkg/models/protocol.go | 4 + pkg/startup/device_manager/device_manager.go | 90 ++++++++++++++ .../device_manager/handle_operations_meta.go | 36 ++++++ pkg/startup/device_manager/startup.go | 13 +++ pkg/startup/device_service/device_service.go | 110 ++++++++++++++++++ .../device_service/handle_operations_meta.go | 14 +++ pkg/startup/device_service/startup.go | 14 +++ pkg/version/version.go | 3 + 27 files changed, 845 insertions(+), 79 deletions(-) create mode 100644 config/config.go create mode 100644 internal/operations/client_manager.go create mode 100644 internal/operations/client_service.go create mode 100644 internal/operations/operation_device_data.go create mode 100644 internal/operations/operation_device_list.go create mode 100644 internal/operations/operation_message.go create mode 100644 internal/operations/operation_product_list.go create mode 100644 internal/operations/operation_protocol_register.go rename {internal/logger => logger}/logger.go (100%) create mode 100644 pkg/models/data_converter.go rename {internal/message_bus => pkg/models}/data_device.go (70%) create mode 100644 pkg/models/device_connector.go create mode 100644 pkg/models/meta_loader.go create mode 100644 pkg/startup/device_manager/device_manager.go create mode 100644 pkg/startup/device_manager/handle_operations_meta.go create mode 100644 pkg/startup/device_manager/startup.go create mode 100644 pkg/startup/device_service/device_service.go create mode 100644 pkg/startup/device_service/handle_operations_meta.go create mode 100644 pkg/startup/device_service/startup.go create mode 100644 pkg/version/version.go diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..1187175 --- /dev/null +++ b/config/config.go @@ -0,0 +1,6 @@ +package config + +const ( + ProductsPath = "./etc/resources/products" + DevicesPath = "./etc/resources/devices" +) diff --git a/go.mod b/go.mod index 1175ce2..0ae26f2 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.16 require ( github.com/eclipse/paho.mqtt.golang v1.3.5 github.com/sirupsen/logrus v1.8.1 + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 29ad16c..c7f001e 100644 --- a/go.sum +++ b/go.sum @@ -18,3 +18,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/message_bus/bus.go b/internal/message_bus/bus.go index 4f222b3..7e6c960 100644 --- a/internal/message_bus/bus.go +++ b/internal/message_bus/bus.go @@ -4,7 +4,7 @@ import ( "fmt" mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/thingio/edge-device-sdk/config" - "github.com/thingio/edge-device-sdk/internal/logger" + "github.com/thingio/edge-device-sdk/logger" "strconv" "sync" "time" @@ -15,7 +15,6 @@ func NewMessageBus(opts *config.MessageBusOptions, logger *logger.Logger) (Messa timeout: time.Millisecond * time.Duration(opts.TimeoutMillisecond), qos: opts.QoS, routes: make(map[string]MessageHandler), - serves: make(map[string]chan Data), mutex: sync.Mutex{}, logger: logger, } @@ -40,8 +39,6 @@ type MessageBus interface { Unsubscribe(topics ...string) error Call(request Data) (response Data, err error) - - Serve(request Data) (responses <-chan Data, err error) } type messageBus struct { @@ -50,7 +47,6 @@ type messageBus struct { qos int routes map[string]MessageHandler // topic -> handler - serves map[string]chan Data mutex sync.Mutex logger *logger.Logger @@ -161,39 +157,6 @@ func (mb *messageBus) Call(request Data) (response Data, err error) { } } -func (mb *messageBus) Serve(request Data) (<-chan Data, error) { - // subscribe request - reqMsg, err := request.ToMessage() - if err != nil { - return nil, err - } - reqTpc := reqMsg.Topic - - mb.serves[reqTpc] = make(chan Data, 1) - if err = mb.Subscribe(func(msg *Message) { - // publish response - response, err := request.Response() - if err != nil { - mb.logger.WithError(err).Errorf("fail to parse the request and construct the response") - return - } - _, fields, err := msg.Parse() - if err != nil { - mb.logger.WithError(err).Error("fail to parse the message") - return - } - response.SetFields(fields) - mb.serves[reqTpc] <- response - }, reqTpc); err != nil { - return nil, err - } - defer func() { - close(mb.serves[reqTpc]) - _ = mb.Unsubscribe(reqTpc) - }() - return mb.serves[reqTpc], nil -} - func (mb *messageBus) handleToken(token mqtt.Token) error { if mb.timeout > 0 { token.WaitTimeout(mb.timeout) @@ -205,7 +168,9 @@ func (mb *messageBus) handleToken(token mqtt.Token) error { func (mb *messageBus) setClient(options *config.MessageBusOptions) error { opts := mqtt.NewClientOptions() - opts.SetClientID("edge-device-sub-" + strconv.FormatInt(time.Now().UnixNano(), 10)) + clientID := "edge-device-sub-" + strconv.FormatInt(time.Now().UnixNano(), 10) + mb.logger.Infof("the ID of client for the message bus is %s", clientID) + opts.SetClientID(clientID) opts.AddBroker(fmt.Sprintf("%s://%s:%d", options.Protocol, options.Host, options.Port)) opts.SetConnectTimeout(time.Duration(options.ConnectTimoutMillisecond) * time.Millisecond) opts.SetKeepAlive(time.Minute) @@ -220,7 +185,7 @@ func (mb *messageBus) setClient(options *config.MessageBusOptions) error { func (mb *messageBus) onConnect(mc mqtt.Client) { reader := mc.OptionsReader() - mb.logger.Infof("the connection with %s has been established.", reader.Servers()[0].String()) + mb.logger.Infof("the connection with %s for the message bus has been established.", reader.Servers()[0].String()) for tpc, hdl := range mb.routes { if err := mb.Subscribe(hdl, tpc); err != nil { @@ -231,6 +196,6 @@ func (mb *messageBus) onConnect(mc mqtt.Client) { func (mb *messageBus) onConnectLost(mc mqtt.Client, err error) { reader := mc.OptionsReader() - mb.logger.WithError(err).Errorf("the connection with %s has host, trying to reconnect.", + mb.logger.WithError(err).Errorf("the connection with %s for the message bus has lost, trying to reconnect.", reader.Servers()[0].String()) } diff --git a/internal/message_bus/data.go b/internal/message_bus/data.go index df0aed2..b58d0f7 100644 --- a/internal/message_bus/data.go +++ b/internal/message_bus/data.go @@ -1,6 +1,8 @@ package bus -import "errors" +import ( + "errors" +) type Data interface { SetFields(fields map[string]interface{}) @@ -13,39 +15,39 @@ type Data interface { Response() (response Data, err error) } -type data struct { - fields map[string]interface{} +type MessageData struct { + Fields map[string]interface{} } -func (d *data) SetFields(fields map[string]interface{}) { - if d.fields == nil { - d.fields = make(map[string]interface{}) +func (d *MessageData) SetFields(fields map[string]interface{}) { + if d.Fields == nil { + d.Fields = make(map[string]interface{}) } for key, value := range fields { - d.fields[key] = value + d.Fields[key] = value } } -func (d *data) GetFields() map[string]interface{} { - return d.fields +func (d *MessageData) GetFields() map[string]interface{} { + return d.Fields } -func (d *data) SetField(key string, value interface{}) { - if d.fields == nil { - d.fields = make(map[string]interface{}) +func (d *MessageData) SetField(key string, value interface{}) { + if d.Fields == nil { + d.Fields = make(map[string]interface{}) } - d.fields[key] = value + d.Fields[key] = value } -func (d *data) GetField(key string) interface{} { - return d.fields[key] +func (d *MessageData) GetField(key string) interface{} { + return d.Fields[key] } -func (d *data) ToMessage() (*Message, error) { +func (d *MessageData) ToMessage() (*Message, error) { return nil, errors.New("implement me") } -func (d *data) Response() (response Data, err error) { +func (d *MessageData) Response() (response Data, err error) { return nil, errors.New("implement me") } diff --git a/internal/message_bus/data_meta.go b/internal/message_bus/data_meta.go index 22806f2..6685e52 100644 --- a/internal/message_bus/data_meta.go +++ b/internal/message_bus/data_meta.go @@ -28,7 +28,7 @@ const ( ) type MetaData struct { - data + MessageData MetaType MetaDataType `json:"meta_type"` OptType MetaDataOperation `json:"opt_type"` @@ -38,7 +38,7 @@ type MetaData struct { func (d *MetaData) ToMessage() (*Message, error) { topic := NewMetaDataTopic(d.MetaType, d.OptType, d.OptMode, d.DataID) - payload, err := json.Marshal(d.fields) + payload, err := json.Marshal(d.Fields) if err != nil { return nil, err } diff --git a/internal/operations/client_manager.go b/internal/operations/client_manager.go new file mode 100644 index 0000000..3b569b7 --- /dev/null +++ b/internal/operations/client_manager.go @@ -0,0 +1,89 @@ +package operations + +import ( + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/logger" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func NewDeviceManagerMetaOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceManagerMetaOperationClient, error) { + protocolClient, err := newDeviceManagerProtocolOperationClient(mb, logger) + if err != nil { + return nil, err + } + productClient, err := newDeviceManagerProductOperationClient(mb, logger) + if err != nil { + return nil, err + } + deviceClient, err := newDeviceManagerDeviceOperationClient(mb, logger) + if err != nil { + return nil, err + } + return &deviceManagerMetaOperationClient{ + protocolClient, + productClient, + deviceClient, + }, nil +} + +type DeviceManagerMetaOperationClient interface { + DeviceManagerProtocolOperationClient + DeviceManagerProductOperationClient + DeviceManagerDeviceOperationClient +} +type deviceManagerMetaOperationClient struct { + DeviceManagerProtocolOperationClient + DeviceManagerProductOperationClient + DeviceManagerDeviceOperationClient +} + +func newDeviceManagerProtocolOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceManagerProtocolOperationClient, error) { + return &deviceManagerProtocolOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceManagerProtocolOperationClient interface { + RegisterProtocols(registerProtocol func(protocol *models.Protocol) error) error +} +type deviceManagerProtocolOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} + +func newDeviceManagerProductOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceManagerProductOperationClient, error) { + return &deviceManagerProductOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceManagerProductOperationClient interface { + ListProducts(productID string) error +} +type deviceManagerProductOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} + +func newDeviceManagerDeviceOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceManagerDeviceOperationClient, error) { + return &deviceManagerDeviceOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceManagerDeviceOperationClient interface { + ListDevices(productID string, listDevices func(productID string) ([]*models.Device, error)) error +} +type deviceManagerDeviceOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} + +func NewDeviceManagerDeviceDataOperationClient(mb bus.MessageBus, logger *logger.Logger) (DeviceManagerDeviceDataOperationClient, error) { + return &deviceManagerDeviceDataOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceManagerDeviceDataOperationClient interface {} + +type deviceManagerDeviceDataOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} diff --git a/internal/operations/client_service.go b/internal/operations/client_service.go new file mode 100644 index 0000000..6ed96d5 --- /dev/null +++ b/internal/operations/client_service.go @@ -0,0 +1,91 @@ +package operations + +import ( + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/logger" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func NewDeviceServiceMetaOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceServiceMetaOperationClient, error) { + protocolClient, err := newDeviceServiceProtocolOperationClient(mb, logger) + if err != nil { + return nil, err + } + productClient, err := newDeviceServiceProductOperationClient(mb, logger) + if err != nil { + return nil, err + } + deviceClient, err := newDeviceServiceDeviceOperationClient(mb, logger) + if err != nil { + return nil, err + } + return &deviceServiceMetaOperationClient{ + protocolClient, + productClient, + deviceClient, + }, nil +} + +type DeviceServiceMetaOperationClient interface { + DeviceServiceProtocolOperationClient + DeviceServiceProductOperationClient + DeviceServiceDeviceOperationClient +} +type deviceServiceMetaOperationClient struct { + DeviceServiceProtocolOperationClient + DeviceServiceProductOperationClient + DeviceServiceDeviceOperationClient +} + +func newDeviceServiceProtocolOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceServiceProtocolOperationClient, error) { + return &deviceServiceProtocolOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceServiceProtocolOperationClient interface { + RegisterProtocol(protocol *models.Protocol) error +} +type deviceServiceProtocolOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} + +func newDeviceServiceProductOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceServiceProductOperationClient, error) { + return &deviceServiceProductOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceServiceProductOperationClient interface { + ListProducts(protocolID string) error +} +type deviceServiceProductOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} + +func newDeviceServiceDeviceOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceServiceDeviceOperationClient, error) { + return &deviceServiceDeviceOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceServiceDeviceOperationClient interface { + ListDevices(productID string) ([]*models.Device, error) +} +type deviceServiceDeviceOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} + +func NewDeviceServiceDeviceDataOperationClient(mb bus.MessageBus, + logger *logger.Logger) (DeviceServiceDeviceDataOperationClient, error) { + return &deviceServiceDeviceDataOperationClient{mb: mb, logger: logger}, nil +} + +type DeviceServiceDeviceDataOperationClient interface { +} + +type deviceServiceDeviceDataOperationClient struct { + mb bus.MessageBus + logger *logger.Logger +} diff --git a/internal/operations/operation_device_data.go b/internal/operations/operation_device_data.go new file mode 100644 index 0000000..19c01a9 --- /dev/null +++ b/internal/operations/operation_device_data.go @@ -0,0 +1 @@ +package operations diff --git a/internal/operations/operation_device_list.go b/internal/operations/operation_device_list.go new file mode 100644 index 0000000..d5ede84 --- /dev/null +++ b/internal/operations/operation_device_list.go @@ -0,0 +1,17 @@ +package operations + +import ( + "errors" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +// ListDevices for the device manager puts devices into the message bus. +func (c *deviceManagerDeviceOperationClient) ListDevices(productID string, + listDevices func(productID string) ([]*models.Device, error)) error { + return errors.New("implement me") +} + +// ListDevices for the device service takes devices from the message bus. +func (c *deviceServiceDeviceOperationClient) ListDevices(productID string) ([]*models.Device, error) { + return nil, errors.New("implement me") +} diff --git a/internal/operations/operation_message.go b/internal/operations/operation_message.go new file mode 100644 index 0000000..b4d7342 --- /dev/null +++ b/internal/operations/operation_message.go @@ -0,0 +1,31 @@ +package operations + +import "encoding/json" + +type OperationMessage interface { + Unmarshal(fields map[string]interface{}) error + Marshal() (map[string]interface{}, error) +} + +func Struct2Map(o interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(o) + if err != nil { + return nil, err + } + fields := make(map[string]interface{}) + if err = json.Unmarshal(data, &fields); err != nil { + return nil, err + } + return fields, nil +} + +func Map2Struct(m map[string]interface{}, s interface{}) error { + data, err := json.Marshal(m) + if err != nil { + return err + } + if err = json.Unmarshal(data, s); err != nil { + return err + } + return nil +} diff --git a/internal/operations/operation_product_list.go b/internal/operations/operation_product_list.go new file mode 100644 index 0000000..9cf27dd --- /dev/null +++ b/internal/operations/operation_product_list.go @@ -0,0 +1,13 @@ +package operations + +import "errors" + +// ListProducts for the device manager puts products into the message bus. +func (c *deviceManagerProductOperationClient) ListProducts(protocolID string) error { + return errors.New("implement me") +} + +// ListProducts for the device service takes products from the message bus. +func (c *deviceServiceProductOperationClient) ListProducts(protocolID string) error { + return errors.New("implement me") +} diff --git a/internal/operations/operation_protocol_register.go b/internal/operations/operation_protocol_register.go new file mode 100644 index 0000000..ca223de --- /dev/null +++ b/internal/operations/operation_protocol_register.go @@ -0,0 +1,99 @@ +package operations + +import ( + "errors" + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +type RegisterProtocolRequest struct { + Protocol models.Protocol `json:"protocol"` +} + +func (r *RegisterProtocolRequest) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *RegisterProtocolRequest) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + +type RegisterProtocolResponse struct { + Success bool `json:"success"` +} + +func (r *RegisterProtocolResponse) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *RegisterProtocolResponse) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + +// RegisterProtocols for the device manager takes the protocols from the message bus. +func (c *deviceManagerProtocolOperationClient) RegisterProtocols(registerProtocol func(protocol *models.Protocol) error) error { + schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, + bus.MetaDataOperationModeRequest, bus.TopicWildcard) + message, err := schema.ToMessage() + if err != nil { + return err + } + if err = c.mb.Subscribe(func(msg *bus.Message) { + // parse request from the message + _, fields, err := msg.Parse() + if err != nil { + c.logger.WithError(err).Error("fail to parse the message for registering protocol") + return + } + req := &RegisterProtocolRequest{} + if err := req.Unmarshal(fields); err != nil { + c.logger.WithError(err).Error("fail to unmarshal the request for registering protocol") + return + } + protocol := &req.Protocol + if err := registerProtocol(protocol); err != nil { + c.logger.Error(err.Error()) + return + } + + // publish response + response := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, + bus.MetaDataOperationModeResponse, protocol.ID) + rsp := &RegisterProtocolResponse{Success: true} + fields, err = rsp.Marshal() + if err != nil { + c.logger.WithError(err).Error("fail to marshal the response for registering protocol") + return + } + response.SetFields(fields) + if err := c.mb.Publish(response); err != nil { + c.logger.WithError(err).Error("fail to publish the response for registering protocol") + return + } + }, message.Topic); err != nil { + return err + } + return nil +} + +// RegisterProtocol for the device service puts the protocol into the message bus. +func (c *deviceServiceProtocolOperationClient) RegisterProtocol(protocol *models.Protocol) error { + request := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, + bus.MetaDataOperationModeRequest, protocol.ID) + fields, err := (&RegisterProtocolRequest{Protocol: *protocol}).Marshal() + if err != nil { + return err + } + request.SetFields(fields) + response, err := c.mb.Call(request) + if err != nil { + return err + } + + rsp := &RegisterProtocolResponse{} + if err := rsp.Unmarshal(response.GetFields()); err != nil { + return err + } + if !rsp.Success { + return errors.New("fail to register protocol") + } + return nil +} diff --git a/internal/logger/logger.go b/logger/logger.go similarity index 100% rename from internal/logger/logger.go rename to logger/logger.go diff --git a/pkg/models/data_converter.go b/pkg/models/data_converter.go new file mode 100644 index 0000000..06526b0 --- /dev/null +++ b/pkg/models/data_converter.go @@ -0,0 +1,35 @@ +package models + +import ( + "strconv" +) + +var StrConverters = map[PropertyType]StrConverter{ + PropertyTypeInt: Str2Int, + PropertyTypeUint: Str2Uint, + PropertyTypeFloat: Str2Float, + PropertyTypeBool: Str2Bool, + PropertyTypeString: Str2String, +} + +type StrConverter func(s string) (interface{}, error) + +func Str2Int(s string) (interface{}, error) { + return strconv.ParseInt(s, 10, 64) +} + +func Str2Uint(s string) (interface{}, error) { + return strconv.ParseUint(s, 10, 64) +} + +func Str2Float(s string) (interface{}, error) { + return strconv.ParseFloat(s, 64) +} + +func Str2Bool(s string) (interface{}, error) { + return strconv.ParseBool(s) +} + +func Str2String(s string) (interface{}, error) { + return s, nil +} diff --git a/internal/message_bus/data_device.go b/pkg/models/data_device.go similarity index 70% rename from internal/message_bus/data_device.go rename to pkg/models/data_device.go index 4104cbf..85734fc 100644 --- a/internal/message_bus/data_device.go +++ b/pkg/models/data_device.go @@ -1,32 +1,39 @@ -package bus +package models import ( "encoding/json" "fmt" + "github.com/thingio/edge-device-sdk/internal/message_bus" ) type ( DeviceDataOperation = string // the type of device data's operation DeviceDataReportMode = string // the mode of device data's reporting - ProductFuncID = string // product functionality ID - ProductFuncType = string // product functionality type - //ProductPropertyID = ProductFuncID // product property's functionality ID - //ProductEventID = ProductFuncID // product event's functionality ID - //ProductMethodID = ProductFuncID // product method's functionality ID + ProductFuncID = string // product functionality ID + ProductFuncType = string // product functionality type + ProductPropertyID = ProductFuncID // product property's functionality ID + ProductEventID = ProductFuncID // product event's functionality ID + ProductMethodID = ProductFuncID // product method's functionality ID ) const ( - PropertyFunc ProductFuncType = "props" // product property's functionality - EventFunc ProductFuncType = "events" // product event's functionality - MethodFunc ProductFuncType = "methods" // product method's functionality - DeviceDataOperationRead DeviceDataOperation = "read" // Device Property Read DeviceDataOperationWrite DeviceDataOperation = "write" // Device Property Write DeviceDataOperationEvent DeviceDataOperation = "event" // Device Event DeviceDataOperationRequest DeviceDataOperation = "request" // Device Method Request DeviceDataOperationResponse DeviceDataOperation = "response" // Device Method Response DeviceDataOperationError DeviceDataOperation = "error" // Device Method Error + + DeviceDataReportModeRegular DeviceDataReportMode = "regular" // report device data at regular intervals, e.g. 5s, 1m, 0.5h + DeviceDataReportModeChanged DeviceDataReportMode = "changed" // report device data while changed + + PropertyFunc ProductFuncType = "props" // product property's functionality + EventFunc ProductFuncType = "events" // product event's functionality + MethodFunc ProductFuncType = "methods" // product method's functionality + + DeviceDataMultiPropsID = "*" + DeviceDataMultiPropsName = "多属性" ) // Opts2FuncType maps operation upon device data as product's functionality. @@ -40,7 +47,7 @@ var opts2FuncType = map[DeviceDataOperation]ProductFuncType{ } type DeviceData struct { - data + bus.MessageData ProductID string `json:"product_id"` DeviceID string `json:"device_id"` @@ -49,14 +56,14 @@ type DeviceData struct { FuncType ProductFuncType `json:"func_type"` } -func (d *DeviceData) ToMessage() (*Message, error) { - topic := NewDeviceDataTopic(d.ProductID, d.DeviceID, d.OptType, d.FuncID) - payload, err := json.Marshal(d.fields) +func (d *DeviceData) ToMessage() (*bus.Message, error) { + topic := bus.NewDeviceDataTopic(d.ProductID, d.DeviceID, d.OptType, d.FuncID) + payload, err := json.Marshal(d.Fields) if err != nil { return nil, err } - return &Message{ + return &bus.Message{ Topic: topic.String(), Payload: payload, }, nil @@ -66,7 +73,7 @@ func (d *DeviceData) isRequest() bool { return d.OptType == DeviceDataOperationRequest } -func (d *DeviceData) Response() (response Data, err error) { +func (d *DeviceData) Response() (response bus.Data, err error) { if !d.isRequest() { return nil, fmt.Errorf("the device data is not a request: %+v", *d) } @@ -83,7 +90,7 @@ func NewDeviceData(productID, deviceID string, optType DeviceDataOperation, data } } -func ParseDeviceData(msg *Message) (*DeviceData, error) { +func ParseDeviceData(msg *bus.Message) (*DeviceData, error) { tags, fields, err := msg.Parse() if err != nil { return nil, err diff --git a/pkg/models/device_connector.go b/pkg/models/device_connector.go new file mode 100644 index 0000000..7f43b93 --- /dev/null +++ b/pkg/models/device_connector.go @@ -0,0 +1,20 @@ +package models + +import ( + "github.com/thingio/edge-device-sdk/logger" +) + +type DeviceConnector interface { + Initialize(lg *logger.Logger) error + + Start() error + Stop(force bool) error + Ping() bool + + Watch(bus chan<- DeviceData) error // property read periodically + Write(propertyID ProductPropertyID, data interface{}) error // property write + Subscribe(eventID ProductEventID, bus <-chan DeviceData) error // event subscribe + Call(methodID ProductMethodID, request DeviceData) (response DeviceData, err error) // method call +} + +type ConnectorBuilder func(product *Product, device *Device) (DeviceConnector, error) diff --git a/pkg/models/meta_loader.go b/pkg/models/meta_loader.go new file mode 100644 index 0000000..230fe96 --- /dev/null +++ b/pkg/models/meta_loader.go @@ -0,0 +1,104 @@ +package models + +import ( + "encoding/json" + "fmt" + "github.com/thingio/edge-device-sdk/config" + "gopkg.in/yaml.v2" + "io/fs" + "io/ioutil" + "path/filepath" +) + +func LoadProtocol(path string) (*Protocol, error) { + protocol := new(Protocol) + if err := load(path, protocol); err != nil { + return nil, err + } + return protocol, nil +} + +func LoadProducts(protocolID string) []*Product { + products := make([]*Product, 0) + _ = filepath.Walk(config.ProductsPath, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + var product *Product + product, err = loadProduct(path) + if err != nil { + return err + } + if product.Protocol != protocolID { + return nil + } + products = append(products, product) + return nil + }) + return products +} + +func LoadDevices(productID string) []*Device { + devices := make([]*Device, 0) + _ = filepath.Walk(config.DevicesPath, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + var device *Device + device, err = loadDevice(path) + if err != nil { + return err + } + if device.ProductID != productID { + return nil + } + devices = append(devices, device) + return nil + }) + return devices +} + +func loadProduct(path string) (*Product, error) { + product := new(Product) + if err := load(path, product); err != nil { + return nil, err + } + return product, nil +} + +func loadDevice(path string) (*Device, error) { + device := new(Device) + if err := load(path, device); err != nil { + return nil, err + } + return device, nil +} + +func load(path string, meta interface{}) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("fail to load the meta configurtion stored in %s, got %s", + path, err.Error()) + } + + var unmarshaller func([]byte, interface{}) error + switch ext := filepath.Ext(path); ext { + case ".json": + unmarshaller = json.Unmarshal + case ".yaml", ".yml": + unmarshaller = yaml.Unmarshal + default: + return fmt.Errorf("invalid meta configuration extension %s, only supporing json/yaml/yml", ext) + } + + if err := unmarshaller(data, meta); err != nil { + return fmt.Errorf("fail to unmarshal the device configuration, got %s", err.Error()) + } + return nil +} diff --git a/pkg/models/product.go b/pkg/models/product.go index b8f1ae7..cbd6a53 100644 --- a/pkg/models/product.go +++ b/pkg/models/product.go @@ -1,7 +1,5 @@ package models - - type Product struct { ID string `json:"id"` // 产品 ID Name string `json:"name"` // 产品名称 diff --git a/pkg/models/protocol.go b/pkg/models/protocol.go index ad2d544..551d083 100644 --- a/pkg/models/protocol.go +++ b/pkg/models/protocol.go @@ -1,5 +1,9 @@ package models +type ( + ProtocolPropertyKey = string // common property owned by devices with the same protocol +) + type Protocol struct { ID string `json:"id"` // 协议 ID Name string `json:"name"` // 协议名称 diff --git a/pkg/startup/device_manager/device_manager.go b/pkg/startup/device_manager/device_manager.go new file mode 100644 index 0000000..add01b5 --- /dev/null +++ b/pkg/startup/device_manager/device_manager.go @@ -0,0 +1,90 @@ +package startup + +import ( + "context" + "github.com/thingio/edge-device-sdk/config" + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/internal/operations" + "github.com/thingio/edge-device-sdk/logger" + "github.com/thingio/edge-device-sdk/pkg/version" + "os" + "sync" +) + +type DeviceManager struct { + // manager information + Version string + + // caches + protocols sync.Map + + // operation clients + moc operations.DeviceManagerMetaOperationClient // wrap the message bus to manipulate meta + doc operations.DeviceManagerDeviceDataOperationClient // warp the message bus to manipulate device data + + // lifetime control variables for the device service + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + logger *logger.Logger +} + +func (m *DeviceManager) Initialize(ctx context.Context, cancel context.CancelFunc) { + m.logger = logger.NewLogger() + + m.Version = version.Version + + m.protocols = sync.Map{} + + m.initializeOperationClients() + + m.ctx = ctx + m.cancel = cancel + m.wg = sync.WaitGroup{} +} + +func (m *DeviceManager) initializeOperationClients() { + // TODO Read from the configuration file + options := &config.MessageBusOptions{ + Host: "172.16.251.163", + Port: 1883, + Protocol: "tcp", + ConnectTimoutMillisecond: 30000, + TimeoutMillisecond: 1000, + QoS: 0, + CleanSession: false, + } + mb, err := bus.NewMessageBus(options, m.logger) + if err != nil { + m.logger.WithError(err).Error("fail to initialize the message bus") + os.Exit(1) + } + if err = mb.Connect(); err != nil { + m.logger.WithError(err).Error("fail to connect to the message bus") + os.Exit(1) + } + + moc, err := operations.NewDeviceManagerMetaOperationClient(mb, m.logger) + if err != nil { + m.logger.WithError(err).Error("fail to initialize the meta operation client for the device service") + os.Exit(1) + } + m.moc = moc + doc, err := operations.NewDeviceManagerDeviceDataOperationClient(mb, m.logger) + if err != nil { + m.logger.WithError(err).Error("fail to initialize the device data operation client for the device service") + os.Exit(1) + } + m.doc = doc +} + +func (m *DeviceManager) Serve() { + defer m.Stop(false) + + m.wg.Add(1) + go m.watchingProtocols() + + m.wg.Wait() +} + +func (m *DeviceManager) Stop(force bool) {} diff --git a/pkg/startup/device_manager/handle_operations_meta.go b/pkg/startup/device_manager/handle_operations_meta.go new file mode 100644 index 0000000..2d450ab --- /dev/null +++ b/pkg/startup/device_manager/handle_operations_meta.go @@ -0,0 +1,36 @@ +package startup + +import ( + "github.com/thingio/edge-device-sdk/pkg/models" + "time" +) + +func (m *DeviceManager) watchingProtocols() { + defer func() { + m.wg.Done() + }() + + if err := m.moc.RegisterProtocols(m.registerProtocol); err != nil { + m.logger.WithError(err).Error("fail to waiting for watching protocols") + return + } + + ticker := time.NewTicker(1 * time.Minute) + for { + select { + case <-ticker.C: // check the connection of protocol + m.protocols.Range(func(key, value interface{}) bool { + // TODO PING + return true + }) + case <-m.ctx.Done(): + break + } + } +} + +func (m *DeviceManager) registerProtocol(protocol *models.Protocol) error { + m.protocols.Store(protocol.ID, protocol) + m.logger.Infof("the protocol[%s] has registered successfully", protocol.ID) + return nil +} diff --git a/pkg/startup/device_manager/startup.go b/pkg/startup/device_manager/startup.go new file mode 100644 index 0000000..b356923 --- /dev/null +++ b/pkg/startup/device_manager/startup.go @@ -0,0 +1,13 @@ +package startup + +import ( + "context" +) + +func Startup() { + ctx, cancel := context.WithCancel(context.Background()) + + dm := &DeviceManager{} + dm.Initialize(ctx, cancel) + dm.Serve() +} diff --git a/pkg/startup/device_service/device_service.go b/pkg/startup/device_service/device_service.go new file mode 100644 index 0000000..ef7d52d --- /dev/null +++ b/pkg/startup/device_service/device_service.go @@ -0,0 +1,110 @@ +package startup + +import ( + "context" + "github.com/thingio/edge-device-sdk/config" + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/internal/operations" + "github.com/thingio/edge-device-sdk/logger" + "github.com/thingio/edge-device-sdk/pkg/models" + "github.com/thingio/edge-device-sdk/pkg/version" + "os" + "sync" +) + +type DeviceService struct { + // service information + ID string + Name string + Version string + + // driver + protocol *models.Protocol + connectorBuilder models.ConnectorBuilder + + // caches + products sync.Map + devices sync.Map + deviceConnectors sync.Map + + // operation clients + moc operations.DeviceServiceMetaOperationClient // wrap the message bus to manipulate + doc operations.DeviceServiceDeviceDataOperationClient // warp the message bus to manipulate device data + + // lifetime control variables for the device service + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + logger *logger.Logger +} + +func (s *DeviceService) Initialize(ctx context.Context, cancel context.CancelFunc, + protocol *models.Protocol, connectorBuilder models.ConnectorBuilder) { + s.logger = logger.NewLogger() + + s.ID = protocol.ID + s.Name = protocol.Name + s.Version = version.Version + + s.protocol = protocol + if connectorBuilder == nil { + s.logger.Error("please implement and specify the connector builder") + os.Exit(1) + } + s.connectorBuilder = connectorBuilder + + s.products = sync.Map{} + s.devices = sync.Map{} + s.deviceConnectors = sync.Map{} + + s.initializeOperationClients() + + s.ctx = ctx + s.cancel = cancel + s.wg = sync.WaitGroup{} +} + +func (s *DeviceService) initializeOperationClients() { + // TODO Read from the configuration file + options := &config.MessageBusOptions{ + Host: "172.16.251.163", + Port: 1883, + Protocol: "tcp", + ConnectTimoutMillisecond: 30000, + TimeoutMillisecond: 1000, + QoS: 0, + CleanSession: false, + } + mb, err := bus.NewMessageBus(options, s.logger) + if err != nil { + s.logger.WithError(err).Error("fail to initialize the message bus") + os.Exit(1) + } + if err = mb.Connect(); err != nil { + s.logger.WithError(err).Error("fail to connect to the message bus") + os.Exit(1) + } + + moc, err := operations.NewDeviceServiceMetaOperationClient(mb, s.logger) + if err != nil { + s.logger.WithError(err).Error("fail to initialize the meta operation client for the device service") + os.Exit(1) + } + s.moc = moc + doc, err := operations.NewDeviceServiceDeviceDataOperationClient(mb, s.logger) + if err != nil { + s.logger.WithError(err).Error("fail to initialize the device data operation client for the device service") + os.Exit(1) + } + s.doc = doc +} + +func (s *DeviceService) Serve() { + defer s.Stop(false) + + s.registerProtocol() + + s.wg.Wait() +} + +func (s *DeviceService) Stop(force bool) {} diff --git a/pkg/startup/device_service/handle_operations_meta.go b/pkg/startup/device_service/handle_operations_meta.go new file mode 100644 index 0000000..a873d1d --- /dev/null +++ b/pkg/startup/device_service/handle_operations_meta.go @@ -0,0 +1,14 @@ +package startup + +import ( + "os" +) + +func (s *DeviceService) registerProtocol() { + if err := s.moc.RegisterProtocol(s.protocol); err != nil { + s.logger.WithError(err).Errorf("fail to register the protocol[%s] "+ + "to the device manager", s.protocol.ID) + os.Exit(1) + } + s.logger.Infof("success to register the protocol[%s] to the device manager", s.protocol.ID) +} diff --git a/pkg/startup/device_service/startup.go b/pkg/startup/device_service/startup.go new file mode 100644 index 0000000..1a7e113 --- /dev/null +++ b/pkg/startup/device_service/startup.go @@ -0,0 +1,14 @@ +package startup + +import ( + "context" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func Startup(protocol *models.Protocol, builder models.ConnectorBuilder) { + ctx, cancel := context.WithCancel(context.Background()) + + ds := &DeviceService{} + ds.Initialize(ctx, cancel, protocol, builder) + ds.Serve() +} diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..d03976b --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,3 @@ +package version + +const Version = "1.0.0" From befd978f03d500aac03c16a482cd46a272c8693e Mon Sep 17 00:00:00 2001 From: xufangyou Date: Thu, 18 Nov 2021 11:00:55 +0800 Subject: [PATCH 08/12] activate devices --- internal/operations/client_manager.go | 9 +- internal/operations/client_service.go | 3 +- internal/operations/operation_device_list.go | 90 ++++++++++++++++- internal/operations/operation_product_list.go | 98 ++++++++++++++++++- .../operations/operation_protocol_register.go | 4 +- .../operation_protocol_unregister.go | 98 +++++++++++++++++++ pkg/models/meta_operator.go | 6 ++ pkg/startup/device_manager/device_manager.go | 18 +++- .../device_manager/handle_operations_meta.go | 38 ++++++- pkg/startup/device_manager/startup.go | 5 +- pkg/startup/device_service/device_service.go | 29 ++++++ .../device_service/handle_operations_meta.go | 94 ++++++++++++++++++ 12 files changed, 467 insertions(+), 25 deletions(-) create mode 100644 internal/operations/operation_protocol_unregister.go create mode 100644 pkg/models/meta_operator.go diff --git a/internal/operations/client_manager.go b/internal/operations/client_manager.go index 3b569b7..2186659 100644 --- a/internal/operations/client_manager.go +++ b/internal/operations/client_manager.go @@ -44,7 +44,8 @@ func newDeviceManagerProtocolOperationClient(mb bus.MessageBus, } type DeviceManagerProtocolOperationClient interface { - RegisterProtocols(registerProtocol func(protocol *models.Protocol) error) error + RegisterProtocols(register func(protocol *models.Protocol) error) error + UnregisterProtocols(unregister func(protocolID string) error) error } type deviceManagerProtocolOperationClient struct { mb bus.MessageBus @@ -57,7 +58,7 @@ func newDeviceManagerProductOperationClient(mb bus.MessageBus, } type DeviceManagerProductOperationClient interface { - ListProducts(productID string) error + ListProducts(list func(protocolID string) ([]*models.Product, error)) error } type deviceManagerProductOperationClient struct { mb bus.MessageBus @@ -70,7 +71,7 @@ func newDeviceManagerDeviceOperationClient(mb bus.MessageBus, } type DeviceManagerDeviceOperationClient interface { - ListDevices(productID string, listDevices func(productID string) ([]*models.Device, error)) error + ListDevices(list func(productID string) ([]*models.Device, error)) error } type deviceManagerDeviceOperationClient struct { mb bus.MessageBus @@ -81,7 +82,7 @@ func NewDeviceManagerDeviceDataOperationClient(mb bus.MessageBus, logger *logger return &deviceManagerDeviceDataOperationClient{mb: mb, logger: logger}, nil } -type DeviceManagerDeviceDataOperationClient interface {} +type DeviceManagerDeviceDataOperationClient interface{} type deviceManagerDeviceDataOperationClient struct { mb bus.MessageBus diff --git a/internal/operations/client_service.go b/internal/operations/client_service.go index 6ed96d5..9cb8d30 100644 --- a/internal/operations/client_service.go +++ b/internal/operations/client_service.go @@ -45,6 +45,7 @@ func newDeviceServiceProtocolOperationClient(mb bus.MessageBus, type DeviceServiceProtocolOperationClient interface { RegisterProtocol(protocol *models.Protocol) error + UnregisterProtocol(protocolID string) error } type deviceServiceProtocolOperationClient struct { mb bus.MessageBus @@ -57,7 +58,7 @@ func newDeviceServiceProductOperationClient(mb bus.MessageBus, } type DeviceServiceProductOperationClient interface { - ListProducts(protocolID string) error + ListProducts(protocolID string) ([]*models.Product, error) } type deviceServiceProductOperationClient struct { mb bus.MessageBus diff --git a/internal/operations/operation_device_list.go b/internal/operations/operation_device_list.go index d5ede84..37ef759 100644 --- a/internal/operations/operation_device_list.go +++ b/internal/operations/operation_device_list.go @@ -1,17 +1,97 @@ package operations import ( - "errors" + bus "github.com/thingio/edge-device-sdk/internal/message_bus" "github.com/thingio/edge-device-sdk/pkg/models" ) +type ListDevicesRequest struct { + ProductID string `json:"product_id"` +} + +func (r *ListDevicesRequest) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *ListDevicesRequest) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + +type ListDevicesResponse struct { + Devices []*models.Device `json:"devices"` +} + +func (r *ListDevicesResponse) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *ListDevicesResponse) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + // ListDevices for the device manager puts devices into the message bus. -func (c *deviceManagerDeviceOperationClient) ListDevices(productID string, - listDevices func(productID string) ([]*models.Device, error)) error { - return errors.New("implement me") +func (c *deviceManagerDeviceOperationClient) ListDevices(list func(productID string) ([]*models.Device, error)) error { + schema := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, + bus.MetaDataOperationModeRequest, bus.TopicWildcard) + message, err := schema.ToMessage() + if err != nil { + return err + } + if err = c.mb.Subscribe(func(msg *bus.Message) { + // parse request from the message + _, fields, err := msg.Parse() + if err != nil { + c.logger.WithError(err).Error("fail to parse the message for listing devices") + return + } + req := &ListDevicesRequest{} + if err := req.Unmarshal(fields); err != nil { + c.logger.WithError(err).Error("fail to unmarshal the request for listing devices") + return + } + + productID := req.ProductID + devices, err := list(productID) + if err != nil { + c.logger.WithError(err).Error("fail to list devices") + return + } + + // publish response + response := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, + bus.MetaDataOperationModeResponse, productID) + rsp := &ListDevicesResponse{Devices: devices} + fields, err = rsp.Marshal() + if err != nil { + c.logger.WithError(err).Error("fail to marshal the response for listing devices") + return + } + response.SetFields(fields) + if err := c.mb.Publish(response); err != nil { + c.logger.WithError(err).Error("fail to publish the response for listing devices") + return + } + }, message.Topic); err != nil { + return err + } + return nil } // ListDevices for the device service takes devices from the message bus. func (c *deviceServiceDeviceOperationClient) ListDevices(productID string) ([]*models.Device, error) { - return nil, errors.New("implement me") + request := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, + bus.MetaDataOperationModeRequest, productID) + fields, err := (&ListDevicesRequest{ProductID: productID}).Marshal() + if err != nil { + return nil, err + } + request.SetFields(fields) + response, err := c.mb.Call(request) + if err != nil { + return nil, err + } + + rsp := &ListDevicesResponse{} + if err := rsp.Unmarshal(response.GetFields()); err != nil { + return nil, err + } + return rsp.Devices, nil } diff --git a/internal/operations/operation_product_list.go b/internal/operations/operation_product_list.go index 9cf27dd..3a7aefd 100644 --- a/internal/operations/operation_product_list.go +++ b/internal/operations/operation_product_list.go @@ -1,13 +1,101 @@ package operations -import "errors" +import ( + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +type ListProductsRequest struct { + ProtocolID string `json:"protocol_id"` +} + +func (r *ListProductsRequest) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *ListProductsRequest) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + +type ListProductsResponse struct { + Products []*models.Product `json:"products"` +} + +func (r *ListProductsResponse) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *ListProductsResponse) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} // ListProducts for the device manager puts products into the message bus. -func (c *deviceManagerProductOperationClient) ListProducts(protocolID string) error { - return errors.New("implement me") +func (c *deviceManagerProductOperationClient) ListProducts(list func(protocolID string) ([]*models.Product, error)) error { + schema := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, + bus.MetaDataOperationModeRequest, bus.TopicWildcard) + message, err := schema.ToMessage() + if err != nil { + return err + } + if err = c.mb.Subscribe(func(msg *bus.Message) { + // parse request from the message + _, fields, err := msg.Parse() + if err != nil { + c.logger.WithError(err).Error("fail to parse the message for listing products") + return + } + req := &ListProductsRequest{} + if err := req.Unmarshal(fields); err != nil { + c.logger.WithError(err).Error("fail to unmarshal the request for listing products") + return + } + + protocolID := req.ProtocolID + products, err := list(protocolID) + if err != nil { + c.logger.WithError(err).Error("fail to list products") + return + } + + // publish response + response := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, + bus.MetaDataOperationModeResponse, protocolID) + if err != nil { + c.logger.WithError(err).Error("fail to construct response for the request") + return + } + rsp := &ListProductsResponse{Products: products} + fields, err = rsp.Marshal() + if err != nil { + c.logger.WithError(err).Error("fail to marshal the response for listing products") + return + } + response.SetFields(fields) + if err := c.mb.Publish(response); err != nil { + c.logger.WithError(err).Error("fail to publish the response for listing products") + return + } + }, message.Topic); err != nil { + return err + } + return nil } // ListProducts for the device service takes products from the message bus. -func (c *deviceServiceProductOperationClient) ListProducts(protocolID string) error { - return errors.New("implement me") +func (c *deviceServiceProductOperationClient) ListProducts(protocolID string) ([]*models.Product, error) { + request := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, + bus.MetaDataOperationModeRequest, protocolID) + fields, err := (&ListProductsRequest{ProtocolID: protocolID}).Marshal() + if err != nil { + return nil, err + } + request.SetFields(fields) + response, err := c.mb.Call(request) + if err != nil { + return nil, err + } + + rsp := &ListProductsResponse{} + if err := rsp.Unmarshal(response.GetFields()); err != nil { + return nil, err + } + return rsp.Products, nil } diff --git a/internal/operations/operation_protocol_register.go b/internal/operations/operation_protocol_register.go index ca223de..89c6778 100644 --- a/internal/operations/operation_protocol_register.go +++ b/internal/operations/operation_protocol_register.go @@ -29,7 +29,7 @@ func (r *RegisterProtocolResponse) Marshal() (map[string]interface{}, error) { } // RegisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) RegisterProtocols(registerProtocol func(protocol *models.Protocol) error) error { +func (c *deviceManagerProtocolOperationClient) RegisterProtocols(register func(protocol *models.Protocol) error) error { schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, bus.MetaDataOperationModeRequest, bus.TopicWildcard) message, err := schema.ToMessage() @@ -49,7 +49,7 @@ func (c *deviceManagerProtocolOperationClient) RegisterProtocols(registerProtoco return } protocol := &req.Protocol - if err := registerProtocol(protocol); err != nil { + if err := register(protocol); err != nil { c.logger.Error(err.Error()) return } diff --git a/internal/operations/operation_protocol_unregister.go b/internal/operations/operation_protocol_unregister.go new file mode 100644 index 0000000..a60f336 --- /dev/null +++ b/internal/operations/operation_protocol_unregister.go @@ -0,0 +1,98 @@ +package operations + +import ( + "errors" + bus "github.com/thingio/edge-device-sdk/internal/message_bus" +) + +type UnregisterProtocolRequest struct { + ProtocolID string `json:"protocol_id"` +} + +func (r *UnregisterProtocolRequest) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *UnregisterProtocolRequest) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + +type UnregisterProtocolResponse struct { + Success bool `json:"success"` +} + +func (r *UnregisterProtocolResponse) Unmarshal(fields map[string]interface{}) error { + return Map2Struct(fields, r) +} +func (r *UnregisterProtocolResponse) Marshal() (map[string]interface{}, error) { + return Struct2Map(*r) +} + +// UnregisterProtocols for the device manager takes the protocols from the message bus. +func (c *deviceManagerProtocolOperationClient) UnregisterProtocols(unregister func(protocolID string) error) error { + schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, + bus.MetaDataOperationModeRequest, bus.TopicWildcard) + message, err := schema.ToMessage() + if err != nil { + return err + } + if err = c.mb.Subscribe(func(msg *bus.Message) { + // parse request from the message + _, fields, err := msg.Parse() + if err != nil { + c.logger.WithError(err).Error("fail to parse the message for unregistering protocol") + return + } + req := &UnregisterProtocolRequest{} + if err := req.Unmarshal(fields); err != nil { + c.logger.WithError(err).Error("fail to unmarshal the request for unregistering protocol") + return + } + protocolID := req.ProtocolID + if err := unregister(protocolID); err != nil { + c.logger.Error(err.Error()) + return + } + + // publish response + response := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, + bus.MetaDataOperationModeResponse, protocolID) + rsp := &UnregisterProtocolResponse{Success: true} + fields, err = rsp.Marshal() + if err != nil { + c.logger.WithError(err).Error("fail to marshal the response for unregistering protocol") + return + } + response.SetFields(fields) + if err := c.mb.Publish(response); err != nil { + c.logger.WithError(err).Error("fail to publish the response for unregistering protocol") + return + } + }, message.Topic); err != nil { + return err + } + return nil +} + +// UnregisterProtocol for the device service puts the protocol into the message bus. +func (c *deviceServiceProtocolOperationClient) UnregisterProtocol(protocolID string) error { + request := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, + bus.MetaDataOperationModeRequest, protocolID) + fields, err := (&UnregisterProtocolRequest{ProtocolID: protocolID}).Marshal() + if err != nil { + return err + } + request.SetFields(fields) + response, err := c.mb.Call(request) + if err != nil { + return err + } + + rsp := &UnregisterProtocolResponse{} + if err := rsp.Unmarshal(response.GetFields()); err != nil { + return err + } + if !rsp.Success { + return errors.New("fail to unregister protocol") + } + return nil +} diff --git a/pkg/models/meta_operator.go b/pkg/models/meta_operator.go new file mode 100644 index 0000000..05cf2c5 --- /dev/null +++ b/pkg/models/meta_operator.go @@ -0,0 +1,6 @@ +package models + +type MetaStore interface { + ListProducts(protocolID string) ([]*Product, error) + ListDevices(productID string) ([]*Device, error) +} diff --git a/pkg/startup/device_manager/device_manager.go b/pkg/startup/device_manager/device_manager.go index add01b5..65de399 100644 --- a/pkg/startup/device_manager/device_manager.go +++ b/pkg/startup/device_manager/device_manager.go @@ -6,6 +6,7 @@ import ( bus "github.com/thingio/edge-device-sdk/internal/message_bus" "github.com/thingio/edge-device-sdk/internal/operations" "github.com/thingio/edge-device-sdk/logger" + "github.com/thingio/edge-device-sdk/pkg/models" "github.com/thingio/edge-device-sdk/pkg/version" "os" "sync" @@ -19,8 +20,9 @@ type DeviceManager struct { protocols sync.Map // operation clients - moc operations.DeviceManagerMetaOperationClient // wrap the message bus to manipulate meta - doc operations.DeviceManagerDeviceDataOperationClient // warp the message bus to manipulate device data + moc operations.DeviceManagerMetaOperationClient // wrap the message bus to manipulate meta + doc operations.DeviceManagerDeviceDataOperationClient // warp the message bus to manipulate device data + metaStore models.MetaStore // meta store // lifetime control variables for the device service ctx context.Context @@ -29,21 +31,21 @@ type DeviceManager struct { logger *logger.Logger } -func (m *DeviceManager) Initialize(ctx context.Context, cancel context.CancelFunc) { +func (m *DeviceManager) Initialize(ctx context.Context, cancel context.CancelFunc, metaStore models.MetaStore) { m.logger = logger.NewLogger() m.Version = version.Version m.protocols = sync.Map{} - m.initializeOperationClients() + m.initializeOperationClients(metaStore) m.ctx = ctx m.cancel = cancel m.wg = sync.WaitGroup{} } -func (m *DeviceManager) initializeOperationClients() { +func (m *DeviceManager) initializeOperationClients(metaStore models.MetaStore) { // TODO Read from the configuration file options := &config.MessageBusOptions{ Host: "172.16.251.163", @@ -76,6 +78,8 @@ func (m *DeviceManager) initializeOperationClients() { os.Exit(1) } m.doc = doc + + m.metaStore = metaStore } func (m *DeviceManager) Serve() { @@ -83,6 +87,10 @@ func (m *DeviceManager) Serve() { m.wg.Add(1) go m.watchingProtocols() + m.wg.Add(1) + go m.watchingProductOperations() + m.wg.Add(1) + go m.watchingDeviceOperations() m.wg.Wait() } diff --git a/pkg/startup/device_manager/handle_operations_meta.go b/pkg/startup/device_manager/handle_operations_meta.go index 2d450ab..c0cc6e2 100644 --- a/pkg/startup/device_manager/handle_operations_meta.go +++ b/pkg/startup/device_manager/handle_operations_meta.go @@ -11,7 +11,11 @@ func (m *DeviceManager) watchingProtocols() { }() if err := m.moc.RegisterProtocols(m.registerProtocol); err != nil { - m.logger.WithError(err).Error("fail to waiting for watching protocols") + m.logger.WithError(err).Error("fail to wait for registering protocols") + return + } + if err := m.moc.UnregisterProtocols(m.unregisterProtocol); err != nil { + m.logger.WithError(err).Error("fail to wait for unregistering protocols") return } @@ -34,3 +38,35 @@ func (m *DeviceManager) registerProtocol(protocol *models.Protocol) error { m.logger.Infof("the protocol[%s] has registered successfully", protocol.ID) return nil } + +func (m *DeviceManager) unregisterProtocol(protocolID string) error { + m.protocols.Delete(protocolID) + m.logger.Infof("the protocol[%s] has unregistered successfully", protocolID) + return nil +} + +func (m *DeviceManager) watchingProductOperations() { + defer func() { + m.wg.Done() + }() + + if err := m.moc.ListProducts(m.metaStore.ListProducts); err != nil { + m.logger.WithError(err).Error("fail to wait for listing products") + return + } else { + m.logger.Infof("start to watch the operations for product...") + } +} + +func (m *DeviceManager) watchingDeviceOperations() { + defer func() { + m.wg.Done() + }() + + if err := m.moc.ListDevices(m.metaStore.ListDevices); err != nil { + m.logger.WithError(err).Error("fail to wait for listing devices") + return + } else { + m.logger.Infof("start to watch the operations for device...") + } +} diff --git a/pkg/startup/device_manager/startup.go b/pkg/startup/device_manager/startup.go index b356923..d7b328b 100644 --- a/pkg/startup/device_manager/startup.go +++ b/pkg/startup/device_manager/startup.go @@ -2,12 +2,13 @@ package startup import ( "context" + "github.com/thingio/edge-device-sdk/pkg/models" ) -func Startup() { +func Startup(metaStore models.MetaStore) { ctx, cancel := context.WithCancel(context.Background()) dm := &DeviceManager{} - dm.Initialize(ctx, cancel) + dm.Initialize(ctx, cancel, metaStore) dm.Serve() } diff --git a/pkg/startup/device_service/device_service.go b/pkg/startup/device_service/device_service.go index ef7d52d..22777b7 100644 --- a/pkg/startup/device_service/device_service.go +++ b/pkg/startup/device_service/device_service.go @@ -2,6 +2,7 @@ package startup import ( "context" + "fmt" "github.com/thingio/edge-device-sdk/config" bus "github.com/thingio/edge-device-sdk/internal/message_bus" "github.com/thingio/edge-device-sdk/internal/operations" @@ -103,8 +104,36 @@ func (s *DeviceService) Serve() { defer s.Stop(false) s.registerProtocol() + defer s.unregisterProtocol() + + s.activateDevices() + defer s.deactivateDevices() s.wg.Wait() } func (s *DeviceService) Stop(force bool) {} + +func (s *DeviceService) getProduct(productID string) (*models.Product, error) { + v, ok := s.products.Load(productID) + if ok { + return v.(*models.Product), nil + } + return nil, fmt.Errorf("the product[%s] is not found in cache", productID) +} + +func (s *DeviceService) getDevice(deviceID string) (*models.Device, error) { + v, ok := s.devices.Load(deviceID) + if ok { + return v.(*models.Device), nil + } + return nil, fmt.Errorf("the device[%s] is not found in cache", deviceID) +} + +func (s *DeviceService) getDeviceConnector(deviceID string) (models.DeviceConnector, error) { + v, ok := s.deviceConnectors.Load(deviceID) + if ok { + return v.(models.DeviceConnector), nil + } + return nil, fmt.Errorf("the device[%s] is not activated", deviceID) +} diff --git a/pkg/startup/device_service/handle_operations_meta.go b/pkg/startup/device_service/handle_operations_meta.go index a873d1d..8a7cbe1 100644 --- a/pkg/startup/device_service/handle_operations_meta.go +++ b/pkg/startup/device_service/handle_operations_meta.go @@ -1,9 +1,11 @@ package startup import ( + "github.com/thingio/edge-device-sdk/pkg/models" "os" ) +// registerProtocol tries to register the protocol to the device manager. func (s *DeviceService) registerProtocol() { if err := s.moc.RegisterProtocol(s.protocol); err != nil { s.logger.WithError(err).Errorf("fail to register the protocol[%s] "+ @@ -12,3 +14,95 @@ func (s *DeviceService) registerProtocol() { } s.logger.Infof("success to register the protocol[%s] to the device manager", s.protocol.ID) } + +func (s *DeviceService) unregisterProtocol() { + if err := s.moc.UnregisterProtocol(s.protocol.ID); err != nil { + s.logger.WithError(err).Errorf("fail to unregister the protocol[%s] "+ + "from the device manager", s.protocol.ID) + } + s.logger.Infof("success to unregister the protocol[%s] from the device manager", s.protocol.ID) +} + +// activateDevices tries to activate all devices. +func (s *DeviceService) activateDevices() { + products, err := s.moc.ListProducts(s.protocol.ID) + if err != nil { + s.logger.WithError(err).Error("fail to fetch products from the device manager") + os.Exit(1) + } + for _, product := range products { + devices, err := s.moc.ListDevices(product.ID) + if err != nil { + s.logger.WithError(err).Error("fail to fetch devices from the device manager") + os.Exit(1) + } + + s.products.Store(product.ID, product) + for _, device := range devices { + if err := s.activateDevice(device); err != nil { + s.logger.WithError(err).Errorf("fail to activate the device[%s]", device.ID) + continue + } + s.devices.Store(device.ID, device) + } + } +} + +// activateDevice is responsible for establishing the connection with the real device. +func (s *DeviceService) activateDevice(device *models.Device) error { + if connector, _ := s.getDeviceConnector(device.ID); connector != nil { // the device has been activated + _ = s.deactivateDevice(device.ID) + } + + // build, initialize and start connector + product, err := s.getProduct(device.ProductID) + if err != nil { + return err + } + connector, err := s.connectorBuilder(product, device) + if err != nil { + return err + } + if err := connector.Initialize(s.logger); err != nil { + return err + } + if err := connector.Start(); err != nil { + return err + } + + // TODO What we need to pick up from the device? How do we pick up? + //for _, event := range product.Events { + // if err := connector.Subscribe(event.Id, s.buffer); err != nil { + // s.logger.WithError(err).Errorf("fail to subscribe the product event[%s]", event.Id) + // continue + // } + //} + + s.deviceConnectors.Store(device.ID, connector) + s.logger.Infof("success to activate the device[%s]", device.ID) + return nil +} + +func (s *DeviceService) deactivateDevices() { + s.deviceConnectors.Range(func(key, value interface{}) bool { + deviceID := key.(string) + if err := s.deactivateDevice(deviceID); err != nil { + s.logger.WithError(err).Errorf("fail to deactivate the device[%s]", deviceID) + } + return true + }) +} + +func (s *DeviceService) deactivateDevice(deviceID string) error { + connector, _ := s.getDeviceConnector(deviceID) + if connector == nil { + return nil + } + if err := connector.Stop(false); err != nil { + return err + } + + s.deviceConnectors.Delete(deviceID) + s.logger.Infof("success to deactivate the device[%s]", deviceID) + return nil +} From 5d316f4dc353d7a58b1a8b1aaaccef8dfad2a13b Mon Sep 17 00:00:00 2001 From: xufangyou Date: Thu, 18 Nov 2021 11:29:00 +0800 Subject: [PATCH 09/12] add version info in the topic --- .gitignore | 5 ++++- docs/zh/README.md | 6 ++++-- internal/message_bus/topic.go | 17 +++++++++++------ pkg/startup/device_manager/device_manager.go | 2 +- pkg/startup/device_service/device_service.go | 2 +- pkg/version/version.go | 3 --- version/version.go | 3 +++ 7 files changed, 24 insertions(+), 14 deletions(-) delete mode 100644 pkg/version/version.go create mode 100644 version/version.go diff --git a/.gitignore b/.gitignore index 69a17e0..ae25089 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,10 @@ *.out # Dependency directories (remove the comment below to include it) -vendor/ +# vendor/ + +# Dependency +go.sum # IDEA .idea \ No newline at end of file diff --git a/docs/zh/README.md b/docs/zh/README.md index 3dc07fd..9e25f79 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -55,8 +55,9 @@ ### 物模型 -对于物模型来说,Topic 格式为 `DATA/{ProductID}/{DeviceID}/{OptType}/{DataID}`: +对于物模型来说,Topic 格式为 `{Version}/DATA/{ProductID}/{DeviceID}/{OptType}/{DataID}`: +- `Version`:SDK 版本; - `ProductID`:设备元数据中产品的 UUID,产品唯一; - `DeviceID`:设备元数据中设备的 UUID,设备唯一; - `OptType`,产生当前数据的操作类型: @@ -69,8 +70,9 @@ ### 元数据操作 -对于元数据增删改查等操作来说,Topic 格式为 `META/{MetaType}/{MethodType}/{MethodMode}/{DataID}`: +对于元数据增删改查等操作来说,Topic 格式为 `{Version}/META/{MetaType}/{MethodType}/{MethodMode}/{DataID}`: +- `Version`:SDK 版本; - `MetaType`:元数据类型,可选 `protocol | product | device`; - `MethodType`:调用方法,可选 `create | update | delete | get | list`,对于不同的元数据类型,可选范围是不同的; - `MethodMode`:数据类型,可选 `request | response | error`; diff --git a/internal/message_bus/topic.go b/internal/message_bus/topic.go index e0b78e9..37dbe60 100644 --- a/internal/message_bus/topic.go +++ b/internal/message_bus/topic.go @@ -2,9 +2,14 @@ package bus import ( "fmt" + "github.com/thingio/edge-device-sdk/version" "strings" ) +const ( + TagsOffset = 2 +) + type TopicTagKey string type TopicTags map[TopicTagKey]string @@ -13,7 +18,7 @@ type TopicType string // Topic returns the topic that could subscribe all messages belong to this type. func (t TopicType) Topic() string { - return string(t) + TopicWildcard + return strings.Join([]string{version.Version, string(t)}, TopicSep) + TopicWildcard } const ( @@ -61,7 +66,7 @@ func (c *commonTopic) Type() TopicType { func (c *commonTopic) String() string { topicType := string(c.topicType) tagValues := c.TagValues() - return strings.Join(append([]string{topicType}, tagValues...), TopicSep) + return strings.Join(append([]string{version.Version, topicType}, tagValues...), TopicSep) } func (c *commonTopic) Tags() TopicTags { @@ -89,21 +94,21 @@ func (c *commonTopic) TagValue(key TopicTagKey) (value string, ok bool) { func NewTopic(topic string) (Topic, error) { parts := strings.Split(topic, TopicSep) - if len(parts) == 0 { + if len(parts) < TagsOffset { return nil, fmt.Errorf("invalid topic: %s", topic) } - topicType := TopicType(parts[0]) + topicType := TopicType(parts[1]) keys, ok := Schemas[topicType] if !ok { return nil, fmt.Errorf("undefined topic type: %s", topicType) } - if len(parts)-1 != len(keys) { + if len(parts)-TagsOffset != len(keys) { return nil, fmt.Errorf("invalid topic: %s, keys [%+v] are necessary", topic, keys) } tags := make(map[TopicTagKey]string) for i, key := range keys { - tags[key] = parts[i+1] + tags[key] = parts[i+TagsOffset] } return &commonTopic{ topicType: topicType, diff --git a/pkg/startup/device_manager/device_manager.go b/pkg/startup/device_manager/device_manager.go index 65de399..45c6ad1 100644 --- a/pkg/startup/device_manager/device_manager.go +++ b/pkg/startup/device_manager/device_manager.go @@ -7,7 +7,7 @@ import ( "github.com/thingio/edge-device-sdk/internal/operations" "github.com/thingio/edge-device-sdk/logger" "github.com/thingio/edge-device-sdk/pkg/models" - "github.com/thingio/edge-device-sdk/pkg/version" + "github.com/thingio/edge-device-sdk/version" "os" "sync" ) diff --git a/pkg/startup/device_service/device_service.go b/pkg/startup/device_service/device_service.go index 22777b7..4d440d1 100644 --- a/pkg/startup/device_service/device_service.go +++ b/pkg/startup/device_service/device_service.go @@ -8,7 +8,7 @@ import ( "github.com/thingio/edge-device-sdk/internal/operations" "github.com/thingio/edge-device-sdk/logger" "github.com/thingio/edge-device-sdk/pkg/models" - "github.com/thingio/edge-device-sdk/pkg/version" + "github.com/thingio/edge-device-sdk/version" "os" "sync" ) diff --git a/pkg/version/version.go b/pkg/version/version.go deleted file mode 100644 index d03976b..0000000 --- a/pkg/version/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package version - -const Version = "1.0.0" diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..57bfd1d --- /dev/null +++ b/version/version.go @@ -0,0 +1,3 @@ +package version + +const Version = "v1" From 5994c3d8e1701c4e0f6e2f68740cb3c0dbc8bd05 Mon Sep 17 00:00:00 2001 From: xufangyou Date: Fri, 19 Nov 2021 16:42:56 +0800 Subject: [PATCH 10/12] watching and publishing device data --- config/config.go | 138 +++++++++++++- config/message_bus_options.go | 10 ++ docs/zh/CHANGELOG.md | 6 + go.mod | 3 +- go.sum | 6 + internal/message_bus/bus.go | 16 +- internal/message_bus/data.go | 6 +- internal/message_bus/message.go | 5 + internal/message_bus/topic.go | 2 +- internal/operations/client_manager.go | 34 +++- internal/operations/client_service.go | 2 + internal/operations/device_data_publish.go | 7 + internal/operations/device_data_subscribe.go | 9 + internal/operations/device_event_receive.go | 34 ++++ internal/operations/device_method_call.go | 17 ++ internal/operations/device_property_read.go | 34 ++++ internal/operations/device_property_write.go | 21 +++ ...ion_device_list.go => meta_device_list.go} | 6 +- .../{operation_message.go => meta_message.go} | 2 +- ...n_product_list.go => meta_product_list.go} | 7 +- ..._register.go => meta_protocol_register.go} | 7 +- ...egister.go => meta_protocol_unregister.go} | 7 +- internal/operations/operation_device_data.go | 1 - pkg/models/data_device.go | 12 +- pkg/models/device_connector.go | 23 ++- pkg/startup/device_manager/device_manager.go | 12 +- .../device_manager/handle_operations_meta.go | 12 +- pkg/startup/device_service/device_service.go | 26 ++- .../handle_operations_device.go | 169 ++++++++++++++++++ .../device_service/handle_operations_meta.go | 25 ++- 30 files changed, 571 insertions(+), 88 deletions(-) create mode 100644 docs/zh/CHANGELOG.md create mode 100644 internal/operations/device_data_publish.go create mode 100644 internal/operations/device_data_subscribe.go create mode 100644 internal/operations/device_event_receive.go create mode 100644 internal/operations/device_method_call.go create mode 100644 internal/operations/device_property_read.go create mode 100644 internal/operations/device_property_write.go rename internal/operations/{operation_device_list.go => meta_device_list.go} (89%) rename internal/operations/{operation_message.go => meta_message.go} (94%) rename internal/operations/{operation_product_list.go => meta_product_list.go} (89%) rename internal/operations/{operation_protocol_register.go => meta_protocol_register.go} (89%) rename internal/operations/{operation_protocol_unregister.go => meta_protocol_unregister.go} (89%) delete mode 100644 internal/operations/operation_device_data.go create mode 100644 pkg/startup/device_service/handle_operations_device.go diff --git a/config/config.go b/config/config.go index 1187175..c191cc3 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,140 @@ package config +import ( + "fmt" + "github.com/jinzhu/configor" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +func init() { + if _, err := LoadConfiguration(); err != nil { + panic("fail to load configuration") + } +} + const ( - ProductsPath = "./etc/resources/products" - DevicesPath = "./etc/resources/devices" + ConfigurationPath = "./resources/configuration.yaml" // xxx/resource/configuration.yaml + ProtocolsPath = "./resources/protocols" // xxx-device-conn/cmd/resources/protocols + ProductsPath = "./resources/products" // edge-device-manager/resources/products + DevicesPath = "./resources/devices" // edge-device-manager/resources/devices + + EnvSep = "," ) + +var C *Configuration + +type Configuration struct { + MessageBus MessageBusOptions `json:"message_bus" yaml:"message_bus"` +} + +func LoadConfiguration() (*Configuration, error) { + C = new(Configuration) + + path := os.Getenv("CONFIG_FILE") + if path == "" { + path = ConfigurationPath + } + if err := configor.Load(C, path); err != nil { + return nil, fmt.Errorf("failed to load configuration file, got %s", err.Error()) + } + + if value := reflect.ValueOf(C).Elem().FieldByName("MessageBus"); value.IsValid() { + m := value.Interface().(MessageBusOptions) + LoadMessageBusOptions(&m) + value.Set(reflect.ValueOf(m)) + } + return C, nil +} + +func LoadEnv(target *string, env string) { + value := os.Getenv(env) + if value != "" { + *target = value + } +} + +// LoadEnvs will read the values of given environments, +// then overwrite the pointer if value is not empty. +func LoadEnvs(envs map[string]interface{}) error { + var err error + for env, target := range envs { + value := os.Getenv(env) + if value == "" { + continue + } + switch target.(type) { + case *string: + *(target.(*string)) = value + case *[]string: + values := strings.Split(value, EnvSep) + result := make([]string, 0) + for _, v := range values { + if v != "" { + result = append(result, v) + } + } + if len(result) != 0 { + *(target.(*[]string)) = result + } + case *int: + *(target.(*int)), err = strconv.Atoi(value) + case *bool: + *(target.(*bool)), err = strconv.ParseBool(value) + case *int64: + *(target.(*int64)), err = strconv.ParseInt(value, 10, 64) + case *float32: + if v, err := strconv.ParseFloat(value, 32); err == nil { + *(target.(*float32)) = float32(v) + } + case *float64: + *(target.(*float64)), err = strconv.ParseFloat(value, 64) + case *time.Duration: + *(target.(*time.Duration)), err = time.ParseDuration(value) + default: + return fmt.Errorf("unsupported env type : %T", target) + } + if err != nil { + return fmt.Errorf("fail to load environments, got %s", err.Error()) + } + } + return nil +} + +func LoadEnvList(target *[]string, env string) { + value := os.Getenv(env) + values := strings.Split(value, EnvSep) + result := make([]string, 0) + for _, v := range values { + if v != "" { + result = append(result, v) + } + } + if len(result) != 0 { + *target = result + } +} + +func LoadEnvBool(target *bool, env string) { + value := os.Getenv(env) + if value != "" { + *target, _ = strconv.ParseBool(value) + } +} + +func LoadEnvInt(target *int, env string) { + value := os.Getenv(env) + if value != "" { + *target, _ = strconv.Atoi(value) + } +} + +func LoadEnvInt64(target *int64, env string) { + value := os.Getenv(env) + if value != "" { + *target, _ = strconv.ParseInt(value, 10, 64) + } +} diff --git a/config/message_bus_options.go b/config/message_bus_options.go index 05bac99..2d49828 100644 --- a/config/message_bus_options.go +++ b/config/message_bus_options.go @@ -1,5 +1,15 @@ package config +func LoadMessageBusOptions(options *MessageBusOptions) { + LoadEnv(&options.Host, "MESSAGE_BUS_HOST") + LoadEnvInt(&options.Port, "MESSAGE_BUS_PORT") + LoadEnv(&options.Protocol, "MESSAGE_BUS_PROTOCOL") + LoadEnvInt(&options.ConnectTimoutMillisecond, "MESSAGE_BUS_CONNECT_TIMEOUT_MS") + LoadEnvInt(&options.TimeoutMillisecond, "MESSAGE_BUS_TIMEOUT_MS") + LoadEnvInt(&options.QoS, "MESSAGE_BUS_QOS") + LoadEnvBool(&options.CleanSession, "MESSAGE_BUS_CLEAN_SESSION") +} + type MessageBusOptions struct { // Host is the hostname or IP address of the MQTT broker. Host string `json:"host" yaml:"host"` diff --git a/docs/zh/CHANGELOG.md b/docs/zh/CHANGELOG.md new file mode 100644 index 0000000..a497591 --- /dev/null +++ b/docs/zh/CHANGELOG.md @@ -0,0 +1,6 @@ +# CHANGELOG + +## TODO + +1. 支持 MQTTS 配置 +2. 支持设备级别的 MQTT QoS 配置 \ No newline at end of file diff --git a/go.mod b/go.mod index 0ae26f2..d296633 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/eclipse/paho.mqtt.golang v1.3.5 + github.com/jinzhu/configor v1.2.1 github.com/sirupsen/logrus v1.8.1 - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index c7f001e..03a304e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= +github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= @@ -18,6 +22,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/message_bus/bus.go b/internal/message_bus/bus.go index 7e6c960..c091a44 100644 --- a/internal/message_bus/bus.go +++ b/internal/message_bus/bus.go @@ -6,7 +6,6 @@ import ( "github.com/thingio/edge-device-sdk/config" "github.com/thingio/edge-device-sdk/logger" "strconv" - "sync" "time" ) @@ -15,7 +14,6 @@ func NewMessageBus(opts *config.MessageBusOptions, logger *logger.Logger) (Messa timeout: time.Millisecond * time.Duration(opts.TimeoutMillisecond), qos: opts.QoS, routes: make(map[string]MessageHandler), - mutex: sync.Mutex{}, logger: logger, } if err := mb.setClient(opts); err != nil { @@ -30,7 +28,7 @@ type MessageBus interface { Connect() error - Disconnected() error + Disconnect() error Publish(data Data) error @@ -48,7 +46,6 @@ type messageBus struct { routes map[string]MessageHandler // topic -> handler - mutex sync.Mutex logger *logger.Logger } @@ -65,7 +62,7 @@ func (mb *messageBus) Connect() error { return mb.handleToken(token) } -func (mb *messageBus) Disconnected() error { +func (mb *messageBus) Disconnect() error { if mb.IsConnected() { mb.client.Disconnect(2000) // waiting 2s } @@ -83,9 +80,6 @@ func (mb *messageBus) Publish(data Data) error { } func (mb *messageBus) Subscribe(handler MessageHandler, topics ...string) error { - mb.mutex.Lock() - defer mb.mutex.Unlock() - filters := make(map[string]byte) for _, topic := range topics { mb.routes[topic] = handler @@ -103,9 +97,6 @@ func (mb *messageBus) Subscribe(handler MessageHandler, topics ...string) error } func (mb *messageBus) Unsubscribe(topics ...string) error { - mb.mutex.Lock() - defer mb.mutex.Unlock() - for _, topic := range topics { delete(mb.routes, topic) } @@ -147,7 +138,8 @@ func (mb *messageBus) Call(request Data) (response Data, err error) { case msg := <-ch: _, fields, err := msg.Parse() if err != nil { - return nil, fmt.Errorf("fail to parse message, got %s", err.Error()) + return nil, fmt.Errorf("fail to parse the message: %s, got %s", + msg.ToString(), err.Error()) } response.SetFields(fields) return response, nil diff --git a/internal/message_bus/data.go b/internal/message_bus/data.go index b58d0f7..3e9103e 100644 --- a/internal/message_bus/data.go +++ b/internal/message_bus/data.go @@ -20,12 +20,8 @@ type MessageData struct { } func (d *MessageData) SetFields(fields map[string]interface{}) { - if d.Fields == nil { - d.Fields = make(map[string]interface{}) - } - for key, value := range fields { - d.Fields[key] = value + d.SetField(key, value) } } diff --git a/internal/message_bus/message.go b/internal/message_bus/message.go index ad14418..110abe4 100644 --- a/internal/message_bus/message.go +++ b/internal/message_bus/message.go @@ -2,6 +2,7 @@ package bus import ( "encoding/json" + "fmt" ) type MessageHandler func(msg *Message) @@ -27,3 +28,7 @@ func (m *Message) Parse() ([]string, map[string]interface{}, error) { } return tagValues, fields, nil } + +func (m *Message) ToString() string { + return fmt.Sprintf("%+v", *m) +} diff --git a/internal/message_bus/topic.go b/internal/message_bus/topic.go index 37dbe60..c68ab9f 100644 --- a/internal/message_bus/topic.go +++ b/internal/message_bus/topic.go @@ -18,7 +18,7 @@ type TopicType string // Topic returns the topic that could subscribe all messages belong to this type. func (t TopicType) Topic() string { - return strings.Join([]string{version.Version, string(t)}, TopicSep) + TopicWildcard + return strings.Join([]string{version.Version, string(t), TopicWildcard}, TopicSep) } const ( diff --git a/internal/operations/client_manager.go b/internal/operations/client_manager.go index 2186659..b52b2e1 100644 --- a/internal/operations/client_manager.go +++ b/internal/operations/client_manager.go @@ -44,8 +44,8 @@ func newDeviceManagerProtocolOperationClient(mb bus.MessageBus, } type DeviceManagerProtocolOperationClient interface { - RegisterProtocols(register func(protocol *models.Protocol) error) error - UnregisterProtocols(unregister func(protocolID string) error) error + OnRegisterProtocols(register func(protocol *models.Protocol) error) error + OnUnregisterProtocols(unregister func(protocolID string) error) error } type deviceManagerProtocolOperationClient struct { mb bus.MessageBus @@ -58,7 +58,7 @@ func newDeviceManagerProductOperationClient(mb bus.MessageBus, } type DeviceManagerProductOperationClient interface { - ListProducts(list func(protocolID string) ([]*models.Product, error)) error + OnListProducts(list func(protocolID string) ([]*models.Product, error)) error } type deviceManagerProductOperationClient struct { mb bus.MessageBus @@ -71,7 +71,7 @@ func newDeviceManagerDeviceOperationClient(mb bus.MessageBus, } type DeviceManagerDeviceOperationClient interface { - ListDevices(list func(productID string) ([]*models.Device, error)) error + OnListDevices(list func(productID string) ([]*models.Device, error)) error } type deviceManagerDeviceOperationClient struct { mb bus.MessageBus @@ -79,12 +79,34 @@ type deviceManagerDeviceOperationClient struct { } func NewDeviceManagerDeviceDataOperationClient(mb bus.MessageBus, logger *logger.Logger) (DeviceManagerDeviceDataOperationClient, error) { - return &deviceManagerDeviceDataOperationClient{mb: mb, logger: logger}, nil + reads := make(map[models.ProductPropertyID]chan models.DeviceData) + events := make(map[models.ProductEventID]chan models.DeviceData) + return &deviceManagerDeviceDataOperationClient{ + mb: mb, + logger: logger, + reads: reads, + events: events, + }, nil } -type DeviceManagerDeviceDataOperationClient interface{} +type DeviceManagerDeviceDataOperationClient interface { + Read(productID, deviceID string, + propertyID models.ProductPropertyID) (dd <-chan models.DeviceData, cc func(), err error) + + Write(productID, deviceID string, + propertyID models.ProductPropertyID, value interface{}) error + + Receive(productID, deviceID string, + eventID models.ProductEventID) (dd <-chan models.DeviceData, cc func(), err error) + + Call(productID, deviceID string, methodID models.ProductMethodID, + req map[string]interface{}) (rsp map[string]interface{}, err error) +} type deviceManagerDeviceDataOperationClient struct { mb bus.MessageBus logger *logger.Logger + + reads map[string]chan models.DeviceData + events map[string]chan models.DeviceData } diff --git a/internal/operations/client_service.go b/internal/operations/client_service.go index 9cb8d30..1fc7a52 100644 --- a/internal/operations/client_service.go +++ b/internal/operations/client_service.go @@ -84,6 +84,8 @@ func NewDeviceServiceDeviceDataOperationClient(mb bus.MessageBus, } type DeviceServiceDeviceDataOperationClient interface { + Publish(data models.DeviceData) error + Subscribe(handler bus.MessageHandler, topic string) error } type deviceServiceDeviceDataOperationClient struct { diff --git a/internal/operations/device_data_publish.go b/internal/operations/device_data_publish.go new file mode 100644 index 0000000..42123c5 --- /dev/null +++ b/internal/operations/device_data_publish.go @@ -0,0 +1,7 @@ +package operations + +import "github.com/thingio/edge-device-sdk/pkg/models" + +func (c *deviceServiceDeviceDataOperationClient) Publish(data models.DeviceData) error { + return c.mb.Publish(&data) +} diff --git a/internal/operations/device_data_subscribe.go b/internal/operations/device_data_subscribe.go new file mode 100644 index 0000000..9ad13d1 --- /dev/null +++ b/internal/operations/device_data_subscribe.go @@ -0,0 +1,9 @@ +package operations + +import ( + bus "github.com/thingio/edge-device-sdk/internal/message_bus" +) + +func (c *deviceServiceDeviceDataOperationClient) Subscribe(handler bus.MessageHandler, topic string) error { + return c.mb.Subscribe(handler, topic) +} diff --git a/internal/operations/device_event_receive.go b/internal/operations/device_event_receive.go new file mode 100644 index 0000000..1df31b6 --- /dev/null +++ b/internal/operations/device_event_receive.go @@ -0,0 +1,34 @@ +package operations + +import ( + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func (c *deviceManagerDeviceDataOperationClient) Receive(productID, deviceID string, + eventID models.ProductEventID) (dd <-chan models.DeviceData, cc func(), err error) { + data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationEvent, eventID) + message, err := data.ToMessage() + if err != nil { + return nil, nil, err + } + topic := message.Topic + if _, ok := c.events[eventID]; !ok { + c.events[eventID] = make(chan models.DeviceData, 100) + } + if err = c.mb.Subscribe(func(msg *bus.Message) { + _, fields, _ := msg.Parse() + data.SetFields(fields) + + c.events[eventID] <- *data + }, topic); err != nil { + return nil, nil, err + } + + return c.events[eventID], func() { + if _, ok := c.events[topic]; ok { + close(c.events[topic]) + delete(c.events, topic) + } + }, nil +} diff --git a/internal/operations/device_method_call.go b/internal/operations/device_method_call.go new file mode 100644 index 0000000..f8698cf --- /dev/null +++ b/internal/operations/device_method_call.go @@ -0,0 +1,17 @@ +package operations + +import ( + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func (c *deviceManagerDeviceDataOperationClient) Call(productID, deviceID string, + methodID models.ProductMethodID, ins map[string]interface{}) (outs map[string]interface{}, err error) { + request := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationRequest, methodID) + request.SetFields(ins) + + response, err := c.mb.Call(request) + if err != nil { + return nil, err + } + return response.GetFields(), nil +} diff --git a/internal/operations/device_property_read.go b/internal/operations/device_property_read.go new file mode 100644 index 0000000..795b35a --- /dev/null +++ b/internal/operations/device_property_read.go @@ -0,0 +1,34 @@ +package operations + +import ( + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func (c *deviceManagerDeviceDataOperationClient) Read(productID, deviceID string, + propertyID models.ProductPropertyID) (dd <-chan models.DeviceData, cc func(), err error) { + data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationRead, propertyID) + message, err := data.ToMessage() + if err != nil { + return nil, nil, err + } + topic := message.Topic + if _, ok := c.reads[topic]; !ok { + c.reads[topic] = make(chan models.DeviceData, 100) + } + if err = c.mb.Subscribe(func(msg *bus.Message) { + _, fields, _ := msg.Parse() + data.SetFields(fields) + + c.reads[propertyID] <- *data + }, topic); err != nil { + return nil, nil, err + } + + return c.reads[propertyID], func() { + if _, ok := c.reads[topic]; ok { + close(c.reads[topic]) + delete(c.reads, topic) + } + }, nil +} diff --git a/internal/operations/device_property_write.go b/internal/operations/device_property_write.go new file mode 100644 index 0000000..5c65fdc --- /dev/null +++ b/internal/operations/device_property_write.go @@ -0,0 +1,21 @@ +package operations + +import ( + "fmt" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +func (c *deviceManagerDeviceDataOperationClient) Write(productID, deviceID string, + propertyID models.ProductPropertyID, value interface{}) error { + data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationWrite, propertyID) + if propertyID == models.DeviceDataMultiPropsID { + fields, ok := value.(map[models.ProductPropertyID]interface{}) + if !ok { + return fmt.Errorf("%+v must be type map[models.ProductPropertyID]interface{}", value) + } + data.SetFields(fields) + } else { + data.SetField(propertyID, value) + } + return c.mb.Publish(data) +} diff --git a/internal/operations/operation_device_list.go b/internal/operations/meta_device_list.go similarity index 89% rename from internal/operations/operation_device_list.go rename to internal/operations/meta_device_list.go index 37ef759..45ea166 100644 --- a/internal/operations/operation_device_list.go +++ b/internal/operations/meta_device_list.go @@ -27,8 +27,8 @@ func (r *ListDevicesResponse) Marshal() (map[string]interface{}, error) { return Struct2Map(*r) } -// ListDevices for the device manager puts devices into the message bus. -func (c *deviceManagerDeviceOperationClient) ListDevices(list func(productID string) ([]*models.Device, error)) error { +// OnListDevices for the device manager puts devices into the message bus. +func (c *deviceManagerDeviceOperationClient) OnListDevices(list func(productID string) ([]*models.Device, error)) error { schema := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, bus.MetaDataOperationModeRequest, bus.TopicWildcard) message, err := schema.ToMessage() @@ -39,7 +39,7 @@ func (c *deviceManagerDeviceOperationClient) ListDevices(list func(productID str // parse request from the message _, fields, err := msg.Parse() if err != nil { - c.logger.WithError(err).Error("fail to parse the message for listing devices") + c.logger.WithError(err).Errorf("fail to parse the message[%s] for listing devices", msg.ToString()) return } req := &ListDevicesRequest{} diff --git a/internal/operations/operation_message.go b/internal/operations/meta_message.go similarity index 94% rename from internal/operations/operation_message.go rename to internal/operations/meta_message.go index b4d7342..7edfa5b 100644 --- a/internal/operations/operation_message.go +++ b/internal/operations/meta_message.go @@ -2,7 +2,7 @@ package operations import "encoding/json" -type OperationMessage interface { +type MetaMessage interface { Unmarshal(fields map[string]interface{}) error Marshal() (map[string]interface{}, error) } diff --git a/internal/operations/operation_product_list.go b/internal/operations/meta_product_list.go similarity index 89% rename from internal/operations/operation_product_list.go rename to internal/operations/meta_product_list.go index 3a7aefd..3261897 100644 --- a/internal/operations/operation_product_list.go +++ b/internal/operations/meta_product_list.go @@ -27,8 +27,8 @@ func (r *ListProductsResponse) Marshal() (map[string]interface{}, error) { return Struct2Map(*r) } -// ListProducts for the device manager puts products into the message bus. -func (c *deviceManagerProductOperationClient) ListProducts(list func(protocolID string) ([]*models.Product, error)) error { +// OnListProducts for the device manager puts products into the message bus. +func (c *deviceManagerProductOperationClient) OnListProducts(list func(protocolID string) ([]*models.Product, error)) error { schema := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, bus.MetaDataOperationModeRequest, bus.TopicWildcard) message, err := schema.ToMessage() @@ -39,7 +39,8 @@ func (c *deviceManagerProductOperationClient) ListProducts(list func(protocolID // parse request from the message _, fields, err := msg.Parse() if err != nil { - c.logger.WithError(err).Error("fail to parse the message for listing products") + c.logger.WithError(err).Errorf("fail to parse the message[%s] for listing products", + msg.ToString()) return } req := &ListProductsRequest{} diff --git a/internal/operations/operation_protocol_register.go b/internal/operations/meta_protocol_register.go similarity index 89% rename from internal/operations/operation_protocol_register.go rename to internal/operations/meta_protocol_register.go index 89c6778..0e49112 100644 --- a/internal/operations/operation_protocol_register.go +++ b/internal/operations/meta_protocol_register.go @@ -28,8 +28,8 @@ func (r *RegisterProtocolResponse) Marshal() (map[string]interface{}, error) { return Struct2Map(*r) } -// RegisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) RegisterProtocols(register func(protocol *models.Protocol) error) error { +// OnRegisterProtocols for the device manager takes the protocols from the message bus. +func (c *deviceManagerProtocolOperationClient) OnRegisterProtocols(register func(protocol *models.Protocol) error) error { schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, bus.MetaDataOperationModeRequest, bus.TopicWildcard) message, err := schema.ToMessage() @@ -40,7 +40,8 @@ func (c *deviceManagerProtocolOperationClient) RegisterProtocols(register func(p // parse request from the message _, fields, err := msg.Parse() if err != nil { - c.logger.WithError(err).Error("fail to parse the message for registering protocol") + c.logger.WithError(err).Errorf("fail to parse the message[%s] for registering protocol", + msg.ToString()) return } req := &RegisterProtocolRequest{} diff --git a/internal/operations/operation_protocol_unregister.go b/internal/operations/meta_protocol_unregister.go similarity index 89% rename from internal/operations/operation_protocol_unregister.go rename to internal/operations/meta_protocol_unregister.go index a60f336..54f48cf 100644 --- a/internal/operations/operation_protocol_unregister.go +++ b/internal/operations/meta_protocol_unregister.go @@ -27,8 +27,8 @@ func (r *UnregisterProtocolResponse) Marshal() (map[string]interface{}, error) { return Struct2Map(*r) } -// UnregisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) UnregisterProtocols(unregister func(protocolID string) error) error { +// OnUnregisterProtocols for the device manager takes the protocols from the message bus. +func (c *deviceManagerProtocolOperationClient) OnUnregisterProtocols(unregister func(protocolID string) error) error { schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, bus.MetaDataOperationModeRequest, bus.TopicWildcard) message, err := schema.ToMessage() @@ -39,7 +39,8 @@ func (c *deviceManagerProtocolOperationClient) UnregisterProtocols(unregister fu // parse request from the message _, fields, err := msg.Parse() if err != nil { - c.logger.WithError(err).Error("fail to parse the message for unregistering protocol") + c.logger.WithError(err).Errorf("fail to parse the message[%s] for unregistering protocol", + msg.ToString()) return } req := &UnregisterProtocolRequest{} diff --git a/internal/operations/operation_device_data.go b/internal/operations/operation_device_data.go deleted file mode 100644 index 19c01a9..0000000 --- a/internal/operations/operation_device_data.go +++ /dev/null @@ -1 +0,0 @@ -package operations diff --git a/pkg/models/data_device.go b/pkg/models/data_device.go index 85734fc..5e777af 100644 --- a/pkg/models/data_device.go +++ b/pkg/models/data_device.go @@ -7,8 +7,8 @@ import ( ) type ( - DeviceDataOperation = string // the type of device data's operation - DeviceDataReportMode = string // the mode of device data's reporting + DeviceDataOperation = string // the type of device data's operation + DeviceDataPropertyReportMode = string // the mode of device data's reporting ProductFuncID = string // product functionality ID ProductFuncType = string // product functionality type @@ -25,15 +25,15 @@ const ( DeviceDataOperationResponse DeviceDataOperation = "response" // Device Method Response DeviceDataOperationError DeviceDataOperation = "error" // Device Method Error - DeviceDataReportModeRegular DeviceDataReportMode = "regular" // report device data at regular intervals, e.g. 5s, 1m, 0.5h - DeviceDataReportModeChanged DeviceDataReportMode = "changed" // report device data while changed + DeviceDataReportModePeriodical DeviceDataPropertyReportMode = "periodical" // report device data at intervals, e.g. 5s, 1m, 0.5h + DeviceDataReportModeMutated DeviceDataPropertyReportMode = "mutated" // report device data while mutated PropertyFunc ProductFuncType = "props" // product property's functionality EventFunc ProductFuncType = "events" // product event's functionality MethodFunc ProductFuncType = "methods" // product method's functionality - DeviceDataMultiPropsID = "*" - DeviceDataMultiPropsName = "多属性" + DeviceDataMultiPropsID ProductFuncID = "*" + DeviceDataMultiPropsName = "多属性" ) // Opts2FuncType maps operation upon device data as product's functionality. diff --git a/pkg/models/device_connector.go b/pkg/models/device_connector.go index 7f43b93..af3a31a 100644 --- a/pkg/models/device_connector.go +++ b/pkg/models/device_connector.go @@ -5,16 +5,33 @@ import ( ) type DeviceConnector interface { + // Initialize will try to initialize a device connector to + // create the connection with device which needs to activate. + // It must always return nil if the device needn't be initialized. Initialize(lg *logger.Logger) error + // Start will to try to create connection with the real device. + // It must always return nil if the device needn't be initialized. Start() error + // Stop will to try to destroy connection with the real device. + // It must always return nil if the device needn't be initialized. Stop(force bool) error + // Ping is used to test the connectivity of the real device. + // If the device is connected, it will return true, else return false. Ping() bool - Watch(bus chan<- DeviceData) error // property read periodically - Write(propertyID ProductPropertyID, data interface{}) error // property write - Subscribe(eventID ProductEventID, bus <-chan DeviceData) error // event subscribe + // Watch will read device's properties periodically with the specified policy. + Watch(bus chan<- DeviceData) error + // Write will write the specified property to the real device. + Write(propertyID ProductPropertyID, data interface{}) error + // Subscribe will subscribe the specified event, + // and put data belonging to this event into the bus. + Subscribe(eventID ProductEventID, bus chan<- DeviceData) error + // Call is used to call the specified method defined in product, + // then waiting for a while to receive its response. + // If the call is timeout, it will return a timeout error. Call(methodID ProductMethodID, request DeviceData) (response DeviceData, err error) // method call } +// ConnectorBuilder is used to create a new device connector using the specified product and device. type ConnectorBuilder func(product *Product, device *Device) (DeviceConnector, error) diff --git a/pkg/startup/device_manager/device_manager.go b/pkg/startup/device_manager/device_manager.go index 45c6ad1..f16bfc6 100644 --- a/pkg/startup/device_manager/device_manager.go +++ b/pkg/startup/device_manager/device_manager.go @@ -46,17 +46,7 @@ func (m *DeviceManager) Initialize(ctx context.Context, cancel context.CancelFun } func (m *DeviceManager) initializeOperationClients(metaStore models.MetaStore) { - // TODO Read from the configuration file - options := &config.MessageBusOptions{ - Host: "172.16.251.163", - Port: 1883, - Protocol: "tcp", - ConnectTimoutMillisecond: 30000, - TimeoutMillisecond: 1000, - QoS: 0, - CleanSession: false, - } - mb, err := bus.NewMessageBus(options, m.logger) + mb, err := bus.NewMessageBus(&config.C.MessageBus, m.logger) if err != nil { m.logger.WithError(err).Error("fail to initialize the message bus") os.Exit(1) diff --git a/pkg/startup/device_manager/handle_operations_meta.go b/pkg/startup/device_manager/handle_operations_meta.go index c0cc6e2..7bd043f 100644 --- a/pkg/startup/device_manager/handle_operations_meta.go +++ b/pkg/startup/device_manager/handle_operations_meta.go @@ -10,11 +10,11 @@ func (m *DeviceManager) watchingProtocols() { m.wg.Done() }() - if err := m.moc.RegisterProtocols(m.registerProtocol); err != nil { + if err := m.moc.OnRegisterProtocols(m.registerProtocol); err != nil { m.logger.WithError(err).Error("fail to wait for registering protocols") return } - if err := m.moc.UnregisterProtocols(m.unregisterProtocol); err != nil { + if err := m.moc.OnUnregisterProtocols(m.unregisterProtocol); err != nil { m.logger.WithError(err).Error("fail to wait for unregistering protocols") return } @@ -50,11 +50,11 @@ func (m *DeviceManager) watchingProductOperations() { m.wg.Done() }() - if err := m.moc.ListProducts(m.metaStore.ListProducts); err != nil { + if err := m.moc.OnListProducts(m.metaStore.ListProducts); err != nil { m.logger.WithError(err).Error("fail to wait for listing products") return } else { - m.logger.Infof("start to watch the operations for product...") + m.logger.Infof("start to watch the changes for product...") } } @@ -63,10 +63,10 @@ func (m *DeviceManager) watchingDeviceOperations() { m.wg.Done() }() - if err := m.moc.ListDevices(m.metaStore.ListDevices); err != nil { + if err := m.moc.OnListDevices(m.metaStore.ListDevices); err != nil { m.logger.WithError(err).Error("fail to wait for listing devices") return } else { - m.logger.Infof("start to watch the operations for device...") + m.logger.Infof("start to watch the changes for device...") } } diff --git a/pkg/startup/device_service/device_service.go b/pkg/startup/device_service/device_service.go index 4d440d1..1d22c83 100644 --- a/pkg/startup/device_service/device_service.go +++ b/pkg/startup/device_service/device_service.go @@ -24,11 +24,14 @@ type DeviceService struct { connectorBuilder models.ConnectorBuilder // caches - products sync.Map - devices sync.Map - deviceConnectors sync.Map + products sync.Map + devices sync.Map + deviceConnectors sync.Map + unsupportedDeviceDataTypes map[models.DeviceDataOperation]struct{} + deviceDataHandlers map[models.DeviceDataOperation]DeviceDataHandler // operation clients + bus chan models.DeviceData moc operations.DeviceServiceMetaOperationClient // wrap the message bus to manipulate doc operations.DeviceServiceDeviceDataOperationClient // warp the message bus to manipulate device data @@ -66,17 +69,9 @@ func (s *DeviceService) Initialize(ctx context.Context, cancel context.CancelFun } func (s *DeviceService) initializeOperationClients() { - // TODO Read from the configuration file - options := &config.MessageBusOptions{ - Host: "172.16.251.163", - Port: 1883, - Protocol: "tcp", - ConnectTimoutMillisecond: 30000, - TimeoutMillisecond: 1000, - QoS: 0, - CleanSession: false, - } - mb, err := bus.NewMessageBus(options, s.logger) + s.bus = make(chan models.DeviceData, 100) + + mb, err := bus.NewMessageBus(&config.C.MessageBus, s.logger) if err != nil { s.logger.WithError(err).Error("fail to initialize the message bus") os.Exit(1) @@ -109,6 +104,9 @@ func (s *DeviceService) Serve() { s.activateDevices() defer s.deactivateDevices() + s.publishingDeviceData() + s.handlingDeviceData() + s.wg.Wait() } diff --git a/pkg/startup/device_service/handle_operations_device.go b/pkg/startup/device_service/handle_operations_device.go new file mode 100644 index 0000000..6915748 --- /dev/null +++ b/pkg/startup/device_service/handle_operations_device.go @@ -0,0 +1,169 @@ +package startup + +import ( + "fmt" + bus "github.com/thingio/edge-device-sdk/internal/message_bus" + "github.com/thingio/edge-device-sdk/pkg/models" +) + +// publishingDeviceData tries to publish data in the bus into the MessageBus. +func (s *DeviceService) publishingDeviceData() { + s.wg.Add(1) + + go func() { + defer s.wg.Done() + + for { + select { + case data := <-s.bus: + if err := s.doc.Publish(data); err != nil { + s.logger.WithError(err).Errorf("fail to publish the device data %+v", data) + } else { + s.logger.Debugf("success to publish the device data %+v", data) + } + case <-s.ctx.Done(): + break + } + } + }() +} + +// TODO 是否可以将 DeviceDataHandler 转移到 DeviceServiceDeviceDataOperationClient 中? + +type DeviceDataHandler func(product *models.Product, device *models.Device, conn models.DeviceConnector, + dataID string, fields map[string]interface{}) error + +func (s *DeviceService) handlingDeviceData() { + s.wg.Add(1) + + s.unsupportedDeviceDataTypes = map[models.DeviceDataOperation]struct{}{ + models.DeviceDataOperationRead: {}, // property read + models.DeviceDataOperationEvent: {}, // event + models.DeviceDataOperationResponse: {}, // method response + models.DeviceDataOperationError: {}, // method error + } + s.deviceDataHandlers = map[models.DeviceDataOperation]DeviceDataHandler{ + models.DeviceDataOperationWrite: s.handleWriteData, // property write + models.DeviceDataOperationRequest: s.handleRequestData, // method request + } + + go func() { + defer s.wg.Done() + + topic := bus.TopicTypeDeviceData.Topic() + if err := s.doc.Subscribe(s.handleDeviceData, topic); err != nil { + s.logger.WithError(err).Errorf("fail to subscribe the topic: %s", topic) + return + } + }() +} + +func (s *DeviceService) handleDeviceData(msg *bus.Message) { + tags, fields, err := msg.Parse() + if err != nil { + s.logger.WithError(err).Errorf("fail to parse the message[%s]", msg.ToString()) + return + } + + productID, deviceID, optType, dataID := tags[0], tags[1], tags[2], tags[3] + product, err := s.getProduct(productID) + if err != nil { + s.logger.Error(err.Error()) + return + } + device, err := s.getDevice(deviceID) + if err != nil { + s.logger.Error(err.Error()) + return + } + connector, err := s.getDeviceConnector(deviceID) + if err != nil { + s.logger.Error(err.Error()) + return + } + + if _, ok := s.unsupportedDeviceDataTypes[optType]; ok { + return + } + handler, ok := s.deviceDataHandlers[optType] + if !ok { + s.logger.Errorf("unsupported operation type: %s", optType) + return + } + if err = handler(product, device, connector, dataID, fields); err != nil { + s.logger.WithError(err).Errorf("fail to handle the message: %+v", msg) + return + } +} + +// handleWriteData is responsible for handling the write request forwarded by the device manager. +// The fields will be written into the real device finally. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/write/float" -m "{\"intf\": 100}" +// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Write("randnum_test01", "randnum_test01", "float", 100) +// 2. Observe the log of device service. +func (s *DeviceService) handleWriteData(product *models.Product, device *models.Device, conn models.DeviceConnector, + propertyID string, fields map[string]interface{}) error { + properties := map[models.ProductPropertyID]*models.ProductProperty{} + for _, property := range product.Properties { + properties[property.Id] = property + } + + var written interface{} + if propertyID == models.DeviceDataMultiPropsID { + tmp := make(map[models.ProductPropertyID]interface{}, len(fields)) + for k, v := range fields { + property, ok := properties[k] + if !ok { + s.logger.Errorf("undefined property: %s", propertyID) + continue + } + if !property.Writeable { + s.logger.Errorf("the property[%s] is read only", propertyID) + continue + } + tmp[k] = v + } + written = tmp + } else { + property, ok := properties[propertyID] + if !ok { + return fmt.Errorf("undefined property: %s", propertyID) + } + if !property.Writeable { + return fmt.Errorf("the property[%s] is read only", propertyID) + } + + v, ok := fields[propertyID] + if !ok { + return fmt.Errorf("the property[%s]'s value is missed", propertyID) + } + written = map[models.ProductPropertyID]interface{}{propertyID: v} + } + return conn.Write(propertyID, written) +} + +// handleRequestData is responsible for handling the method's request forwarded by the device manager. +// The fields will be expanded a request, and +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/request/Intn" -m "{\"n\": 100}" +// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Call("randnum_test01", "randnum_test01", "Intn", map[string]interface{}{"n": 100}) +// 2. Observe the log of device service and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/response/Intn". +func (s *DeviceService) handleRequestData(product *models.Product, device *models.Device, conn models.DeviceConnector, + methodID string, fields map[string]interface{}) error { + request := models.NewDeviceData(product.ID, device.ID, models.DeviceDataOperationRequest, methodID) + request.SetFields(fields) + response, err := conn.Call(methodID, *request) + if err != nil { + return err + } + if err = s.doc.Publish(response); err != nil { + return err + } + return nil +} diff --git a/pkg/startup/device_service/handle_operations_meta.go b/pkg/startup/device_service/handle_operations_meta.go index 8a7cbe1..a7a0ef9 100644 --- a/pkg/startup/device_service/handle_operations_meta.go +++ b/pkg/startup/device_service/handle_operations_meta.go @@ -15,6 +15,7 @@ func (s *DeviceService) registerProtocol() { s.logger.Infof("success to register the protocol[%s] to the device manager", s.protocol.ID) } +// unregisterProtocol tries to unregister the protocol from the device manager. func (s *DeviceService) unregisterProtocol() { if err := s.moc.UnregisterProtocol(s.protocol.ID); err != nil { s.logger.WithError(err).Errorf("fail to unregister the protocol[%s] "+ @@ -64,25 +65,34 @@ func (s *DeviceService) activateDevice(device *models.Device) error { return err } if err := connector.Initialize(s.logger); err != nil { + s.logger.WithError(err).Error("fail to initialize the random device connector") return err } if err := connector.Start(); err != nil { + s.logger.WithError(err).Error("fail to start the random device connector") return err } - // TODO What we need to pick up from the device? How do we pick up? - //for _, event := range product.Events { - // if err := connector.Subscribe(event.Id, s.buffer); err != nil { - // s.logger.WithError(err).Errorf("fail to subscribe the product event[%s]", event.Id) - // continue - // } - //} + if len(product.Properties) != 0 { + if err := connector.Watch(s.bus); err != nil { + s.logger.WithError(err).Error("fail to watch properties for the device") + return err + } + } + + for _, event := range product.Events { + if err := connector.Subscribe(event.Id, s.bus); err != nil { + s.logger.WithError(err).Errorf("fail to subscribe the product event[%s]", event.Id) + continue + } + } s.deviceConnectors.Store(device.ID, connector) s.logger.Infof("success to activate the device[%s]", device.ID) return nil } +// deactivateDevices tries to deactivate all devices. func (s *DeviceService) deactivateDevices() { s.deviceConnectors.Range(func(key, value interface{}) bool { deviceID := key.(string) @@ -93,6 +103,7 @@ func (s *DeviceService) deactivateDevices() { }) } +// deactivateDevice is responsible for breaking up the connection with the real device. func (s *DeviceService) deactivateDevice(deviceID string) error { connector, _ := s.getDeviceConnector(deviceID) if connector == nil { From f914cc3ebdd5522930e6a508fa2e8c047427fe63 Mon Sep 17 00:00:00 2001 From: xufangyou Date: Thu, 25 Nov 2021 11:52:21 +0800 Subject: [PATCH 11/12] refactor code --- README.md | 2 +- config/config.go | 140 ------------- config/message_bus_options.go | 29 --- go.mod | 13 +- go.sum | 3 +- internal/driver/device_service.go | 159 ++++++++++++++ internal/driver/handle_operations_device.go | 194 ++++++++++++++++++ internal/driver/handle_operations_meta.go | 142 +++++++++++++ internal/message_bus/bus.go | 193 ----------------- internal/message_bus/data.go | 49 ----- internal/message_bus/data_meta.go | 81 -------- internal/message_bus/message.go | 34 --- internal/message_bus/topic.go | 141 ------------- internal/operations/client_manager.go | 112 ---------- internal/operations/client_service.go | 94 --------- internal/operations/device_data_publish.go | 7 - internal/operations/device_data_subscribe.go | 9 - internal/operations/device_event_receive.go | 34 --- internal/operations/device_method_call.go | 17 -- internal/operations/device_property_read.go | 34 --- internal/operations/device_property_write.go | 21 -- internal/operations/meta_device_list.go | 97 --------- internal/operations/meta_message.go | 31 --- internal/operations/meta_product_list.go | 102 --------- internal/operations/meta_protocol_register.go | 100 --------- .../operations/meta_protocol_unregister.go | 99 --------- logger/logger.go | 110 ---------- pkg/models/data_converter.go | 35 ---- pkg/models/data_device.go | 101 --------- pkg/models/device.go | 19 -- pkg/models/device_connector.go | 37 ---- pkg/models/meta_loader.go | 104 ---------- pkg/models/meta_operator.go | 6 - pkg/models/product.go | 55 ----- pkg/models/property.go | 37 ---- pkg/models/protocol.go | 16 -- pkg/startup/device_manager/device_manager.go | 88 -------- .../device_manager/handle_operations_meta.go | 72 ------- pkg/startup/device_manager/startup.go | 14 -- pkg/startup/device_service/device_service.go | 137 ------------- .../handle_operations_device.go | 169 --------------- .../device_service/handle_operations_meta.go | 119 ----------- pkg/startup/device_service/startup.go | 14 -- pkg/startup/startup.go | 18 ++ version/version.go | 3 - 45 files changed, 520 insertions(+), 2571 deletions(-) delete mode 100644 config/config.go delete mode 100644 config/message_bus_options.go create mode 100644 internal/driver/device_service.go create mode 100644 internal/driver/handle_operations_device.go create mode 100644 internal/driver/handle_operations_meta.go delete mode 100644 internal/message_bus/bus.go delete mode 100644 internal/message_bus/data.go delete mode 100644 internal/message_bus/data_meta.go delete mode 100644 internal/message_bus/message.go delete mode 100644 internal/message_bus/topic.go delete mode 100644 internal/operations/client_manager.go delete mode 100644 internal/operations/client_service.go delete mode 100644 internal/operations/device_data_publish.go delete mode 100644 internal/operations/device_data_subscribe.go delete mode 100644 internal/operations/device_event_receive.go delete mode 100644 internal/operations/device_method_call.go delete mode 100644 internal/operations/device_property_read.go delete mode 100644 internal/operations/device_property_write.go delete mode 100644 internal/operations/meta_device_list.go delete mode 100644 internal/operations/meta_message.go delete mode 100644 internal/operations/meta_product_list.go delete mode 100644 internal/operations/meta_protocol_register.go delete mode 100644 internal/operations/meta_protocol_unregister.go delete mode 100644 logger/logger.go delete mode 100644 pkg/models/data_converter.go delete mode 100644 pkg/models/data_device.go delete mode 100644 pkg/models/device.go delete mode 100644 pkg/models/device_connector.go delete mode 100644 pkg/models/meta_loader.go delete mode 100644 pkg/models/meta_operator.go delete mode 100644 pkg/models/product.go delete mode 100644 pkg/models/property.go delete mode 100644 pkg/models/protocol.go delete mode 100644 pkg/startup/device_manager/device_manager.go delete mode 100644 pkg/startup/device_manager/handle_operations_meta.go delete mode 100644 pkg/startup/device_manager/startup.go delete mode 100644 pkg/startup/device_service/device_service.go delete mode 100644 pkg/startup/device_service/handle_operations_device.go delete mode 100644 pkg/startup/device_service/handle_operations_meta.go delete mode 100644 pkg/startup/device_service/startup.go create mode 100644 pkg/startup/startup.go delete mode 100644 version/version.go diff --git a/README.md b/README.md index fa1f171..6c4a8ec 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# edge-device-sdk \ No newline at end of file +# edge-device-driver \ No newline at end of file diff --git a/config/config.go b/config/config.go deleted file mode 100644 index c191cc3..0000000 --- a/config/config.go +++ /dev/null @@ -1,140 +0,0 @@ -package config - -import ( - "fmt" - "github.com/jinzhu/configor" - "os" - "reflect" - "strconv" - "strings" - "time" -) - -func init() { - if _, err := LoadConfiguration(); err != nil { - panic("fail to load configuration") - } -} - -const ( - ConfigurationPath = "./resources/configuration.yaml" // xxx/resource/configuration.yaml - ProtocolsPath = "./resources/protocols" // xxx-device-conn/cmd/resources/protocols - ProductsPath = "./resources/products" // edge-device-manager/resources/products - DevicesPath = "./resources/devices" // edge-device-manager/resources/devices - - EnvSep = "," -) - -var C *Configuration - -type Configuration struct { - MessageBus MessageBusOptions `json:"message_bus" yaml:"message_bus"` -} - -func LoadConfiguration() (*Configuration, error) { - C = new(Configuration) - - path := os.Getenv("CONFIG_FILE") - if path == "" { - path = ConfigurationPath - } - if err := configor.Load(C, path); err != nil { - return nil, fmt.Errorf("failed to load configuration file, got %s", err.Error()) - } - - if value := reflect.ValueOf(C).Elem().FieldByName("MessageBus"); value.IsValid() { - m := value.Interface().(MessageBusOptions) - LoadMessageBusOptions(&m) - value.Set(reflect.ValueOf(m)) - } - return C, nil -} - -func LoadEnv(target *string, env string) { - value := os.Getenv(env) - if value != "" { - *target = value - } -} - -// LoadEnvs will read the values of given environments, -// then overwrite the pointer if value is not empty. -func LoadEnvs(envs map[string]interface{}) error { - var err error - for env, target := range envs { - value := os.Getenv(env) - if value == "" { - continue - } - switch target.(type) { - case *string: - *(target.(*string)) = value - case *[]string: - values := strings.Split(value, EnvSep) - result := make([]string, 0) - for _, v := range values { - if v != "" { - result = append(result, v) - } - } - if len(result) != 0 { - *(target.(*[]string)) = result - } - case *int: - *(target.(*int)), err = strconv.Atoi(value) - case *bool: - *(target.(*bool)), err = strconv.ParseBool(value) - case *int64: - *(target.(*int64)), err = strconv.ParseInt(value, 10, 64) - case *float32: - if v, err := strconv.ParseFloat(value, 32); err == nil { - *(target.(*float32)) = float32(v) - } - case *float64: - *(target.(*float64)), err = strconv.ParseFloat(value, 64) - case *time.Duration: - *(target.(*time.Duration)), err = time.ParseDuration(value) - default: - return fmt.Errorf("unsupported env type : %T", target) - } - if err != nil { - return fmt.Errorf("fail to load environments, got %s", err.Error()) - } - } - return nil -} - -func LoadEnvList(target *[]string, env string) { - value := os.Getenv(env) - values := strings.Split(value, EnvSep) - result := make([]string, 0) - for _, v := range values { - if v != "" { - result = append(result, v) - } - } - if len(result) != 0 { - *target = result - } -} - -func LoadEnvBool(target *bool, env string) { - value := os.Getenv(env) - if value != "" { - *target, _ = strconv.ParseBool(value) - } -} - -func LoadEnvInt(target *int, env string) { - value := os.Getenv(env) - if value != "" { - *target, _ = strconv.Atoi(value) - } -} - -func LoadEnvInt64(target *int64, env string) { - value := os.Getenv(env) - if value != "" { - *target, _ = strconv.ParseInt(value, 10, 64) - } -} diff --git a/config/message_bus_options.go b/config/message_bus_options.go deleted file mode 100644 index 2d49828..0000000 --- a/config/message_bus_options.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -func LoadMessageBusOptions(options *MessageBusOptions) { - LoadEnv(&options.Host, "MESSAGE_BUS_HOST") - LoadEnvInt(&options.Port, "MESSAGE_BUS_PORT") - LoadEnv(&options.Protocol, "MESSAGE_BUS_PROTOCOL") - LoadEnvInt(&options.ConnectTimoutMillisecond, "MESSAGE_BUS_CONNECT_TIMEOUT_MS") - LoadEnvInt(&options.TimeoutMillisecond, "MESSAGE_BUS_TIMEOUT_MS") - LoadEnvInt(&options.QoS, "MESSAGE_BUS_QOS") - LoadEnvBool(&options.CleanSession, "MESSAGE_BUS_CLEAN_SESSION") -} - -type MessageBusOptions struct { - // Host is the hostname or IP address of the MQTT broker. - Host string `json:"host" yaml:"host"` - // Port is the port of the MQTT broker. - Port int `json:"port" yaml:"port"` - // Protocol is the protocol to use when communicating with the MQTT broker, such as "tcp". - Protocol string `json:"protocol" yaml:"protocol"` - - // ConnectTimoutMillisecond indicates the timeout of connecting to the MQTT broker. - ConnectTimoutMillisecond int `json:"connect_timout_millisecond" yaml:"connect_timout_millisecond"` - // TimeoutMillisecond indicates the timeout of manipulations. - TimeoutMillisecond int `json:"timeout_millisecond" yaml:"timeout_millisecond"` - // QoS is the abbreviation of MQTT Quality of Service. - QoS int `json:"qos" yaml:"qos"` - // CleanSession indicates whether retain messages after reconnecting for QoS1 and QoS2. - CleanSession bool `json:"clean_session" yaml:"clean_session"` -} diff --git a/go.mod b/go.mod index d296633..0efdd39 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,7 @@ -module github.com/thingio/edge-device-sdk +module github.com/thingio/edge-device-driver -go 1.16 +replace github.com/thingio/edge-device-std => ./../edge-device-std + +require github.com/thingio/edge-device-std v0.0.0 -require ( - github.com/eclipse/paho.mqtt.golang v1.3.5 - github.com/jinzhu/configor v1.2.1 - github.com/sirupsen/logrus v1.8.1 - gopkg.in/yaml.v2 v2.4.0 -) +go 1.16 diff --git a/go.sum b/go.sum index 03a304e..b2b6df8 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,5 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/driver/device_service.go b/internal/driver/device_service.go new file mode 100644 index 0000000..fe989b8 --- /dev/null +++ b/internal/driver/device_service.go @@ -0,0 +1,159 @@ +package driver + +import ( + "context" + "errors" + "fmt" + "github.com/thingio/edge-device-std/config" + "github.com/thingio/edge-device-std/logger" + "github.com/thingio/edge-device-std/models" + bus "github.com/thingio/edge-device-std/msgbus" + "github.com/thingio/edge-device-std/operations" + "github.com/thingio/edge-device-std/version" + "os" + "sync" +) + +func NewDeviceDriver(ctx context.Context, cancel context.CancelFunc, + protocol *models.Protocol, dtBuilder models.DeviceTwinBuilder) (*DeviceDriver, error) { + if protocol == nil { + return nil, errors.New("the product cannot be nil") + } + if dtBuilder == nil { + return nil, errors.New("please implement and specify the connector builder") + } + dd := &DeviceDriver{ + ID: protocol.ID, + Name: protocol.Name, + Version: version.Version, + + protocol: protocol, + dtBuilder: dtBuilder, + + products: sync.Map{}, + devices: sync.Map{}, + deviceConnectors: sync.Map{}, + + ctx: ctx, + cancel: cancel, + wg: sync.WaitGroup{}, + logger: logger.NewLogger(), + } + + dd.unsupportedDataOperations = map[models.DataOperationType]struct{}{ + models.DataOperationTypeHealthCheckPong: {}, // device health check pong + models.DataOperationTypeReadRsp: {}, // property read response + models.DataOperationTypeWatch: {}, // property watch + models.DataOperationTypeEvent: {}, // event + models.DataOperationTypeResponse: {}, // method response + models.DataOperationTypeError: {}, // method error + } + dd.dataOperationHandlers = map[models.DataOperationType]DeviceOperationHandler{ + models.DataOperationTypeHealthCheckPing: dd.handleHealthCheck, // device health check ping + models.DataOperationTypeReadReq: dd.handleRead, // property read + models.DataOperationTypeWrite: dd.handleWrite, // property write + models.DataOperationTypeRequest: dd.handleRequest, // method request + } + return dd, nil +} + +type DeviceDriver struct { + // driver information + ID string + Name string + Version string + + // driver + protocol *models.Protocol + dtBuilder models.DeviceTwinBuilder + + // caches + products sync.Map + devices sync.Map + deviceConnectors sync.Map + unsupportedDataOperations map[models.DataOperationType]struct{} + dataOperationHandlers map[models.DataOperationType]DeviceOperationHandler + + // operation clients + bus chan models.DataOperation + moc operations.MetaOperationDriverClient // wrap the message bus to manipulate + doc operations.DataOperationDriverClient // warp the message bus to manipulate device data + + // lifetime control variables for the device driver + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + logger *logger.Logger +} + +func (d *DeviceDriver) Initialize() { + d.initializeOperationClients() +} + +func (d *DeviceDriver) initializeOperationClients() { + d.bus = make(chan models.DataOperation, 100) + + mb, err := bus.NewMessageBus(&config.C.MessageBus, d.logger) + if err != nil { + d.logger.WithError(err).Error("fail to initialize the message bus") + os.Exit(1) + } + if err = mb.Connect(); err != nil { + d.logger.WithError(err).Error("fail to connect to the message bus") + os.Exit(1) + } + + moc, err := operations.NewMetaOperationDriverClient(mb, d.logger) + if err != nil { + d.logger.WithError(err).Error("fail to initialize the meta operation client for the device driver") + os.Exit(1) + } + d.moc = moc + doc, err := operations.NewDataOperationDriverClient(mb, d.logger) + if err != nil { + d.logger.WithError(err).Error("fail to initialize the device data operation client for the device driver") + os.Exit(1) + } + d.doc = doc +} + +func (d *DeviceDriver) Serve() { + defer d.Stop(false) + + d.registerProtocol() + defer d.unregisterProtocol() + + d.activateDevices() + defer d.deactivateDevices() + + d.publishingDeviceData() + d.handlingDeviceData() + + d.wg.Wait() +} + +func (d *DeviceDriver) Stop(force bool) {} + +func (d *DeviceDriver) getProduct(productID string) (*models.Product, error) { + v, ok := d.products.Load(productID) + if ok { + return v.(*models.Product), nil + } + return nil, fmt.Errorf("the product[%d] is not found in cache", productID) +} + +func (d *DeviceDriver) getDevice(deviceID string) (*models.Device, error) { + v, ok := d.devices.Load(deviceID) + if ok { + return v.(*models.Device), nil + } + return nil, fmt.Errorf("the device[%d] is not found in cache", deviceID) +} + +func (d *DeviceDriver) getDeviceConnector(deviceID string) (models.DeviceTwin, error) { + v, ok := d.deviceConnectors.Load(deviceID) + if ok { + return v.(models.DeviceTwin), nil + } + return nil, fmt.Errorf("the device[%d] is not activated", deviceID) +} diff --git a/internal/driver/handle_operations_device.go b/internal/driver/handle_operations_device.go new file mode 100644 index 0000000..c8d23fe --- /dev/null +++ b/internal/driver/handle_operations_device.go @@ -0,0 +1,194 @@ +package driver + +import ( + "fmt" + "github.com/thingio/edge-device-std/models" + "github.com/thingio/edge-device-std/msgbus/message" +) + +// publishingDeviceData tries to publish data in the bus into the MQTTMessageBus. +func (d *DeviceDriver) publishingDeviceData() { + d.wg.Add(1) + + go func() { + defer d.wg.Done() + + for { + select { + case data := <-d.bus: + if err := d.doc.Publish(data); err != nil { + d.logger.WithError(err).Errorf("fail to publish the device data %+v", data) + } else { + d.logger.Debugf("success to publish the device data %+v", data) + } + case <-d.ctx.Done(): + break + } + } + }() +} + +type DeviceOperationHandler func(product *models.Product, device *models.Device, conn models.DeviceTwin, + dataID string, fields map[string]interface{}) error + +func (d *DeviceDriver) handlingDeviceData() { + d.wg.Add(1) + + go func() { + defer d.wg.Done() + + topic := models.TopicTypeDeviceData.Topic() + if err := d.doc.Subscribe(d.handleDeviceOperation, topic); err != nil { + d.logger.WithError(err).Errorf("fail to subscribe the topic: %d", topic) + return + } + }() +} + +func (d *DeviceDriver) handleDeviceOperation(msg *message.Message) { + o, err := models.ParseDataOperation(msg) + if err != nil { + d.logger.WithError(err).Errorf("fail to parse the message[%d]", msg.ToString()) + return + } + + productID, deviceID, optType, dataID := o.ProductID, o.DeviceID, o.OptType, o.FuncID + product, err := d.getProduct(productID) + if err != nil { + d.logger.Error(err.Error()) + return + } + device, err := d.getDevice(deviceID) + if err != nil { + d.logger.Error(err.Error()) + return + } + connector, err := d.getDeviceConnector(deviceID) + if err != nil { + d.logger.Error(err.Error()) + return + } + + if _, ok := d.unsupportedDataOperations[optType]; ok { + return + } + handler, ok := d.dataOperationHandlers[optType] + if !ok { + d.logger.Errorf("unsupported operation type: %d", optType) + return + } + if err = handler(product, device, connector, dataID, o.GetFields()); err != nil { + d.logger.WithError(err).Errorf("fail to handle the message: %+v", msg) + return + } +} + +// handleHealthCheck is responsible for handling health check forwarded by the device manager. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/health-check-ping/Intn" -m "{\"n\": 100}" +// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Call("randnum_test01", "randnum_test01", "Intn", map[string]interface{}{"n": 100}) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/response/Intn". +func (d *DeviceDriver) handleHealthCheck(product *models.Product, device *models.Device, twin models.DeviceTwin, + _ models.ProductMethodID, _ map[string]interface{}) error { + fields := make(map[string]interface{}) + status, err := twin.HealthCheck() + if err != nil { + fields[models.ProductPropertyStatusDetail] = err.Error() + } + fields[models.ProductPropertyStatus] = status + response := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeHealthCheckPong, device.ID) + response.SetFields(fields) + return d.doc.Publish(*response) +} + +// handleWrite is responsible for handling the read request forwarded by the device manager. +// It will read fields from the real device. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/read-req/float" -m "{\"float\": 100}" +// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Read("randnum_test01", "randnum_test01", "float", 100) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/read-rsp/float". +func (d *DeviceDriver) handleRead(product *models.Product, device *models.Device, twin models.DeviceTwin, + propertyID string, _ map[string]interface{}) error { + o := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeReadRsp, propertyID) + values, err := twin.Read(propertyID) + if err != nil { + return err + } + o.SetFields(values) + return d.doc.Publish(*o) +} + +// handleWrite is responsible for handling the write request forwarded by the device manager. +// The fields will be written into the real device finally. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/write/float" -m "{\"float\": 100}" +// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Write("randnum_test01", "randnum_test01", "float", 100) +// 2. Observe the log of device driver. +func (d *DeviceDriver) handleWrite(product *models.Product, device *models.Device, twin models.DeviceTwin, + propertyID string, fields map[models.ProductPropertyID]interface{}) error { + properties := map[models.ProductPropertyID]*models.ProductProperty{} + for _, property := range product.Properties { + properties[property.Id] = property + } + + var values map[models.ProductPropertyID]interface{} + if propertyID == models.DeviceDataMultiPropsID { + tmp := make(map[models.ProductPropertyID]interface{}, len(fields)) + for k, v := range fields { + property, ok := properties[k] + if !ok { + d.logger.Errorf("undefined property: %d", propertyID) + continue + } + if !property.Writeable { + d.logger.Errorf("the property[%d] is read only", propertyID) + continue + } + tmp[k] = v + } + values = tmp + } else { + property, ok := properties[propertyID] + if !ok { + return fmt.Errorf("undefined property: %d", propertyID) + } + if !property.Writeable { + return fmt.Errorf("the property[%d] is read only", propertyID) + } + + v, ok := fields[propertyID] + if !ok { + return fmt.Errorf("the property[%d]'d value is missed", propertyID) + } + values = map[models.ProductPropertyID]interface{}{propertyID: v} + } + return twin.Write(propertyID, values) +} + +// handleRequest is responsible for handling the method's request forwarded by the device manager. +// The fields will be expanded a request, and +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/request/Intn" -m "{\"n\": 100}" +// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Call("randnum_test01", "randnum_test01", "Intn", map[string]interface{}{"n": 100}) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/response/Intn". +func (d *DeviceDriver) handleRequest(product *models.Product, device *models.Device, twin models.DeviceTwin, + methodID models.ProductMethodID, ins map[string]interface{}) error { + outs, err := twin.Call(methodID, ins) + if err != nil { + return err + } + response := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeResponse, methodID) + response.SetFields(outs) + return d.doc.Publish(*response) +} diff --git a/internal/driver/handle_operations_meta.go b/internal/driver/handle_operations_meta.go new file mode 100644 index 0000000..f601a58 --- /dev/null +++ b/internal/driver/handle_operations_meta.go @@ -0,0 +1,142 @@ +package driver + +import ( + "github.com/thingio/edge-device-std/config" + "github.com/thingio/edge-device-std/models" + "os" + "time" +) + +// registerProtocol tries to register the protocol to the device manager. +func (d *DeviceDriver) registerProtocol() { + d.wg.Add(1) + + register := func() { + if err := d.moc.RegisterProtocol(d.protocol); err != nil { + d.logger.WithError(err).Errorf("fail to register the protocol[%d] "+ + "to the device manager", d.protocol.ID) + os.Exit(1) + } + d.logger.Infof("success to register the protocol[%d] to the device manager", d.protocol.ID) + } + register() + + go func() { + defer d.wg.Done() + + protocolRegisterInterval := time.Duration(config.C.CommonOptions.ProtocolRegisterIntervalSecond) * time.Second + ticker := time.NewTicker(protocolRegisterInterval) + for { + select { + case <-ticker.C: + register() + case <-d.ctx.Done(): + ticker.Stop() + return + } + } + }() +} + +// unregisterProtocol tries to unregister the protocol from the device manager. +func (d *DeviceDriver) unregisterProtocol() { + if err := d.moc.UnregisterProtocol(d.protocol.ID); err != nil { + d.logger.WithError(err).Errorf("fail to unregister the protocol[%d] "+ + "from the device manager", d.protocol.ID) + } + d.logger.Infof("success to unregister the protocol[%d] from the device manager", d.protocol.ID) +} + +// activateDevices tries to activate all devices. +func (d *DeviceDriver) activateDevices() { + products, err := d.moc.ListProducts(d.protocol.ID) + if err != nil { + d.logger.WithError(err).Error("fail to fetch products from the device manager") + os.Exit(1) + } + for _, product := range products { + devices, err := d.moc.ListDevices(product.ID) + if err != nil { + d.logger.WithError(err).Error("fail to fetch devices from the device manager") + os.Exit(1) + } + + d.products.Store(product.ID, product) + for _, device := range devices { + if err := d.activateDevice(device); err != nil { + d.logger.WithError(err).Errorf("fail to activate the device[%d]", device.ID) + continue + } + d.devices.Store(device.ID, device) + } + } +} + +// activateDevice is responsible for establishing the connection with the real device. +func (d *DeviceDriver) activateDevice(device *models.Device) error { + if connector, _ := d.getDeviceConnector(device.ID); connector != nil { // the device has been activated + _ = d.deactivateDevice(device.ID) + } + + // build, initialize and start connector + product, err := d.getProduct(device.ProductID) + if err != nil { + return err + } + connector, err := d.dtBuilder(product, device) + if err != nil { + return err + } + if err := connector.Initialize(d.logger); err != nil { + d.logger.WithError(err).Error("fail to initialize the random device connector") + return err + } + if err := connector.Start(); err != nil { + d.logger.WithError(err).Error("fail to start the random device connector") + return err + } + + if len(product.Properties) != 0 { + if err := connector.Watch(d.bus); err != nil { + d.logger.WithError(err).Error("fail to watch properties for the device") + return err + } + } + + for _, event := range product.Events { + if err := connector.Subscribe(event.Id, d.bus); err != nil { + d.logger.WithError(err).Errorf("fail to subscribe the product event[%d]", event.Id) + continue + } + } + + d.deviceConnectors.Store(device.ID, connector) + d.logger.Infof("success to activate the device[%d]", device.ID) + return nil +} + +// deactivateDevices tries to deactivate all devices. +func (d *DeviceDriver) deactivateDevices() { + d.deviceConnectors.Range(func(key, value interface{}) bool { + deviceID := key.(string) + if err := d.deactivateDevice(deviceID); err != nil { + d.logger.WithError(err).Errorf("fail to deactivate the device[%d]", deviceID) + } + return true + }) +} + +// deactivateDevice is responsible for breaking up the connection with the real device. +func (d *DeviceDriver) deactivateDevice(deviceID string) error { + connector, _ := d.getDeviceConnector(deviceID) + if connector == nil { + return nil + } + if err := connector.Stop(false); err != nil { + return err + } + + d.deviceConnectors.Delete(deviceID) + d.logger.Infof("success to deactivate the device[%d]", deviceID) + return nil +} diff --git a/internal/message_bus/bus.go b/internal/message_bus/bus.go deleted file mode 100644 index c091a44..0000000 --- a/internal/message_bus/bus.go +++ /dev/null @@ -1,193 +0,0 @@ -package bus - -import ( - "fmt" - mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/thingio/edge-device-sdk/config" - "github.com/thingio/edge-device-sdk/logger" - "strconv" - "time" -) - -func NewMessageBus(opts *config.MessageBusOptions, logger *logger.Logger) (MessageBus, error) { - mb := messageBus{ - timeout: time.Millisecond * time.Duration(opts.TimeoutMillisecond), - qos: opts.QoS, - routes: make(map[string]MessageHandler), - logger: logger, - } - if err := mb.setClient(opts); err != nil { - return nil, err - } - return &mb, nil -} - -// MessageBus encapsulates all common manipulations based on MQTT. -type MessageBus interface { - IsConnected() bool - - Connect() error - - Disconnect() error - - Publish(data Data) error - - Subscribe(handler MessageHandler, topics ...string) error - - Unsubscribe(topics ...string) error - - Call(request Data) (response Data, err error) -} - -type messageBus struct { - client mqtt.Client - timeout time.Duration - qos int - - routes map[string]MessageHandler // topic -> handler - - logger *logger.Logger -} - -func (mb *messageBus) IsConnected() bool { - return mb.client.IsConnected() -} - -func (mb *messageBus) Connect() error { - if mb.IsConnected() { - return nil - } - - token := mb.client.Connect() - return mb.handleToken(token) -} - -func (mb *messageBus) Disconnect() error { - if mb.IsConnected() { - mb.client.Disconnect(2000) // waiting 2s - } - return nil -} - -func (mb *messageBus) Publish(data Data) error { - msg, err := data.ToMessage() - if err != nil { - return err - } - - token := mb.client.Publish(msg.Topic, byte(mb.qos), false, msg.Payload) - return mb.handleToken(token) -} - -func (mb *messageBus) Subscribe(handler MessageHandler, topics ...string) error { - filters := make(map[string]byte) - for _, topic := range topics { - mb.routes[topic] = handler - filters[topic] = byte(mb.qos) - } - callback := func(mc mqtt.Client, msg mqtt.Message) { - go handler(&Message{ - Topic: msg.Topic(), - Payload: msg.Payload(), - }) - } - - token := mb.client.SubscribeMultiple(filters, callback) - return mb.handleToken(token) -} - -func (mb *messageBus) Unsubscribe(topics ...string) error { - for _, topic := range topics { - delete(mb.routes, topic) - } - - token := mb.client.Unsubscribe(topics...) - return mb.handleToken(token) -} - -func (mb *messageBus) Call(request Data) (response Data, err error) { - response, err = request.Response() - if err != nil { - return nil, err - } - - // subscribe response - rspMsg, err := response.ToMessage() - if err != nil { - return nil, err - } - ch := make(chan *Message, 1) - rspTpc := rspMsg.Topic - if err = mb.Subscribe(func(msg *Message) { - ch <- msg - }, rspTpc); err != nil { - return nil, err - } - defer func() { - close(ch) - _ = mb.Unsubscribe(rspTpc) - }() - - // publish request - if err = mb.Publish(request); err != nil { - return nil, err - } - // waiting for the response - ticker := time.NewTicker(mb.timeout) - select { - case msg := <-ch: - _, fields, err := msg.Parse() - if err != nil { - return nil, fmt.Errorf("fail to parse the message: %s, got %s", - msg.ToString(), err.Error()) - } - response.SetFields(fields) - return response, nil - case <-ticker.C: - ticker.Stop() - return nil, fmt.Errorf("call timeout: %dms", mb.timeout/time.Millisecond) - } -} - -func (mb *messageBus) handleToken(token mqtt.Token) error { - if mb.timeout > 0 { - token.WaitTimeout(mb.timeout) - } else { - token.Wait() - } - return token.Error() -} - -func (mb *messageBus) setClient(options *config.MessageBusOptions) error { - opts := mqtt.NewClientOptions() - clientID := "edge-device-sub-" + strconv.FormatInt(time.Now().UnixNano(), 10) - mb.logger.Infof("the ID of client for the message bus is %s", clientID) - opts.SetClientID(clientID) - opts.AddBroker(fmt.Sprintf("%s://%s:%d", options.Protocol, options.Host, options.Port)) - opts.SetConnectTimeout(time.Duration(options.ConnectTimoutMillisecond) * time.Millisecond) - opts.SetKeepAlive(time.Minute) - opts.SetAutoReconnect(true) - opts.SetOnConnectHandler(mb.onConnect) - opts.SetConnectionLostHandler(mb.onConnectLost) - opts.SetCleanSession(options.CleanSession) - - mb.client = mqtt.NewClient(opts) - return nil -} - -func (mb *messageBus) onConnect(mc mqtt.Client) { - reader := mc.OptionsReader() - mb.logger.Infof("the connection with %s for the message bus has been established.", reader.Servers()[0].String()) - - for tpc, hdl := range mb.routes { - if err := mb.Subscribe(hdl, tpc); err != nil { - mb.logger.WithError(err).Errorf("fail to resubscribe the topic: %s", tpc) - } - } -} - -func (mb *messageBus) onConnectLost(mc mqtt.Client, err error) { - reader := mc.OptionsReader() - mb.logger.WithError(err).Errorf("the connection with %s for the message bus has lost, trying to reconnect.", - reader.Servers()[0].String()) -} diff --git a/internal/message_bus/data.go b/internal/message_bus/data.go deleted file mode 100644 index 3e9103e..0000000 --- a/internal/message_bus/data.go +++ /dev/null @@ -1,49 +0,0 @@ -package bus - -import ( - "errors" -) - -type Data interface { - SetFields(fields map[string]interface{}) - GetFields() map[string]interface{} - SetField(key string, value interface{}) - GetField(key string) interface{} - - ToMessage() (*Message, error) - - Response() (response Data, err error) -} - -type MessageData struct { - Fields map[string]interface{} -} - -func (d *MessageData) SetFields(fields map[string]interface{}) { - for key, value := range fields { - d.SetField(key, value) - } -} - -func (d *MessageData) GetFields() map[string]interface{} { - return d.Fields -} - -func (d *MessageData) SetField(key string, value interface{}) { - if d.Fields == nil { - d.Fields = make(map[string]interface{}) - } - d.Fields[key] = value -} - -func (d *MessageData) GetField(key string) interface{} { - return d.Fields[key] -} - -func (d *MessageData) ToMessage() (*Message, error) { - return nil, errors.New("implement me") -} - -func (d *MessageData) Response() (response Data, err error) { - return nil, errors.New("implement me") -} diff --git a/internal/message_bus/data_meta.go b/internal/message_bus/data_meta.go deleted file mode 100644 index 6685e52..0000000 --- a/internal/message_bus/data_meta.go +++ /dev/null @@ -1,81 +0,0 @@ -package bus - -import ( - "encoding/json" - "fmt" -) - -type ( - MetaDataType = string - - MetaDataOperation = string - MetaDataOperationMode = string -) - -const ( - MetaDataTypeProtocol MetaDataType = "protocol" - MetaDataTypeProduct MetaDataType = "product" - MetaDataTypeDevice MetaDataType = "device" - - MetaDataOperationCreate MetaDataOperation = "create" - MetaDataOperationUpdate MetaDataOperation = "update" - MetaDataOperationDelete MetaDataOperation = "delete" - MetaDataOperationGet MetaDataOperation = "get" - MetaDataOperationList MetaDataOperation = "list" - - MetaDataOperationModeRequest MetaDataOperationMode = "request" - MetaDataOperationModeResponse MetaDataOperationMode = "response" -) - -type MetaData struct { - MessageData - - MetaType MetaDataType `json:"meta_type"` - OptType MetaDataOperation `json:"opt_type"` - OptMode MetaDataOperationMode `json:"opt_mode"` - DataID string `json:"data_id"` -} - -func (d *MetaData) ToMessage() (*Message, error) { - topic := NewMetaDataTopic(d.MetaType, d.OptType, d.OptMode, d.DataID) - payload, err := json.Marshal(d.Fields) - if err != nil { - return nil, err - } - - return &Message{ - Topic: topic.String(), - Payload: payload, - }, nil -} - -func (d *MetaData) isRequest() bool { - return d.OptMode == MetaDataOperationModeRequest -} - -func (d *MetaData) Response() (Data, error) { - if !d.isRequest() { - return nil, fmt.Errorf("the device data is not a request: %+v", *d) - } - return NewMetaData(d.MetaType, d.OptType, MetaDataOperationModeResponse, d.DataID), nil -} - -func NewMetaData(metaType MetaDataType, methodType MetaDataOperation, - optMode MetaDataOperationMode, dataID string) *MetaData { - return &MetaData{ - MetaType: metaType, - OptType: methodType, - OptMode: optMode, - DataID: dataID, - } -} - -func ParseMetaData(msg *Message) (*MetaData, error) { - tags, fields, err := msg.Parse() - if err != nil { - return nil, err - } - metaData := NewMetaData(tags[0], tags[1], tags[2], tags[3]) - metaData.SetFields(fields) - return metaData, nil -} diff --git a/internal/message_bus/message.go b/internal/message_bus/message.go deleted file mode 100644 index 110abe4..0000000 --- a/internal/message_bus/message.go +++ /dev/null @@ -1,34 +0,0 @@ -package bus - -import ( - "encoding/json" - "fmt" -) - -type MessageHandler func(msg *Message) - -// Message is an intermediate data format between MQTT and Data. -type Message struct { - Topic string - Payload []byte -} - -func (m *Message) Parse() ([]string, map[string]interface{}, error) { - // parse topic - topic, err := NewTopic(m.Topic) - if err != nil { - return nil, nil, err - } - tagValues := topic.TagValues() - - // parse payload - fields := make(map[string]interface{}) - if err := json.Unmarshal(m.Payload, &fields); err != nil { - return nil, nil, err - } - return tagValues, fields, nil -} - -func (m *Message) ToString() string { - return fmt.Sprintf("%+v", *m) -} diff --git a/internal/message_bus/topic.go b/internal/message_bus/topic.go deleted file mode 100644 index c68ab9f..0000000 --- a/internal/message_bus/topic.go +++ /dev/null @@ -1,141 +0,0 @@ -package bus - -import ( - "fmt" - "github.com/thingio/edge-device-sdk/version" - "strings" -) - -const ( - TagsOffset = 2 -) - -type TopicTagKey string - -type TopicTags map[TopicTagKey]string - -type TopicType string - -// Topic returns the topic that could subscribe all messages belong to this type. -func (t TopicType) Topic() string { - return strings.Join([]string{version.Version, string(t), TopicWildcard}, TopicSep) -} - -const ( - TopicTagKeyMetaDataType TopicTagKey = "meta_type" - TopicTagKeyMetaDataMethodType TopicTagKey = "method_type" - TopicTagKeyMetaDataMethodMode TopicTagKey = "method_mode" - TopicTagKeyProductID TopicTagKey = "product_id" - TopicTagKeyDeviceID TopicTagKey = "device_id" - TopicTagKeyOptType TopicTagKey = "opt_type" - TopicTagKeyDataID TopicTagKey = "data_id" - - TopicTypeMetaData TopicType = "META" - TopicTypeDeviceData TopicType = "DATA" - - TopicSep = "/" - TopicWildcard = "#" -) - -// Schemas describes all topics' forms. Every topic is formed by //.../. -var Schemas = map[TopicType][]TopicTagKey{ - TopicTypeMetaData: {TopicTagKeyMetaDataType, TopicTagKeyMetaDataMethodType, TopicTagKeyMetaDataMethodMode, TopicTagKeyDataID}, - TopicTypeDeviceData: {TopicTagKeyProductID, TopicTagKeyDeviceID, TopicTagKeyOptType, TopicTagKeyDataID}, -} - -type Topic interface { - Type() TopicType - - String() string - - Tags() TopicTags - TagKeys() []TopicTagKey - TagValues() []string - TagValue(key TopicTagKey) (value string, ok bool) -} - -type commonTopic struct { - topicTags TopicTags - topicType TopicType -} - -func (c *commonTopic) Type() TopicType { - return c.topicType -} - -func (c *commonTopic) String() string { - topicType := string(c.topicType) - tagValues := c.TagValues() - return strings.Join(append([]string{version.Version, topicType}, tagValues...), TopicSep) -} - -func (c *commonTopic) Tags() TopicTags { - return c.topicTags -} - -func (c *commonTopic) TagKeys() []TopicTagKey { - return Schemas[c.topicType] -} - -func (c *commonTopic) TagValues() []string { - tagKeys := c.TagKeys() - values := make([]string, len(tagKeys)) - for idx, topicTagKey := range tagKeys { - values[idx] = c.topicTags[topicTagKey] - } - return values -} - -func (c *commonTopic) TagValue(key TopicTagKey) (value string, ok bool) { - tags := c.Tags() - value, ok = tags[key] - return -} - -func NewTopic(topic string) (Topic, error) { - parts := strings.Split(topic, TopicSep) - if len(parts) < TagsOffset { - return nil, fmt.Errorf("invalid topic: %s", topic) - } - topicType := TopicType(parts[1]) - keys, ok := Schemas[topicType] - if !ok { - return nil, fmt.Errorf("undefined topic type: %s", topicType) - } - if len(parts)-TagsOffset != len(keys) { - return nil, fmt.Errorf("invalid topic: %s, keys [%+v] are necessary", topic, keys) - } - - tags := make(map[TopicTagKey]string) - for i, key := range keys { - tags[key] = parts[i+TagsOffset] - } - return &commonTopic{ - topicType: topicType, - topicTags: tags, - }, nil -} - -func NewMetaDataTopic(metaType, methodType, methodMode, dataID string) Topic { - return &commonTopic{ - topicTags: map[TopicTagKey]string{ - TopicTagKeyMetaDataType: metaType, - TopicTagKeyMetaDataMethodType: methodType, - TopicTagKeyMetaDataMethodMode: methodMode, - TopicTagKeyDataID: dataID, - }, - topicType: TopicTypeMetaData, - } -} - -func NewDeviceDataTopic(productID, deviceID, optType, dataID string) Topic { - return &commonTopic{ - topicTags: map[TopicTagKey]string{ - TopicTagKeyProductID: productID, - TopicTagKeyDeviceID: deviceID, - TopicTagKeyOptType: optType, - TopicTagKeyDataID: dataID, - }, - topicType: TopicTypeDeviceData, - } -} diff --git a/internal/operations/client_manager.go b/internal/operations/client_manager.go deleted file mode 100644 index b52b2e1..0000000 --- a/internal/operations/client_manager.go +++ /dev/null @@ -1,112 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func NewDeviceManagerMetaOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerMetaOperationClient, error) { - protocolClient, err := newDeviceManagerProtocolOperationClient(mb, logger) - if err != nil { - return nil, err - } - productClient, err := newDeviceManagerProductOperationClient(mb, logger) - if err != nil { - return nil, err - } - deviceClient, err := newDeviceManagerDeviceOperationClient(mb, logger) - if err != nil { - return nil, err - } - return &deviceManagerMetaOperationClient{ - protocolClient, - productClient, - deviceClient, - }, nil -} - -type DeviceManagerMetaOperationClient interface { - DeviceManagerProtocolOperationClient - DeviceManagerProductOperationClient - DeviceManagerDeviceOperationClient -} -type deviceManagerMetaOperationClient struct { - DeviceManagerProtocolOperationClient - DeviceManagerProductOperationClient - DeviceManagerDeviceOperationClient -} - -func newDeviceManagerProtocolOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerProtocolOperationClient, error) { - return &deviceManagerProtocolOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceManagerProtocolOperationClient interface { - OnRegisterProtocols(register func(protocol *models.Protocol) error) error - OnUnregisterProtocols(unregister func(protocolID string) error) error -} -type deviceManagerProtocolOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceManagerProductOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerProductOperationClient, error) { - return &deviceManagerProductOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceManagerProductOperationClient interface { - OnListProducts(list func(protocolID string) ([]*models.Product, error)) error -} -type deviceManagerProductOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceManagerDeviceOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerDeviceOperationClient, error) { - return &deviceManagerDeviceOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceManagerDeviceOperationClient interface { - OnListDevices(list func(productID string) ([]*models.Device, error)) error -} -type deviceManagerDeviceOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func NewDeviceManagerDeviceDataOperationClient(mb bus.MessageBus, logger *logger.Logger) (DeviceManagerDeviceDataOperationClient, error) { - reads := make(map[models.ProductPropertyID]chan models.DeviceData) - events := make(map[models.ProductEventID]chan models.DeviceData) - return &deviceManagerDeviceDataOperationClient{ - mb: mb, - logger: logger, - reads: reads, - events: events, - }, nil -} - -type DeviceManagerDeviceDataOperationClient interface { - Read(productID, deviceID string, - propertyID models.ProductPropertyID) (dd <-chan models.DeviceData, cc func(), err error) - - Write(productID, deviceID string, - propertyID models.ProductPropertyID, value interface{}) error - - Receive(productID, deviceID string, - eventID models.ProductEventID) (dd <-chan models.DeviceData, cc func(), err error) - - Call(productID, deviceID string, methodID models.ProductMethodID, - req map[string]interface{}) (rsp map[string]interface{}, err error) -} - -type deviceManagerDeviceDataOperationClient struct { - mb bus.MessageBus - logger *logger.Logger - - reads map[string]chan models.DeviceData - events map[string]chan models.DeviceData -} diff --git a/internal/operations/client_service.go b/internal/operations/client_service.go deleted file mode 100644 index 1fc7a52..0000000 --- a/internal/operations/client_service.go +++ /dev/null @@ -1,94 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func NewDeviceServiceMetaOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceMetaOperationClient, error) { - protocolClient, err := newDeviceServiceProtocolOperationClient(mb, logger) - if err != nil { - return nil, err - } - productClient, err := newDeviceServiceProductOperationClient(mb, logger) - if err != nil { - return nil, err - } - deviceClient, err := newDeviceServiceDeviceOperationClient(mb, logger) - if err != nil { - return nil, err - } - return &deviceServiceMetaOperationClient{ - protocolClient, - productClient, - deviceClient, - }, nil -} - -type DeviceServiceMetaOperationClient interface { - DeviceServiceProtocolOperationClient - DeviceServiceProductOperationClient - DeviceServiceDeviceOperationClient -} -type deviceServiceMetaOperationClient struct { - DeviceServiceProtocolOperationClient - DeviceServiceProductOperationClient - DeviceServiceDeviceOperationClient -} - -func newDeviceServiceProtocolOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceProtocolOperationClient, error) { - return &deviceServiceProtocolOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceProtocolOperationClient interface { - RegisterProtocol(protocol *models.Protocol) error - UnregisterProtocol(protocolID string) error -} -type deviceServiceProtocolOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceServiceProductOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceProductOperationClient, error) { - return &deviceServiceProductOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceProductOperationClient interface { - ListProducts(protocolID string) ([]*models.Product, error) -} -type deviceServiceProductOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceServiceDeviceOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceDeviceOperationClient, error) { - return &deviceServiceDeviceOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceDeviceOperationClient interface { - ListDevices(productID string) ([]*models.Device, error) -} -type deviceServiceDeviceOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func NewDeviceServiceDeviceDataOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceDeviceDataOperationClient, error) { - return &deviceServiceDeviceDataOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceDeviceDataOperationClient interface { - Publish(data models.DeviceData) error - Subscribe(handler bus.MessageHandler, topic string) error -} - -type deviceServiceDeviceDataOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} diff --git a/internal/operations/device_data_publish.go b/internal/operations/device_data_publish.go deleted file mode 100644 index 42123c5..0000000 --- a/internal/operations/device_data_publish.go +++ /dev/null @@ -1,7 +0,0 @@ -package operations - -import "github.com/thingio/edge-device-sdk/pkg/models" - -func (c *deviceServiceDeviceDataOperationClient) Publish(data models.DeviceData) error { - return c.mb.Publish(&data) -} diff --git a/internal/operations/device_data_subscribe.go b/internal/operations/device_data_subscribe.go deleted file mode 100644 index 9ad13d1..0000000 --- a/internal/operations/device_data_subscribe.go +++ /dev/null @@ -1,9 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" -) - -func (c *deviceServiceDeviceDataOperationClient) Subscribe(handler bus.MessageHandler, topic string) error { - return c.mb.Subscribe(handler, topic) -} diff --git a/internal/operations/device_event_receive.go b/internal/operations/device_event_receive.go deleted file mode 100644 index 1df31b6..0000000 --- a/internal/operations/device_event_receive.go +++ /dev/null @@ -1,34 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Receive(productID, deviceID string, - eventID models.ProductEventID) (dd <-chan models.DeviceData, cc func(), err error) { - data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationEvent, eventID) - message, err := data.ToMessage() - if err != nil { - return nil, nil, err - } - topic := message.Topic - if _, ok := c.events[eventID]; !ok { - c.events[eventID] = make(chan models.DeviceData, 100) - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - _, fields, _ := msg.Parse() - data.SetFields(fields) - - c.events[eventID] <- *data - }, topic); err != nil { - return nil, nil, err - } - - return c.events[eventID], func() { - if _, ok := c.events[topic]; ok { - close(c.events[topic]) - delete(c.events, topic) - } - }, nil -} diff --git a/internal/operations/device_method_call.go b/internal/operations/device_method_call.go deleted file mode 100644 index f8698cf..0000000 --- a/internal/operations/device_method_call.go +++ /dev/null @@ -1,17 +0,0 @@ -package operations - -import ( - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Call(productID, deviceID string, - methodID models.ProductMethodID, ins map[string]interface{}) (outs map[string]interface{}, err error) { - request := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationRequest, methodID) - request.SetFields(ins) - - response, err := c.mb.Call(request) - if err != nil { - return nil, err - } - return response.GetFields(), nil -} diff --git a/internal/operations/device_property_read.go b/internal/operations/device_property_read.go deleted file mode 100644 index 795b35a..0000000 --- a/internal/operations/device_property_read.go +++ /dev/null @@ -1,34 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Read(productID, deviceID string, - propertyID models.ProductPropertyID) (dd <-chan models.DeviceData, cc func(), err error) { - data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationRead, propertyID) - message, err := data.ToMessage() - if err != nil { - return nil, nil, err - } - topic := message.Topic - if _, ok := c.reads[topic]; !ok { - c.reads[topic] = make(chan models.DeviceData, 100) - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - _, fields, _ := msg.Parse() - data.SetFields(fields) - - c.reads[propertyID] <- *data - }, topic); err != nil { - return nil, nil, err - } - - return c.reads[propertyID], func() { - if _, ok := c.reads[topic]; ok { - close(c.reads[topic]) - delete(c.reads, topic) - } - }, nil -} diff --git a/internal/operations/device_property_write.go b/internal/operations/device_property_write.go deleted file mode 100644 index 5c65fdc..0000000 --- a/internal/operations/device_property_write.go +++ /dev/null @@ -1,21 +0,0 @@ -package operations - -import ( - "fmt" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Write(productID, deviceID string, - propertyID models.ProductPropertyID, value interface{}) error { - data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationWrite, propertyID) - if propertyID == models.DeviceDataMultiPropsID { - fields, ok := value.(map[models.ProductPropertyID]interface{}) - if !ok { - return fmt.Errorf("%+v must be type map[models.ProductPropertyID]interface{}", value) - } - data.SetFields(fields) - } else { - data.SetField(propertyID, value) - } - return c.mb.Publish(data) -} diff --git a/internal/operations/meta_device_list.go b/internal/operations/meta_device_list.go deleted file mode 100644 index 45ea166..0000000 --- a/internal/operations/meta_device_list.go +++ /dev/null @@ -1,97 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -type ListDevicesRequest struct { - ProductID string `json:"product_id"` -} - -func (r *ListDevicesRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListDevicesRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type ListDevicesResponse struct { - Devices []*models.Device `json:"devices"` -} - -func (r *ListDevicesResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListDevicesResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnListDevices for the device manager puts devices into the message bus. -func (c *deviceManagerDeviceOperationClient) OnListDevices(list func(productID string) ([]*models.Device, error)) error { - schema := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for listing devices", msg.ToString()) - return - } - req := &ListDevicesRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for listing devices") - return - } - - productID := req.ProductID - devices, err := list(productID) - if err != nil { - c.logger.WithError(err).Error("fail to list devices") - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, - bus.MetaDataOperationModeResponse, productID) - rsp := &ListDevicesResponse{Devices: devices} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for listing devices") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for listing devices") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// ListDevices for the device service takes devices from the message bus. -func (c *deviceServiceDeviceOperationClient) ListDevices(productID string) ([]*models.Device, error) { - request := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, productID) - fields, err := (&ListDevicesRequest{ProductID: productID}).Marshal() - if err != nil { - return nil, err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return nil, err - } - - rsp := &ListDevicesResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return nil, err - } - return rsp.Devices, nil -} diff --git a/internal/operations/meta_message.go b/internal/operations/meta_message.go deleted file mode 100644 index 7edfa5b..0000000 --- a/internal/operations/meta_message.go +++ /dev/null @@ -1,31 +0,0 @@ -package operations - -import "encoding/json" - -type MetaMessage interface { - Unmarshal(fields map[string]interface{}) error - Marshal() (map[string]interface{}, error) -} - -func Struct2Map(o interface{}) (map[string]interface{}, error) { - data, err := json.Marshal(o) - if err != nil { - return nil, err - } - fields := make(map[string]interface{}) - if err = json.Unmarshal(data, &fields); err != nil { - return nil, err - } - return fields, nil -} - -func Map2Struct(m map[string]interface{}, s interface{}) error { - data, err := json.Marshal(m) - if err != nil { - return err - } - if err = json.Unmarshal(data, s); err != nil { - return err - } - return nil -} diff --git a/internal/operations/meta_product_list.go b/internal/operations/meta_product_list.go deleted file mode 100644 index 3261897..0000000 --- a/internal/operations/meta_product_list.go +++ /dev/null @@ -1,102 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -type ListProductsRequest struct { - ProtocolID string `json:"protocol_id"` -} - -func (r *ListProductsRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListProductsRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type ListProductsResponse struct { - Products []*models.Product `json:"products"` -} - -func (r *ListProductsResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListProductsResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnListProducts for the device manager puts products into the message bus. -func (c *deviceManagerProductOperationClient) OnListProducts(list func(protocolID string) ([]*models.Product, error)) error { - schema := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for listing products", - msg.ToString()) - return - } - req := &ListProductsRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for listing products") - return - } - - protocolID := req.ProtocolID - products, err := list(protocolID) - if err != nil { - c.logger.WithError(err).Error("fail to list products") - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, - bus.MetaDataOperationModeResponse, protocolID) - if err != nil { - c.logger.WithError(err).Error("fail to construct response for the request") - return - } - rsp := &ListProductsResponse{Products: products} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for listing products") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for listing products") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// ListProducts for the device service takes products from the message bus. -func (c *deviceServiceProductOperationClient) ListProducts(protocolID string) ([]*models.Product, error) { - request := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, protocolID) - fields, err := (&ListProductsRequest{ProtocolID: protocolID}).Marshal() - if err != nil { - return nil, err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return nil, err - } - - rsp := &ListProductsResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return nil, err - } - return rsp.Products, nil -} diff --git a/internal/operations/meta_protocol_register.go b/internal/operations/meta_protocol_register.go deleted file mode 100644 index 0e49112..0000000 --- a/internal/operations/meta_protocol_register.go +++ /dev/null @@ -1,100 +0,0 @@ -package operations - -import ( - "errors" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -type RegisterProtocolRequest struct { - Protocol models.Protocol `json:"protocol"` -} - -func (r *RegisterProtocolRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *RegisterProtocolRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type RegisterProtocolResponse struct { - Success bool `json:"success"` -} - -func (r *RegisterProtocolResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *RegisterProtocolResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnRegisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) OnRegisterProtocols(register func(protocol *models.Protocol) error) error { - schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for registering protocol", - msg.ToString()) - return - } - req := &RegisterProtocolRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for registering protocol") - return - } - protocol := &req.Protocol - if err := register(protocol); err != nil { - c.logger.Error(err.Error()) - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, - bus.MetaDataOperationModeResponse, protocol.ID) - rsp := &RegisterProtocolResponse{Success: true} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for registering protocol") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for registering protocol") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// RegisterProtocol for the device service puts the protocol into the message bus. -func (c *deviceServiceProtocolOperationClient) RegisterProtocol(protocol *models.Protocol) error { - request := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, - bus.MetaDataOperationModeRequest, protocol.ID) - fields, err := (&RegisterProtocolRequest{Protocol: *protocol}).Marshal() - if err != nil { - return err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return err - } - - rsp := &RegisterProtocolResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return err - } - if !rsp.Success { - return errors.New("fail to register protocol") - } - return nil -} diff --git a/internal/operations/meta_protocol_unregister.go b/internal/operations/meta_protocol_unregister.go deleted file mode 100644 index 54f48cf..0000000 --- a/internal/operations/meta_protocol_unregister.go +++ /dev/null @@ -1,99 +0,0 @@ -package operations - -import ( - "errors" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" -) - -type UnregisterProtocolRequest struct { - ProtocolID string `json:"protocol_id"` -} - -func (r *UnregisterProtocolRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *UnregisterProtocolRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type UnregisterProtocolResponse struct { - Success bool `json:"success"` -} - -func (r *UnregisterProtocolResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *UnregisterProtocolResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnUnregisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) OnUnregisterProtocols(unregister func(protocolID string) error) error { - schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for unregistering protocol", - msg.ToString()) - return - } - req := &UnregisterProtocolRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for unregistering protocol") - return - } - protocolID := req.ProtocolID - if err := unregister(protocolID); err != nil { - c.logger.Error(err.Error()) - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, - bus.MetaDataOperationModeResponse, protocolID) - rsp := &UnregisterProtocolResponse{Success: true} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for unregistering protocol") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for unregistering protocol") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// UnregisterProtocol for the device service puts the protocol into the message bus. -func (c *deviceServiceProtocolOperationClient) UnregisterProtocol(protocolID string) error { - request := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, - bus.MetaDataOperationModeRequest, protocolID) - fields, err := (&UnregisterProtocolRequest{ProtocolID: protocolID}).Marshal() - if err != nil { - return err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return err - } - - rsp := &UnregisterProtocolResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return err - } - if !rsp.Success { - return errors.New("fail to unregister protocol") - } - return nil -} diff --git a/logger/logger.go b/logger/logger.go deleted file mode 100644 index 42f5737..0000000 --- a/logger/logger.go +++ /dev/null @@ -1,110 +0,0 @@ -package logger - -import ( - "github.com/sirupsen/logrus" - "os" -) - -type LogLevel string - -const ( - DebugLevel LogLevel = "debug" - InfoLevel LogLevel = "info" - WarnLevel LogLevel = "warn" - ErrorLevel LogLevel = "error" - FatalLevel LogLevel = "fatal" - PanicLevel LogLevel = "panic" -) - -func NewLogger() *Logger { - logger := &Logger{ - logger: logrus.NewEntry(logrus.New()), - } - _ = logger.SetLevel(InfoLevel) - return logger -} - -type Logger struct { - logger *logrus.Entry -} - -func (l Logger) SetLevel(level LogLevel) error { - lvl, err := logrus.ParseLevel(string(level)) - if err != nil { - return err - } - l.logger.Logger.SetLevel(lvl) - l.logger.Logger.SetOutput(os.Stdout) - l.logger.Logger.SetFormatter(&logFormatter{logrus.TextFormatter{FullTimestamp: true, ForceColors: true}}) - return nil -} - -// WithFields adds a map of fields to the Entry. -func (l Logger) WithFields(vs ...string) *logrus.Entry { - // mutex.Lock() - // defer mutex.Unlock() - fs := logrus.Fields{} - for index := 0; index < len(vs)-1; index = index + 2 { - fs[vs[index]] = vs[index+1] - } - return l.logger.WithFields(fs) -} - -// WithError adds an error as single field (using the key defined in ErrorKey) to the Entry. -func (l Logger) WithError(err error) *logrus.Entry { - if err == nil { - return l.logger - } - - return l.logger.WithField(logrus.ErrorKey, err.Error()) -} - -func (l Logger) Debugf(format string, args ...interface{}) { - l.logger.Debugf(format, args...) -} -func (l Logger) Infof(format string, args ...interface{}) { - l.logger.Infof(format, args...) -} -func (l Logger) Warnf(format string, args ...interface{}) { - l.logger.Infof(format, args...) -} -func (l Logger) Errorf(format string, args ...interface{}) { - l.logger.Errorf(format, args...) -} -func (l Logger) Fatalf(format string, args ...interface{}) { - l.logger.Fatalf(format, args...) -} -func (l Logger) Panicf(format string, args ...interface{}) { - l.logger.Panicf(format, args...) -} - -func (l Logger) Debug(args ...interface{}) { - l.logger.Debug(args...) -} -func (l Logger) Info(args ...interface{}) { - l.logger.Info(args...) -} -func (l Logger) Warn(args ...interface{}) { - l.logger.Info(args...) -} -func (l Logger) Error(args ...interface{}) { - l.logger.Error(args...) -} -func (l Logger) Fatal(args ...interface{}) { - l.logger.Fatal(args...) -} -func (l Logger) Panic(args ...interface{}) { - l.logger.Panic(args) -} - -type logFormatter struct { - logrus.TextFormatter -} - -func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) { - data, err := f.TextFormatter.Format(entry) - if err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/models/data_converter.go b/pkg/models/data_converter.go deleted file mode 100644 index 06526b0..0000000 --- a/pkg/models/data_converter.go +++ /dev/null @@ -1,35 +0,0 @@ -package models - -import ( - "strconv" -) - -var StrConverters = map[PropertyType]StrConverter{ - PropertyTypeInt: Str2Int, - PropertyTypeUint: Str2Uint, - PropertyTypeFloat: Str2Float, - PropertyTypeBool: Str2Bool, - PropertyTypeString: Str2String, -} - -type StrConverter func(s string) (interface{}, error) - -func Str2Int(s string) (interface{}, error) { - return strconv.ParseInt(s, 10, 64) -} - -func Str2Uint(s string) (interface{}, error) { - return strconv.ParseUint(s, 10, 64) -} - -func Str2Float(s string) (interface{}, error) { - return strconv.ParseFloat(s, 64) -} - -func Str2Bool(s string) (interface{}, error) { - return strconv.ParseBool(s) -} - -func Str2String(s string) (interface{}, error) { - return s, nil -} diff --git a/pkg/models/data_device.go b/pkg/models/data_device.go deleted file mode 100644 index 5e777af..0000000 --- a/pkg/models/data_device.go +++ /dev/null @@ -1,101 +0,0 @@ -package models - -import ( - "encoding/json" - "fmt" - "github.com/thingio/edge-device-sdk/internal/message_bus" -) - -type ( - DeviceDataOperation = string // the type of device data's operation - DeviceDataPropertyReportMode = string // the mode of device data's reporting - - ProductFuncID = string // product functionality ID - ProductFuncType = string // product functionality type - ProductPropertyID = ProductFuncID // product property's functionality ID - ProductEventID = ProductFuncID // product event's functionality ID - ProductMethodID = ProductFuncID // product method's functionality ID -) - -const ( - DeviceDataOperationRead DeviceDataOperation = "read" // Device Property Read - DeviceDataOperationWrite DeviceDataOperation = "write" // Device Property Write - DeviceDataOperationEvent DeviceDataOperation = "event" // Device Event - DeviceDataOperationRequest DeviceDataOperation = "request" // Device Method Request - DeviceDataOperationResponse DeviceDataOperation = "response" // Device Method Response - DeviceDataOperationError DeviceDataOperation = "error" // Device Method Error - - DeviceDataReportModePeriodical DeviceDataPropertyReportMode = "periodical" // report device data at intervals, e.g. 5s, 1m, 0.5h - DeviceDataReportModeMutated DeviceDataPropertyReportMode = "mutated" // report device data while mutated - - PropertyFunc ProductFuncType = "props" // product property's functionality - EventFunc ProductFuncType = "events" // product event's functionality - MethodFunc ProductFuncType = "methods" // product method's functionality - - DeviceDataMultiPropsID ProductFuncID = "*" - DeviceDataMultiPropsName = "多属性" -) - -// Opts2FuncType maps operation upon device data as product's functionality. -var opts2FuncType = map[DeviceDataOperation]ProductFuncType{ - DeviceDataOperationEvent: EventFunc, - DeviceDataOperationRead: PropertyFunc, - DeviceDataOperationWrite: PropertyFunc, - DeviceDataOperationRequest: MethodFunc, - DeviceDataOperationResponse: MethodFunc, - DeviceDataOperationError: MethodFunc, -} - -type DeviceData struct { - bus.MessageData - - ProductID string `json:"product_id"` - DeviceID string `json:"device_id"` - OptType DeviceDataOperation `json:"opt_type"` - FuncID ProductFuncID `json:"func_id"` - FuncType ProductFuncType `json:"func_type"` -} - -func (d *DeviceData) ToMessage() (*bus.Message, error) { - topic := bus.NewDeviceDataTopic(d.ProductID, d.DeviceID, d.OptType, d.FuncID) - payload, err := json.Marshal(d.Fields) - if err != nil { - return nil, err - } - - return &bus.Message{ - Topic: topic.String(), - Payload: payload, - }, nil -} - -func (d *DeviceData) isRequest() bool { - return d.OptType == DeviceDataOperationRequest -} - -func (d *DeviceData) Response() (response bus.Data, err error) { - if !d.isRequest() { - return nil, fmt.Errorf("the device data is not a request: %+v", *d) - } - return NewDeviceData(d.ProductID, d.DeviceID, DeviceDataOperationResponse, d.FuncID), nil -} - -func NewDeviceData(productID, deviceID string, optType DeviceDataOperation, dataID ProductFuncID) *DeviceData { - return &DeviceData{ - ProductID: productID, - DeviceID: deviceID, - OptType: optType, - FuncID: dataID, - FuncType: opts2FuncType[optType], - } -} - -func ParseDeviceData(msg *bus.Message) (*DeviceData, error) { - tags, fields, err := msg.Parse() - if err != nil { - return nil, err - } - deviceData := NewDeviceData(tags[0], tags[1], tags[2], tags[3]) - deviceData.SetFields(fields) - return deviceData, nil -} diff --git a/pkg/models/device.go b/pkg/models/device.go deleted file mode 100644 index 4d8b719..0000000 --- a/pkg/models/device.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -type Device struct { - ID string `json:"id"` // 设备 ID - Name string `json:"name"` // 设备名称 - Desc string `json:"desc"` // 设备描述 - ProductID string `json:"product_id"` // 设备所属产品 ID, 不可更新 - ProductName string `json:"product_name"` // 设备所属产品名称 - Category string `json:"category"` // 设备类型(多媒体, 时序), 不可更新 - Recording bool `json:"recording"` // 是否正在录制 - DeviceStatus string `json:"device_status"` // 设备状态 - DeviceProps map[string]string `json:"device_props"` // 设备动态属性, 取决于具体的设备协议 - DeviceLabels map[string]string `json:"device_labels"` // 设备标签 - DeviceMeta map[string]string `json:"device_meta"` // 视频流元信息 -} - -func (d *Device) GetProperty(key string) string { - return d.DeviceProps[key] -} diff --git a/pkg/models/device_connector.go b/pkg/models/device_connector.go deleted file mode 100644 index af3a31a..0000000 --- a/pkg/models/device_connector.go +++ /dev/null @@ -1,37 +0,0 @@ -package models - -import ( - "github.com/thingio/edge-device-sdk/logger" -) - -type DeviceConnector interface { - // Initialize will try to initialize a device connector to - // create the connection with device which needs to activate. - // It must always return nil if the device needn't be initialized. - Initialize(lg *logger.Logger) error - - // Start will to try to create connection with the real device. - // It must always return nil if the device needn't be initialized. - Start() error - // Stop will to try to destroy connection with the real device. - // It must always return nil if the device needn't be initialized. - Stop(force bool) error - // Ping is used to test the connectivity of the real device. - // If the device is connected, it will return true, else return false. - Ping() bool - - // Watch will read device's properties periodically with the specified policy. - Watch(bus chan<- DeviceData) error - // Write will write the specified property to the real device. - Write(propertyID ProductPropertyID, data interface{}) error - // Subscribe will subscribe the specified event, - // and put data belonging to this event into the bus. - Subscribe(eventID ProductEventID, bus chan<- DeviceData) error - // Call is used to call the specified method defined in product, - // then waiting for a while to receive its response. - // If the call is timeout, it will return a timeout error. - Call(methodID ProductMethodID, request DeviceData) (response DeviceData, err error) // method call -} - -// ConnectorBuilder is used to create a new device connector using the specified product and device. -type ConnectorBuilder func(product *Product, device *Device) (DeviceConnector, error) diff --git a/pkg/models/meta_loader.go b/pkg/models/meta_loader.go deleted file mode 100644 index 230fe96..0000000 --- a/pkg/models/meta_loader.go +++ /dev/null @@ -1,104 +0,0 @@ -package models - -import ( - "encoding/json" - "fmt" - "github.com/thingio/edge-device-sdk/config" - "gopkg.in/yaml.v2" - "io/fs" - "io/ioutil" - "path/filepath" -) - -func LoadProtocol(path string) (*Protocol, error) { - protocol := new(Protocol) - if err := load(path, protocol); err != nil { - return nil, err - } - return protocol, nil -} - -func LoadProducts(protocolID string) []*Product { - products := make([]*Product, 0) - _ = filepath.Walk(config.ProductsPath, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - var product *Product - product, err = loadProduct(path) - if err != nil { - return err - } - if product.Protocol != protocolID { - return nil - } - products = append(products, product) - return nil - }) - return products -} - -func LoadDevices(productID string) []*Device { - devices := make([]*Device, 0) - _ = filepath.Walk(config.DevicesPath, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - var device *Device - device, err = loadDevice(path) - if err != nil { - return err - } - if device.ProductID != productID { - return nil - } - devices = append(devices, device) - return nil - }) - return devices -} - -func loadProduct(path string) (*Product, error) { - product := new(Product) - if err := load(path, product); err != nil { - return nil, err - } - return product, nil -} - -func loadDevice(path string) (*Device, error) { - device := new(Device) - if err := load(path, device); err != nil { - return nil, err - } - return device, nil -} - -func load(path string, meta interface{}) error { - data, err := ioutil.ReadFile(path) - if err != nil { - return fmt.Errorf("fail to load the meta configurtion stored in %s, got %s", - path, err.Error()) - } - - var unmarshaller func([]byte, interface{}) error - switch ext := filepath.Ext(path); ext { - case ".json": - unmarshaller = json.Unmarshal - case ".yaml", ".yml": - unmarshaller = yaml.Unmarshal - default: - return fmt.Errorf("invalid meta configuration extension %s, only supporing json/yaml/yml", ext) - } - - if err := unmarshaller(data, meta); err != nil { - return fmt.Errorf("fail to unmarshal the device configuration, got %s", err.Error()) - } - return nil -} diff --git a/pkg/models/meta_operator.go b/pkg/models/meta_operator.go deleted file mode 100644 index 05cf2c5..0000000 --- a/pkg/models/meta_operator.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type MetaStore interface { - ListProducts(protocolID string) ([]*Product, error) - ListDevices(productID string) ([]*Device, error) -} diff --git a/pkg/models/product.go b/pkg/models/product.go deleted file mode 100644 index cbd6a53..0000000 --- a/pkg/models/product.go +++ /dev/null @@ -1,55 +0,0 @@ -package models - -type Product struct { - ID string `json:"id"` // 产品 ID - Name string `json:"name"` // 产品名称 - Desc string `json:"desc"` // 产品描述 - Protocol string `json:"protocol"` // 产品协议 - DataFormat string `json:"data_format,omitempty"` // 数据格式 - Properties []*ProductProperty `json:"properties,omitempty"` // 属性功能列表 - Events []*ProductEvent `json:"events,omitempty"` // 事件功能列表 - Methods []*ProductMethod `json:"methods,omitempty"` // 方法功能列表 - Topics []*ProductTopic `json:"topics,omitempty"` // 各功能对应的消息主题 -} - -type ProductProperty struct { - Id string `json:"id"` - Name string `json:"name"` - Desc string `json:"desc"` - Interval string `json:"interval"` - Unit string `json:"unit"` - FieldType string `json:"field_type"` - ReportMode string `json:"report_mode"` - Writeable bool `json:"writeable"` - AuxProps map[string]string `json:"aux_props"` -} - -type ProductEvent struct { - Id string `json:"id"` - Name string `json:"name"` - Desc string `json:"desc"` - Outs []*ProductField `json:"outs"` - AuxProps map[string]string `json:"aux_props"` -} - -type ProductMethod struct { - Id string `json:"id"` - Name string `json:"name"` - Desc string `json:"desc"` - Ins []*ProductField `json:"ins"` - Outs []*ProductField `json:"outs"` - AuxProps map[string]string `json:"aux_props"` -} - -type ProductTopic struct { - Topic string `json:"topic"` - OptType string `json:"opt_type"` - Desc string `json:"desc"` -} - -type ProductField struct { - Id string `json:"id"` - Name string `json:"name"` - FieldType string `json:"field_type"` - Desc string `json:"desc"` -} diff --git a/pkg/models/property.go b/pkg/models/property.go deleted file mode 100644 index 69e186e..0000000 --- a/pkg/models/property.go +++ /dev/null @@ -1,37 +0,0 @@ -package models - -type ( - PropertyType = string - PropertyStyle = string // UI Representation Style -) - -const ( - PropertyTypeInt PropertyType = "int" - PropertyTypeUint PropertyType = "uint" - PropertyTypeFloat PropertyType = "float" - PropertyTypeBool PropertyType = "bool" - PropertyTypeString PropertyType = "string" -) - -var ( - DeviceDataPropertyTypes = map[PropertyType]struct{}{ - PropertyTypeInt: {}, - PropertyTypeUint: {}, - PropertyTypeFloat: {}, - PropertyTypeBool: {}, - PropertyTypeString: {}, - } -) - -type Property struct { - Name string `json:"name"` // Name 为属性的展示名称 - Desc string `json:"desc"` // Desc 为属性的描述, 通常以名称旁的?形式进行展示 - Type PropertyType `json:"type"` // Type 为该属性的数据类型 - Style PropertyStyle `json:"style"` // Style 为该属性在前端的展示样式 - Default string `json:"default"` // Default 该属性默认的属性值 - Range string `json:"range"` // Range 为属性值的可选范围 - Precondition string `json:"precondition"` // Precondition 为当前属性展示的前置条件, 用来实现简单的动态依赖功能 - Required bool `json:"required"` // Required 表示该属性是否为必填项 - Multiple bool `json:"multiple"` // Multiple 表示是否支持多选(下拉框), 列表(输入), Map(K,V) - MaxLen int64 `json:"max_len"` // MaxLen 表示当Multiple为true时, 可选择的最大数量 -} diff --git a/pkg/models/protocol.go b/pkg/models/protocol.go deleted file mode 100644 index 551d083..0000000 --- a/pkg/models/protocol.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -type ( - ProtocolPropertyKey = string // common property owned by devices with the same protocol -) - -type Protocol struct { - ID string `json:"id"` // 协议 ID - Name string `json:"name"` // 协议名称 - Desc string `json:"desc"` // 协议描述 - Category string `json:"category"` - Language string `json:"language"` - SupportFuncs []string `json:"support_funcs"` - AuxProps []*Property `json:"aux_props"` - DeviceProps []*Property `json:"device_props"` -} diff --git a/pkg/startup/device_manager/device_manager.go b/pkg/startup/device_manager/device_manager.go deleted file mode 100644 index f16bfc6..0000000 --- a/pkg/startup/device_manager/device_manager.go +++ /dev/null @@ -1,88 +0,0 @@ -package startup - -import ( - "context" - "github.com/thingio/edge-device-sdk/config" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/internal/operations" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" - "github.com/thingio/edge-device-sdk/version" - "os" - "sync" -) - -type DeviceManager struct { - // manager information - Version string - - // caches - protocols sync.Map - - // operation clients - moc operations.DeviceManagerMetaOperationClient // wrap the message bus to manipulate meta - doc operations.DeviceManagerDeviceDataOperationClient // warp the message bus to manipulate device data - metaStore models.MetaStore // meta store - - // lifetime control variables for the device service - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - logger *logger.Logger -} - -func (m *DeviceManager) Initialize(ctx context.Context, cancel context.CancelFunc, metaStore models.MetaStore) { - m.logger = logger.NewLogger() - - m.Version = version.Version - - m.protocols = sync.Map{} - - m.initializeOperationClients(metaStore) - - m.ctx = ctx - m.cancel = cancel - m.wg = sync.WaitGroup{} -} - -func (m *DeviceManager) initializeOperationClients(metaStore models.MetaStore) { - mb, err := bus.NewMessageBus(&config.C.MessageBus, m.logger) - if err != nil { - m.logger.WithError(err).Error("fail to initialize the message bus") - os.Exit(1) - } - if err = mb.Connect(); err != nil { - m.logger.WithError(err).Error("fail to connect to the message bus") - os.Exit(1) - } - - moc, err := operations.NewDeviceManagerMetaOperationClient(mb, m.logger) - if err != nil { - m.logger.WithError(err).Error("fail to initialize the meta operation client for the device service") - os.Exit(1) - } - m.moc = moc - doc, err := operations.NewDeviceManagerDeviceDataOperationClient(mb, m.logger) - if err != nil { - m.logger.WithError(err).Error("fail to initialize the device data operation client for the device service") - os.Exit(1) - } - m.doc = doc - - m.metaStore = metaStore -} - -func (m *DeviceManager) Serve() { - defer m.Stop(false) - - m.wg.Add(1) - go m.watchingProtocols() - m.wg.Add(1) - go m.watchingProductOperations() - m.wg.Add(1) - go m.watchingDeviceOperations() - - m.wg.Wait() -} - -func (m *DeviceManager) Stop(force bool) {} diff --git a/pkg/startup/device_manager/handle_operations_meta.go b/pkg/startup/device_manager/handle_operations_meta.go deleted file mode 100644 index 7bd043f..0000000 --- a/pkg/startup/device_manager/handle_operations_meta.go +++ /dev/null @@ -1,72 +0,0 @@ -package startup - -import ( - "github.com/thingio/edge-device-sdk/pkg/models" - "time" -) - -func (m *DeviceManager) watchingProtocols() { - defer func() { - m.wg.Done() - }() - - if err := m.moc.OnRegisterProtocols(m.registerProtocol); err != nil { - m.logger.WithError(err).Error("fail to wait for registering protocols") - return - } - if err := m.moc.OnUnregisterProtocols(m.unregisterProtocol); err != nil { - m.logger.WithError(err).Error("fail to wait for unregistering protocols") - return - } - - ticker := time.NewTicker(1 * time.Minute) - for { - select { - case <-ticker.C: // check the connection of protocol - m.protocols.Range(func(key, value interface{}) bool { - // TODO PING - return true - }) - case <-m.ctx.Done(): - break - } - } -} - -func (m *DeviceManager) registerProtocol(protocol *models.Protocol) error { - m.protocols.Store(protocol.ID, protocol) - m.logger.Infof("the protocol[%s] has registered successfully", protocol.ID) - return nil -} - -func (m *DeviceManager) unregisterProtocol(protocolID string) error { - m.protocols.Delete(protocolID) - m.logger.Infof("the protocol[%s] has unregistered successfully", protocolID) - return nil -} - -func (m *DeviceManager) watchingProductOperations() { - defer func() { - m.wg.Done() - }() - - if err := m.moc.OnListProducts(m.metaStore.ListProducts); err != nil { - m.logger.WithError(err).Error("fail to wait for listing products") - return - } else { - m.logger.Infof("start to watch the changes for product...") - } -} - -func (m *DeviceManager) watchingDeviceOperations() { - defer func() { - m.wg.Done() - }() - - if err := m.moc.OnListDevices(m.metaStore.ListDevices); err != nil { - m.logger.WithError(err).Error("fail to wait for listing devices") - return - } else { - m.logger.Infof("start to watch the changes for device...") - } -} diff --git a/pkg/startup/device_manager/startup.go b/pkg/startup/device_manager/startup.go deleted file mode 100644 index d7b328b..0000000 --- a/pkg/startup/device_manager/startup.go +++ /dev/null @@ -1,14 +0,0 @@ -package startup - -import ( - "context" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func Startup(metaStore models.MetaStore) { - ctx, cancel := context.WithCancel(context.Background()) - - dm := &DeviceManager{} - dm.Initialize(ctx, cancel, metaStore) - dm.Serve() -} diff --git a/pkg/startup/device_service/device_service.go b/pkg/startup/device_service/device_service.go deleted file mode 100644 index 1d22c83..0000000 --- a/pkg/startup/device_service/device_service.go +++ /dev/null @@ -1,137 +0,0 @@ -package startup - -import ( - "context" - "fmt" - "github.com/thingio/edge-device-sdk/config" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/internal/operations" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" - "github.com/thingio/edge-device-sdk/version" - "os" - "sync" -) - -type DeviceService struct { - // service information - ID string - Name string - Version string - - // driver - protocol *models.Protocol - connectorBuilder models.ConnectorBuilder - - // caches - products sync.Map - devices sync.Map - deviceConnectors sync.Map - unsupportedDeviceDataTypes map[models.DeviceDataOperation]struct{} - deviceDataHandlers map[models.DeviceDataOperation]DeviceDataHandler - - // operation clients - bus chan models.DeviceData - moc operations.DeviceServiceMetaOperationClient // wrap the message bus to manipulate - doc operations.DeviceServiceDeviceDataOperationClient // warp the message bus to manipulate device data - - // lifetime control variables for the device service - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - logger *logger.Logger -} - -func (s *DeviceService) Initialize(ctx context.Context, cancel context.CancelFunc, - protocol *models.Protocol, connectorBuilder models.ConnectorBuilder) { - s.logger = logger.NewLogger() - - s.ID = protocol.ID - s.Name = protocol.Name - s.Version = version.Version - - s.protocol = protocol - if connectorBuilder == nil { - s.logger.Error("please implement and specify the connector builder") - os.Exit(1) - } - s.connectorBuilder = connectorBuilder - - s.products = sync.Map{} - s.devices = sync.Map{} - s.deviceConnectors = sync.Map{} - - s.initializeOperationClients() - - s.ctx = ctx - s.cancel = cancel - s.wg = sync.WaitGroup{} -} - -func (s *DeviceService) initializeOperationClients() { - s.bus = make(chan models.DeviceData, 100) - - mb, err := bus.NewMessageBus(&config.C.MessageBus, s.logger) - if err != nil { - s.logger.WithError(err).Error("fail to initialize the message bus") - os.Exit(1) - } - if err = mb.Connect(); err != nil { - s.logger.WithError(err).Error("fail to connect to the message bus") - os.Exit(1) - } - - moc, err := operations.NewDeviceServiceMetaOperationClient(mb, s.logger) - if err != nil { - s.logger.WithError(err).Error("fail to initialize the meta operation client for the device service") - os.Exit(1) - } - s.moc = moc - doc, err := operations.NewDeviceServiceDeviceDataOperationClient(mb, s.logger) - if err != nil { - s.logger.WithError(err).Error("fail to initialize the device data operation client for the device service") - os.Exit(1) - } - s.doc = doc -} - -func (s *DeviceService) Serve() { - defer s.Stop(false) - - s.registerProtocol() - defer s.unregisterProtocol() - - s.activateDevices() - defer s.deactivateDevices() - - s.publishingDeviceData() - s.handlingDeviceData() - - s.wg.Wait() -} - -func (s *DeviceService) Stop(force bool) {} - -func (s *DeviceService) getProduct(productID string) (*models.Product, error) { - v, ok := s.products.Load(productID) - if ok { - return v.(*models.Product), nil - } - return nil, fmt.Errorf("the product[%s] is not found in cache", productID) -} - -func (s *DeviceService) getDevice(deviceID string) (*models.Device, error) { - v, ok := s.devices.Load(deviceID) - if ok { - return v.(*models.Device), nil - } - return nil, fmt.Errorf("the device[%s] is not found in cache", deviceID) -} - -func (s *DeviceService) getDeviceConnector(deviceID string) (models.DeviceConnector, error) { - v, ok := s.deviceConnectors.Load(deviceID) - if ok { - return v.(models.DeviceConnector), nil - } - return nil, fmt.Errorf("the device[%s] is not activated", deviceID) -} diff --git a/pkg/startup/device_service/handle_operations_device.go b/pkg/startup/device_service/handle_operations_device.go deleted file mode 100644 index 6915748..0000000 --- a/pkg/startup/device_service/handle_operations_device.go +++ /dev/null @@ -1,169 +0,0 @@ -package startup - -import ( - "fmt" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -// publishingDeviceData tries to publish data in the bus into the MessageBus. -func (s *DeviceService) publishingDeviceData() { - s.wg.Add(1) - - go func() { - defer s.wg.Done() - - for { - select { - case data := <-s.bus: - if err := s.doc.Publish(data); err != nil { - s.logger.WithError(err).Errorf("fail to publish the device data %+v", data) - } else { - s.logger.Debugf("success to publish the device data %+v", data) - } - case <-s.ctx.Done(): - break - } - } - }() -} - -// TODO 是否可以将 DeviceDataHandler 转移到 DeviceServiceDeviceDataOperationClient 中? - -type DeviceDataHandler func(product *models.Product, device *models.Device, conn models.DeviceConnector, - dataID string, fields map[string]interface{}) error - -func (s *DeviceService) handlingDeviceData() { - s.wg.Add(1) - - s.unsupportedDeviceDataTypes = map[models.DeviceDataOperation]struct{}{ - models.DeviceDataOperationRead: {}, // property read - models.DeviceDataOperationEvent: {}, // event - models.DeviceDataOperationResponse: {}, // method response - models.DeviceDataOperationError: {}, // method error - } - s.deviceDataHandlers = map[models.DeviceDataOperation]DeviceDataHandler{ - models.DeviceDataOperationWrite: s.handleWriteData, // property write - models.DeviceDataOperationRequest: s.handleRequestData, // method request - } - - go func() { - defer s.wg.Done() - - topic := bus.TopicTypeDeviceData.Topic() - if err := s.doc.Subscribe(s.handleDeviceData, topic); err != nil { - s.logger.WithError(err).Errorf("fail to subscribe the topic: %s", topic) - return - } - }() -} - -func (s *DeviceService) handleDeviceData(msg *bus.Message) { - tags, fields, err := msg.Parse() - if err != nil { - s.logger.WithError(err).Errorf("fail to parse the message[%s]", msg.ToString()) - return - } - - productID, deviceID, optType, dataID := tags[0], tags[1], tags[2], tags[3] - product, err := s.getProduct(productID) - if err != nil { - s.logger.Error(err.Error()) - return - } - device, err := s.getDevice(deviceID) - if err != nil { - s.logger.Error(err.Error()) - return - } - connector, err := s.getDeviceConnector(deviceID) - if err != nil { - s.logger.Error(err.Error()) - return - } - - if _, ok := s.unsupportedDeviceDataTypes[optType]; ok { - return - } - handler, ok := s.deviceDataHandlers[optType] - if !ok { - s.logger.Errorf("unsupported operation type: %s", optType) - return - } - if err = handler(product, device, connector, dataID, fields); err != nil { - s.logger.WithError(err).Errorf("fail to handle the message: %+v", msg) - return - } -} - -// handleWriteData is responsible for handling the write request forwarded by the device manager. -// The fields will be written into the real device finally. -// -// This handler could be tested as follows: -// 1. Send the specified format data to the message bus: -// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/write/float" -m "{\"intf\": 100}" -// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Write("randnum_test01", "randnum_test01", "float", 100) -// 2. Observe the log of device service. -func (s *DeviceService) handleWriteData(product *models.Product, device *models.Device, conn models.DeviceConnector, - propertyID string, fields map[string]interface{}) error { - properties := map[models.ProductPropertyID]*models.ProductProperty{} - for _, property := range product.Properties { - properties[property.Id] = property - } - - var written interface{} - if propertyID == models.DeviceDataMultiPropsID { - tmp := make(map[models.ProductPropertyID]interface{}, len(fields)) - for k, v := range fields { - property, ok := properties[k] - if !ok { - s.logger.Errorf("undefined property: %s", propertyID) - continue - } - if !property.Writeable { - s.logger.Errorf("the property[%s] is read only", propertyID) - continue - } - tmp[k] = v - } - written = tmp - } else { - property, ok := properties[propertyID] - if !ok { - return fmt.Errorf("undefined property: %s", propertyID) - } - if !property.Writeable { - return fmt.Errorf("the property[%s] is read only", propertyID) - } - - v, ok := fields[propertyID] - if !ok { - return fmt.Errorf("the property[%s]'s value is missed", propertyID) - } - written = map[models.ProductPropertyID]interface{}{propertyID: v} - } - return conn.Write(propertyID, written) -} - -// handleRequestData is responsible for handling the method's request forwarded by the device manager. -// The fields will be expanded a request, and -// -// This handler could be tested as follows: -// 1. Send the specified format data to the message bus: -// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/request/Intn" -m "{\"n\": 100}" -// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Call("randnum_test01", "randnum_test01", "Intn", map[string]interface{}{"n": 100}) -// 2. Observe the log of device service and subscribe the specified topic: -// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/response/Intn". -func (s *DeviceService) handleRequestData(product *models.Product, device *models.Device, conn models.DeviceConnector, - methodID string, fields map[string]interface{}) error { - request := models.NewDeviceData(product.ID, device.ID, models.DeviceDataOperationRequest, methodID) - request.SetFields(fields) - response, err := conn.Call(methodID, *request) - if err != nil { - return err - } - if err = s.doc.Publish(response); err != nil { - return err - } - return nil -} diff --git a/pkg/startup/device_service/handle_operations_meta.go b/pkg/startup/device_service/handle_operations_meta.go deleted file mode 100644 index a7a0ef9..0000000 --- a/pkg/startup/device_service/handle_operations_meta.go +++ /dev/null @@ -1,119 +0,0 @@ -package startup - -import ( - "github.com/thingio/edge-device-sdk/pkg/models" - "os" -) - -// registerProtocol tries to register the protocol to the device manager. -func (s *DeviceService) registerProtocol() { - if err := s.moc.RegisterProtocol(s.protocol); err != nil { - s.logger.WithError(err).Errorf("fail to register the protocol[%s] "+ - "to the device manager", s.protocol.ID) - os.Exit(1) - } - s.logger.Infof("success to register the protocol[%s] to the device manager", s.protocol.ID) -} - -// unregisterProtocol tries to unregister the protocol from the device manager. -func (s *DeviceService) unregisterProtocol() { - if err := s.moc.UnregisterProtocol(s.protocol.ID); err != nil { - s.logger.WithError(err).Errorf("fail to unregister the protocol[%s] "+ - "from the device manager", s.protocol.ID) - } - s.logger.Infof("success to unregister the protocol[%s] from the device manager", s.protocol.ID) -} - -// activateDevices tries to activate all devices. -func (s *DeviceService) activateDevices() { - products, err := s.moc.ListProducts(s.protocol.ID) - if err != nil { - s.logger.WithError(err).Error("fail to fetch products from the device manager") - os.Exit(1) - } - for _, product := range products { - devices, err := s.moc.ListDevices(product.ID) - if err != nil { - s.logger.WithError(err).Error("fail to fetch devices from the device manager") - os.Exit(1) - } - - s.products.Store(product.ID, product) - for _, device := range devices { - if err := s.activateDevice(device); err != nil { - s.logger.WithError(err).Errorf("fail to activate the device[%s]", device.ID) - continue - } - s.devices.Store(device.ID, device) - } - } -} - -// activateDevice is responsible for establishing the connection with the real device. -func (s *DeviceService) activateDevice(device *models.Device) error { - if connector, _ := s.getDeviceConnector(device.ID); connector != nil { // the device has been activated - _ = s.deactivateDevice(device.ID) - } - - // build, initialize and start connector - product, err := s.getProduct(device.ProductID) - if err != nil { - return err - } - connector, err := s.connectorBuilder(product, device) - if err != nil { - return err - } - if err := connector.Initialize(s.logger); err != nil { - s.logger.WithError(err).Error("fail to initialize the random device connector") - return err - } - if err := connector.Start(); err != nil { - s.logger.WithError(err).Error("fail to start the random device connector") - return err - } - - if len(product.Properties) != 0 { - if err := connector.Watch(s.bus); err != nil { - s.logger.WithError(err).Error("fail to watch properties for the device") - return err - } - } - - for _, event := range product.Events { - if err := connector.Subscribe(event.Id, s.bus); err != nil { - s.logger.WithError(err).Errorf("fail to subscribe the product event[%s]", event.Id) - continue - } - } - - s.deviceConnectors.Store(device.ID, connector) - s.logger.Infof("success to activate the device[%s]", device.ID) - return nil -} - -// deactivateDevices tries to deactivate all devices. -func (s *DeviceService) deactivateDevices() { - s.deviceConnectors.Range(func(key, value interface{}) bool { - deviceID := key.(string) - if err := s.deactivateDevice(deviceID); err != nil { - s.logger.WithError(err).Errorf("fail to deactivate the device[%s]", deviceID) - } - return true - }) -} - -// deactivateDevice is responsible for breaking up the connection with the real device. -func (s *DeviceService) deactivateDevice(deviceID string) error { - connector, _ := s.getDeviceConnector(deviceID) - if connector == nil { - return nil - } - if err := connector.Stop(false); err != nil { - return err - } - - s.deviceConnectors.Delete(deviceID) - s.logger.Infof("success to deactivate the device[%s]", deviceID) - return nil -} diff --git a/pkg/startup/device_service/startup.go b/pkg/startup/device_service/startup.go deleted file mode 100644 index 1a7e113..0000000 --- a/pkg/startup/device_service/startup.go +++ /dev/null @@ -1,14 +0,0 @@ -package startup - -import ( - "context" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func Startup(protocol *models.Protocol, builder models.ConnectorBuilder) { - ctx, cancel := context.WithCancel(context.Background()) - - ds := &DeviceService{} - ds.Initialize(ctx, cancel, protocol, builder) - ds.Serve() -} diff --git a/pkg/startup/startup.go b/pkg/startup/startup.go new file mode 100644 index 0000000..43612ae --- /dev/null +++ b/pkg/startup/startup.go @@ -0,0 +1,18 @@ +package startup + +import ( + "context" + "github.com/thingio/edge-device-driver/internal/driver" + "github.com/thingio/edge-device-std/models" +) + +func Startup(protocol *models.Protocol, builder models.DeviceTwinBuilder) { + ctx, cancel := context.WithCancel(context.Background()) + + ds, err := driver.NewDeviceDriver(ctx, cancel, protocol, builder) + if err != nil { + panic(err) + } + ds.Initialize() + ds.Serve() +} diff --git a/version/version.go b/version/version.go deleted file mode 100644 index 57bfd1d..0000000 --- a/version/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package version - -const Version = "v1" From 63d50dfc21062665c54e241eb1bad247d97fe6fa Mon Sep 17 00:00:00 2001 From: xufangyou Date: Thu, 25 Nov 2021 11:52:21 +0800 Subject: [PATCH 12/12] refactor code --- README.md | 2 +- config/config.go | 140 ----------- config/message_bus_options.go | 29 --- docs/zh/README.md | 8 +- go.mod | 13 +- go.sum | 3 +- internal/driver/device_service.go | 162 ++++++++++++ internal/driver/handle_operations_device.go | 233 ++++++++++++++++++ internal/driver/handle_operations_meta.go | 142 +++++++++++ internal/message_bus/bus.go | 193 --------------- internal/message_bus/data.go | 49 ---- internal/message_bus/data_meta.go | 81 ------ internal/message_bus/message.go | 34 --- internal/message_bus/topic.go | 141 ----------- internal/operations/client_manager.go | 112 --------- internal/operations/client_service.go | 94 ------- internal/operations/device_data_publish.go | 7 - internal/operations/device_data_subscribe.go | 9 - internal/operations/device_event_receive.go | 34 --- internal/operations/device_method_call.go | 17 -- internal/operations/device_property_read.go | 34 --- internal/operations/device_property_write.go | 21 -- internal/operations/meta_device_list.go | 97 -------- internal/operations/meta_message.go | 31 --- internal/operations/meta_product_list.go | 102 -------- internal/operations/meta_protocol_register.go | 100 -------- .../operations/meta_protocol_unregister.go | 99 -------- logger/logger.go | 110 --------- pkg/models/data_converter.go | 35 --- pkg/models/data_device.go | 101 -------- pkg/models/device.go | 19 -- pkg/models/device_connector.go | 37 --- pkg/models/meta_loader.go | 104 -------- pkg/models/meta_operator.go | 6 - pkg/models/product.go | 55 ----- pkg/models/property.go | 37 --- pkg/models/protocol.go | 16 -- pkg/startup/device_manager/device_manager.go | 88 ------- .../device_manager/handle_operations_meta.go | 72 ------ pkg/startup/device_manager/startup.go | 14 -- pkg/startup/device_service/device_service.go | 137 ---------- .../handle_operations_device.go | 169 ------------- .../device_service/handle_operations_meta.go | 119 --------- pkg/startup/device_service/startup.go | 14 -- pkg/startup/startup.go | 18 ++ version/version.go | 3 - 46 files changed, 566 insertions(+), 2575 deletions(-) delete mode 100644 config/config.go delete mode 100644 config/message_bus_options.go create mode 100644 internal/driver/device_service.go create mode 100644 internal/driver/handle_operations_device.go create mode 100644 internal/driver/handle_operations_meta.go delete mode 100644 internal/message_bus/bus.go delete mode 100644 internal/message_bus/data.go delete mode 100644 internal/message_bus/data_meta.go delete mode 100644 internal/message_bus/message.go delete mode 100644 internal/message_bus/topic.go delete mode 100644 internal/operations/client_manager.go delete mode 100644 internal/operations/client_service.go delete mode 100644 internal/operations/device_data_publish.go delete mode 100644 internal/operations/device_data_subscribe.go delete mode 100644 internal/operations/device_event_receive.go delete mode 100644 internal/operations/device_method_call.go delete mode 100644 internal/operations/device_property_read.go delete mode 100644 internal/operations/device_property_write.go delete mode 100644 internal/operations/meta_device_list.go delete mode 100644 internal/operations/meta_message.go delete mode 100644 internal/operations/meta_product_list.go delete mode 100644 internal/operations/meta_protocol_register.go delete mode 100644 internal/operations/meta_protocol_unregister.go delete mode 100644 logger/logger.go delete mode 100644 pkg/models/data_converter.go delete mode 100644 pkg/models/data_device.go delete mode 100644 pkg/models/device.go delete mode 100644 pkg/models/device_connector.go delete mode 100644 pkg/models/meta_loader.go delete mode 100644 pkg/models/meta_operator.go delete mode 100644 pkg/models/product.go delete mode 100644 pkg/models/property.go delete mode 100644 pkg/models/protocol.go delete mode 100644 pkg/startup/device_manager/device_manager.go delete mode 100644 pkg/startup/device_manager/handle_operations_meta.go delete mode 100644 pkg/startup/device_manager/startup.go delete mode 100644 pkg/startup/device_service/device_service.go delete mode 100644 pkg/startup/device_service/handle_operations_device.go delete mode 100644 pkg/startup/device_service/handle_operations_meta.go delete mode 100644 pkg/startup/device_service/startup.go create mode 100644 pkg/startup/startup.go delete mode 100644 version/version.go diff --git a/README.md b/README.md index fa1f171..6c4a8ec 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# edge-device-sdk \ No newline at end of file +# edge-device-driver \ No newline at end of file diff --git a/config/config.go b/config/config.go deleted file mode 100644 index c191cc3..0000000 --- a/config/config.go +++ /dev/null @@ -1,140 +0,0 @@ -package config - -import ( - "fmt" - "github.com/jinzhu/configor" - "os" - "reflect" - "strconv" - "strings" - "time" -) - -func init() { - if _, err := LoadConfiguration(); err != nil { - panic("fail to load configuration") - } -} - -const ( - ConfigurationPath = "./resources/configuration.yaml" // xxx/resource/configuration.yaml - ProtocolsPath = "./resources/protocols" // xxx-device-conn/cmd/resources/protocols - ProductsPath = "./resources/products" // edge-device-manager/resources/products - DevicesPath = "./resources/devices" // edge-device-manager/resources/devices - - EnvSep = "," -) - -var C *Configuration - -type Configuration struct { - MessageBus MessageBusOptions `json:"message_bus" yaml:"message_bus"` -} - -func LoadConfiguration() (*Configuration, error) { - C = new(Configuration) - - path := os.Getenv("CONFIG_FILE") - if path == "" { - path = ConfigurationPath - } - if err := configor.Load(C, path); err != nil { - return nil, fmt.Errorf("failed to load configuration file, got %s", err.Error()) - } - - if value := reflect.ValueOf(C).Elem().FieldByName("MessageBus"); value.IsValid() { - m := value.Interface().(MessageBusOptions) - LoadMessageBusOptions(&m) - value.Set(reflect.ValueOf(m)) - } - return C, nil -} - -func LoadEnv(target *string, env string) { - value := os.Getenv(env) - if value != "" { - *target = value - } -} - -// LoadEnvs will read the values of given environments, -// then overwrite the pointer if value is not empty. -func LoadEnvs(envs map[string]interface{}) error { - var err error - for env, target := range envs { - value := os.Getenv(env) - if value == "" { - continue - } - switch target.(type) { - case *string: - *(target.(*string)) = value - case *[]string: - values := strings.Split(value, EnvSep) - result := make([]string, 0) - for _, v := range values { - if v != "" { - result = append(result, v) - } - } - if len(result) != 0 { - *(target.(*[]string)) = result - } - case *int: - *(target.(*int)), err = strconv.Atoi(value) - case *bool: - *(target.(*bool)), err = strconv.ParseBool(value) - case *int64: - *(target.(*int64)), err = strconv.ParseInt(value, 10, 64) - case *float32: - if v, err := strconv.ParseFloat(value, 32); err == nil { - *(target.(*float32)) = float32(v) - } - case *float64: - *(target.(*float64)), err = strconv.ParseFloat(value, 64) - case *time.Duration: - *(target.(*time.Duration)), err = time.ParseDuration(value) - default: - return fmt.Errorf("unsupported env type : %T", target) - } - if err != nil { - return fmt.Errorf("fail to load environments, got %s", err.Error()) - } - } - return nil -} - -func LoadEnvList(target *[]string, env string) { - value := os.Getenv(env) - values := strings.Split(value, EnvSep) - result := make([]string, 0) - for _, v := range values { - if v != "" { - result = append(result, v) - } - } - if len(result) != 0 { - *target = result - } -} - -func LoadEnvBool(target *bool, env string) { - value := os.Getenv(env) - if value != "" { - *target, _ = strconv.ParseBool(value) - } -} - -func LoadEnvInt(target *int, env string) { - value := os.Getenv(env) - if value != "" { - *target, _ = strconv.Atoi(value) - } -} - -func LoadEnvInt64(target *int64, env string) { - value := os.Getenv(env) - if value != "" { - *target, _ = strconv.ParseInt(value, 10, 64) - } -} diff --git a/config/message_bus_options.go b/config/message_bus_options.go deleted file mode 100644 index 2d49828..0000000 --- a/config/message_bus_options.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -func LoadMessageBusOptions(options *MessageBusOptions) { - LoadEnv(&options.Host, "MESSAGE_BUS_HOST") - LoadEnvInt(&options.Port, "MESSAGE_BUS_PORT") - LoadEnv(&options.Protocol, "MESSAGE_BUS_PROTOCOL") - LoadEnvInt(&options.ConnectTimoutMillisecond, "MESSAGE_BUS_CONNECT_TIMEOUT_MS") - LoadEnvInt(&options.TimeoutMillisecond, "MESSAGE_BUS_TIMEOUT_MS") - LoadEnvInt(&options.QoS, "MESSAGE_BUS_QOS") - LoadEnvBool(&options.CleanSession, "MESSAGE_BUS_CLEAN_SESSION") -} - -type MessageBusOptions struct { - // Host is the hostname or IP address of the MQTT broker. - Host string `json:"host" yaml:"host"` - // Port is the port of the MQTT broker. - Port int `json:"port" yaml:"port"` - // Protocol is the protocol to use when communicating with the MQTT broker, such as "tcp". - Protocol string `json:"protocol" yaml:"protocol"` - - // ConnectTimoutMillisecond indicates the timeout of connecting to the MQTT broker. - ConnectTimoutMillisecond int `json:"connect_timout_millisecond" yaml:"connect_timout_millisecond"` - // TimeoutMillisecond indicates the timeout of manipulations. - TimeoutMillisecond int `json:"timeout_millisecond" yaml:"timeout_millisecond"` - // QoS is the abbreviation of MQTT Quality of Service. - QoS int `json:"qos" yaml:"qos"` - // CleanSession indicates whether retain messages after reconnecting for QoS1 and QoS2. - CleanSession bool `json:"clean_session" yaml:"clean_session"` -} diff --git a/docs/zh/README.md b/docs/zh/README.md index 9e25f79..5665784 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -70,10 +70,10 @@ ### 元数据操作 -对于元数据增删改查等操作来说,Topic 格式为 `{Version}/META/{MetaType}/{MethodType}/{MethodMode}/{DataID}`: +对于元数据增删改查等操作来说,Topic 格式为 `{Version}/META/{MetaType}/{OptType}/{OptMode}/{DataID}`: - `Version`:SDK 版本; - `MetaType`:元数据类型,可选 `protocol | product | device`; -- `MethodType`:调用方法,可选 `create | update | delete | get | list`,对于不同的元数据类型,可选范围是不同的; -- `MethodMode`:数据类型,可选 `request | response | error`; -- `DataID`:当前数据的 UUID,特别地,对于同一个方法,其 `request` 与 `response` 的 `DataID` 是相同的。 \ No newline at end of file +- `OptType`:操作类型,可选 `create | update | delete | get | list`,对于不同的元数据类型,可选范围是不同的; +- `OptMode`:操作模式,可选 `request | response`; +- `DataID`:当前元数据操作的 UUID,特别地,对于同一个方法,其 `request` 与 `response` 的 `DataID` 是相同的。 \ No newline at end of file diff --git a/go.mod b/go.mod index d296633..0efdd39 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,7 @@ -module github.com/thingio/edge-device-sdk +module github.com/thingio/edge-device-driver -go 1.16 +replace github.com/thingio/edge-device-std => ./../edge-device-std + +require github.com/thingio/edge-device-std v0.0.0 -require ( - github.com/eclipse/paho.mqtt.golang v1.3.5 - github.com/jinzhu/configor v1.2.1 - github.com/sirupsen/logrus v1.8.1 - gopkg.in/yaml.v2 v2.4.0 -) +go 1.16 diff --git a/go.sum b/go.sum index 03a304e..b2b6df8 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,5 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/driver/device_service.go b/internal/driver/device_service.go new file mode 100644 index 0000000..a96f2ed --- /dev/null +++ b/internal/driver/device_service.go @@ -0,0 +1,162 @@ +package driver + +import ( + "context" + "errors" + "fmt" + "github.com/thingio/edge-device-std/config" + "github.com/thingio/edge-device-std/logger" + "github.com/thingio/edge-device-std/models" + bus "github.com/thingio/edge-device-std/msgbus" + "github.com/thingio/edge-device-std/operations" + "github.com/thingio/edge-device-std/version" + "os" + "sync" +) + +func NewDeviceDriver(ctx context.Context, cancel context.CancelFunc, + protocol *models.Protocol, dtBuilder models.DeviceTwinBuilder) (*DeviceDriver, error) { + if protocol == nil { + return nil, errors.New("the product cannot be nil") + } + if dtBuilder == nil { + return nil, errors.New("please implement and specify the connector builder") + } + dd := &DeviceDriver{ + ID: protocol.ID, + Name: protocol.Name, + Version: version.Version, + + protocol: protocol, + dtBuilder: dtBuilder, + + products: sync.Map{}, + devices: sync.Map{}, + deviceConnectors: sync.Map{}, + + ctx: ctx, + cancel: cancel, + wg: sync.WaitGroup{}, + logger: logger.NewLogger(), + } + + dd.unsupportedDataOperations = map[models.DataOperationType]struct{}{ + models.DataOperationTypeHealthCheckPong: {}, // device health check pong + models.DataOperationTypeSoftReadRsp: {}, // property soft read response + models.DataOperationTypeHardReadRsp: {}, // property hard read response + models.DataOperationTypeWriteRsp: {}, // property write response + models.DataOperationTypeWatch: {}, // property watch + models.DataOperationTypeEvent: {}, // event + models.DataOperationTypeResponse: {}, // method response + models.DataOperationTypeError: {}, // method error + } + dd.dataOperationHandlers = map[models.DataOperationType]DeviceOperationHandler{ + models.DataOperationTypeHealthCheckPing: dd.handleHealthCheck, // device health check ping + models.DataOperationTypeSoftReadReq: dd.handleSoftRead, // property soft read + models.DataOperationTypeHardReadReq: dd.handleHardRead, // property hard read + models.DataOperationTypeWriteReq: dd.handleWrite, // property write + models.DataOperationTypeRequest: dd.handleRequest, // method request + } + return dd, nil +} + +type DeviceDriver struct { + // driver information + ID string + Name string + Version string + + // driver + protocol *models.Protocol + dtBuilder models.DeviceTwinBuilder + + // caches + products sync.Map + devices sync.Map + deviceConnectors sync.Map + unsupportedDataOperations map[models.DataOperationType]struct{} + dataOperationHandlers map[models.DataOperationType]DeviceOperationHandler + + // operation clients + bus chan models.DataOperation + moc operations.MetaOperationDriverClient // wrap the message bus to manipulate + doc operations.DataOperationDriverClient // warp the message bus to manipulate device data + + // lifetime control variables for the device driver + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + logger *logger.Logger +} + +func (d *DeviceDriver) Initialize() { + d.initializeOperationClients() +} + +func (d *DeviceDriver) initializeOperationClients() { + d.bus = make(chan models.DataOperation, 100) + + mb, err := bus.NewMessageBus(&config.C.MessageBus, d.logger) + if err != nil { + d.logger.WithError(err).Error("fail to initialize the message bus") + os.Exit(1) + } + if err = mb.Connect(); err != nil { + d.logger.WithError(err).Error("fail to connect to the message bus") + os.Exit(1) + } + + moc, err := operations.NewMetaOperationDriverClient(mb, d.logger) + if err != nil { + d.logger.WithError(err).Error("fail to initialize the meta operation client for the device driver") + os.Exit(1) + } + d.moc = moc + doc, err := operations.NewDataOperationDriverClient(mb, d.logger) + if err != nil { + d.logger.WithError(err).Error("fail to initialize the device data operation client for the device driver") + os.Exit(1) + } + d.doc = doc +} + +func (d *DeviceDriver) Serve() { + defer d.Stop(false) + + d.registerProtocol() + defer d.unregisterProtocol() + + d.activateDevices() + defer d.deactivateDevices() + + d.publishingDeviceOperation() + d.handlingDeviceOperation() + + d.wg.Wait() +} + +func (d *DeviceDriver) Stop(force bool) {} + +func (d *DeviceDriver) getProduct(productID string) (*models.Product, error) { + v, ok := d.products.Load(productID) + if ok { + return v.(*models.Product), nil + } + return nil, fmt.Errorf("the product[%s] is not found in cache", productID) +} + +func (d *DeviceDriver) getDevice(deviceID string) (*models.Device, error) { + v, ok := d.devices.Load(deviceID) + if ok { + return v.(*models.Device), nil + } + return nil, fmt.Errorf("the device[%s] is not found in cache", deviceID) +} + +func (d *DeviceDriver) getDeviceConnector(deviceID string) (models.DeviceTwin, error) { + v, ok := d.deviceConnectors.Load(deviceID) + if ok { + return v.(models.DeviceTwin), nil + } + return nil, fmt.Errorf("the device[%s] is not activated", deviceID) +} diff --git a/internal/driver/handle_operations_device.go b/internal/driver/handle_operations_device.go new file mode 100644 index 0000000..7a27599 --- /dev/null +++ b/internal/driver/handle_operations_device.go @@ -0,0 +1,233 @@ +package driver + +import ( + "fmt" + "github.com/thingio/edge-device-std/models" + "github.com/thingio/edge-device-std/msgbus/message" +) + +// publishingDeviceOperation tries to publish data in the bus into the MQTTMessageBus. +func (d *DeviceDriver) publishingDeviceOperation() { + d.wg.Add(1) + + go func() { + defer d.wg.Done() + + for { + select { + case data := <-d.bus: + if err := d.doc.Publish(data); err != nil { + d.logger.WithError(err).Errorf("fail to publish the device data %+v", data) + } else { + d.logger.Debugf("success to publish the device data %+v", data) + } + case <-d.ctx.Done(): + break + } + } + }() +} + +type DeviceOperationHandler func(product *models.Product, device *models.Device, conn models.DeviceTwin, + dataID string, fields map[string]interface{}) error + +func (d *DeviceDriver) handlingDeviceOperation() { + d.wg.Add(1) + + go func() { + defer d.wg.Done() + + topic := models.TopicTypeDataOperation.Topic() + if err := d.doc.Subscribe(d.handleDeviceOperation, topic); err != nil { + d.logger.WithError(err).Errorf("fail to subscribe the topic: %s", topic) + return + } + }() +} + +func (d *DeviceDriver) handleDeviceOperation(msg *message.Message) { + o, err := models.ParseDataOperation(msg) + if err != nil { + d.logger.WithError(err).Errorf("fail to parse the message[%s]", msg.ToString()) + return + } + + productID, deviceID, optType, dataID := o.ProductID, o.DeviceID, o.OptType, o.FuncID + product, err := d.getProduct(productID) + if err != nil { + d.logger.Error(err.Error()) + return + } + device, err := d.getDevice(deviceID) + if err != nil { + d.logger.Error(err.Error()) + return + } + connector, err := d.getDeviceConnector(deviceID) + if err != nil { + d.logger.Error(err.Error()) + return + } + + if _, ok := d.unsupportedDataOperations[optType]; ok { + return + } + handler, ok := d.dataOperationHandlers[optType] + if !ok { + d.logger.Errorf("unsupported operation type: %s", optType) + return + } + if err = handler(product, device, connector, dataID, o.GetFields()); err != nil { + d.logger.WithError(err).Errorf("fail to handle the message: %+v", msg) + return + } +} + +// handleHealthCheck is responsible for handling health check forwarded by the device manager. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/health-check-ping/randnum_test01" -m "{}" +// (b.) Indirectly: invoke the DataOperationManagerClient.Call("randnum_test01", "randnum_test01") +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/health-check-pong/randnum_test01". +func (d *DeviceDriver) handleHealthCheck(product *models.Product, device *models.Device, twin models.DeviceTwin, + _ models.ProductMethodID, _ map[string]interface{}) error { + fields := make(map[string]interface{}) + status, err := twin.HealthCheck() + if err != nil { + fields[models.ProductPropertyStatusDetail] = err.Error() + } + fields[models.ProductPropertyStatus] = status + response := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeHealthCheckPong, device.ID) + response.SetFields(fields) + return d.doc.Publish(*response) +} + +// handleSoftRead is responsible for handling the soft read request forwarded by the device manager. +// It will read fields from cache in the device twin. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/read-req/float" -m "{\"float\": 100}" +// (b.) Indirectly: invoke the DataOperationManagerClient.Read("randnum_test01", "randnum_test01", "float", 100) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/read-rsp/float". +func (d *DeviceDriver) handleSoftRead(product *models.Product, device *models.Device, twin models.DeviceTwin, + propertyID models.ProductPropertyID, _ map[string]interface{}) error { + o := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeSoftReadRsp, propertyID) + values, err := twin.Read(propertyID) + if err != nil { + if values == nil { + values = make(map[string]interface{}) + } + values[models.DeviceTwinError] = err.Error() + } + o.SetFields(values) + return d.doc.Publish(*o) +} + +// handleHardRead is responsible for handling the hard read request forwarded by the device manager. +// It will read fields from the real device. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/hard-read-req/float" -m "{\"float\": 100}" +// (b.) Indirectly: invoke the DataOperationManagerClient.Read("randnum_test01", "randnum_test01", "float", 100) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/hard-read-rsp/float". +func (d *DeviceDriver) handleHardRead(product *models.Product, device *models.Device, twin models.DeviceTwin, + propertyID models.ProductPropertyID, _ map[string]interface{}) error { + o := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeHardReadRsp, propertyID) + values, err := twin.HardRead(propertyID) + if err != nil { + if values == nil { + values = make(map[string]interface{}) + } + values[models.DeviceTwinError] = err.Error() + } + o.SetFields(values) + return d.doc.Publish(*o) +} + +// handleWrite is responsible for handling the write request forwarded by the device manager. +// The fields will be written into the real device finally. +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/write-req/float" -m "{\"float\": 100}" +// (b.) Indirectly: invoke the DataOperationManagerClient.Write("randnum_test01", "randnum_test01", "float", 100) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/write-rsp/float". +func (d *DeviceDriver) handleWrite(product *models.Product, device *models.Device, twin models.DeviceTwin, + propertyID models.ProductPropertyID, fields map[models.ProductPropertyID]interface{}) error { + var err error + o := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeWriteRsp, propertyID) + results := map[string]interface{}{} + if fields, err = checkWriteValidity(product, propertyID, fields); err != nil { + results[models.DeviceTwinError] = err.Error() + } else if err = twin.Write(propertyID, fields); err != nil { + results[models.DeviceTwinError] = err.Error() + } + o.SetFields(results) + return d.doc.Publish(*o) +} + +func checkWriteValidity(product *models.Product, propertyID string, + fields map[models.ProductPropertyID]interface{}) (map[models.ProductPropertyID]interface{}, error) { + properties := map[models.ProductPropertyID]*models.ProductProperty{} + for _, property := range product.Properties { + properties[property.Id] = property + } + + var values map[models.ProductPropertyID]interface{} + if propertyID == models.DeviceDataMultiPropsID { + tmp := make(map[models.ProductPropertyID]interface{}, len(fields)) + for k, v := range fields { + property, ok := properties[k] + if !ok { + return nil, fmt.Errorf("undefined property: %s", propertyID) + } + if !property.Writeable { + return nil, fmt.Errorf("the property[%s] is read only", propertyID) + } + tmp[k] = v + } + values = tmp + } else { + property, ok := properties[propertyID] + if !ok { + return nil, fmt.Errorf("undefined property: %s", propertyID) + } + if !property.Writeable { + return nil, fmt.Errorf("the property[%s] is read only", propertyID) + } + + v, ok := fields[propertyID] + if !ok { + return nil, fmt.Errorf("the property[%s]'d value is missed", propertyID) + } + values = map[models.ProductPropertyID]interface{}{propertyID: v} + } + return values, nil +} + +// handleRequest is responsible for handling the method's request forwarded by the device manager. +// The fields will be expanded a request, and +// +// This handler could be tested as follows: +// 1. Send the specified format data to the message bus: +// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/request/Intn" -m "{\"n\": 100}" +// (b.) Indirectly: invoke the DataOperationManagerClient.Call("randnum_test01", "randnum_test01", "Intn", map[string]interface{}{"n": 100}) +// 2. Observe the log of device driver and subscribe the specified topic: +// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/response/Intn". +func (d *DeviceDriver) handleRequest(product *models.Product, device *models.Device, twin models.DeviceTwin, + methodID models.ProductMethodID, ins map[string]interface{}) error { + outs, err := twin.Call(methodID, ins) + if err != nil { + return err + } + response := models.NewDataOperation(product.ID, device.ID, models.DataOperationTypeResponse, methodID) + response.SetFields(outs) + return d.doc.Publish(*response) +} diff --git a/internal/driver/handle_operations_meta.go b/internal/driver/handle_operations_meta.go new file mode 100644 index 0000000..6c51a0a --- /dev/null +++ b/internal/driver/handle_operations_meta.go @@ -0,0 +1,142 @@ +package driver + +import ( + "github.com/thingio/edge-device-std/config" + "github.com/thingio/edge-device-std/models" + "os" + "time" +) + +// registerProtocol tries to register the protocol to the device manager. +func (d *DeviceDriver) registerProtocol() { + d.wg.Add(1) + + register := func() { + if err := d.moc.RegisterProtocol(d.protocol); err != nil { + d.logger.WithError(err).Errorf("fail to register the protocol[%s] "+ + "to the device manager", d.protocol.ID) + os.Exit(1) + } + d.logger.Infof("success to register the protocol[%s] to the device manager", d.protocol.ID) + } + register() + + go func() { + defer d.wg.Done() + + protocolRegisterInterval := time.Duration(config.C.CommonOptions.ProtocolRegisterIntervalSecond) * time.Second + ticker := time.NewTicker(protocolRegisterInterval) + for { + select { + case <-ticker.C: + register() + case <-d.ctx.Done(): + ticker.Stop() + return + } + } + }() +} + +// unregisterProtocol tries to unregister the protocol from the device manager. +func (d *DeviceDriver) unregisterProtocol() { + if err := d.moc.UnregisterProtocol(d.protocol.ID); err != nil { + d.logger.WithError(err).Errorf("fail to unregister the protocol[%s] "+ + "from the device manager", d.protocol.ID) + } + d.logger.Infof("success to unregister the protocol[%s] from the device manager", d.protocol.ID) +} + +// activateDevices tries to activate all devices. +func (d *DeviceDriver) activateDevices() { + products, err := d.moc.ListProducts(d.protocol.ID) + if err != nil { + d.logger.WithError(err).Error("fail to fetch products from the device manager") + os.Exit(1) + } + for _, product := range products { + devices, err := d.moc.ListDevices(product.ID) + if err != nil { + d.logger.WithError(err).Error("fail to fetch devices from the device manager") + os.Exit(1) + } + + d.products.Store(product.ID, product) + for _, device := range devices { + if err := d.activateDevice(device); err != nil { + d.logger.WithError(err).Errorf("fail to activate the device[%s]", device.ID) + continue + } + d.devices.Store(device.ID, device) + } + } +} + +// activateDevice is responsible for establishing the connection with the real device. +func (d *DeviceDriver) activateDevice(device *models.Device) error { + if connector, _ := d.getDeviceConnector(device.ID); connector != nil { // the device has been activated + _ = d.deactivateDevice(device.ID) + } + + // build, initialize and start connector + product, err := d.getProduct(device.ProductID) + if err != nil { + return err + } + connector, err := d.dtBuilder(product, device) + if err != nil { + return err + } + if err := connector.Initialize(d.logger); err != nil { + d.logger.WithError(err).Error("fail to initialize the random device connector") + return err + } + if err := connector.Start(); err != nil { + d.logger.WithError(err).Error("fail to start the random device connector") + return err + } + + if len(product.Properties) != 0 { + if err := connector.Watch(d.bus); err != nil { + d.logger.WithError(err).Error("fail to watch properties for the device") + return err + } + } + + for _, event := range product.Events { + if err := connector.Subscribe(event.Id, d.bus); err != nil { + d.logger.WithError(err).Errorf("fail to subscribe the product event[%s]", event.Id) + continue + } + } + + d.deviceConnectors.Store(device.ID, connector) + d.logger.Infof("success to activate the device[%s]", device.ID) + return nil +} + +// deactivateDevices tries to deactivate all devices. +func (d *DeviceDriver) deactivateDevices() { + d.deviceConnectors.Range(func(key, value interface{}) bool { + deviceID := key.(string) + if err := d.deactivateDevice(deviceID); err != nil { + d.logger.WithError(err).Errorf("fail to deactivate the device[%s]", deviceID) + } + return true + }) +} + +// deactivateDevice is responsible for breaking up the connection with the real device. +func (d *DeviceDriver) deactivateDevice(deviceID string) error { + connector, _ := d.getDeviceConnector(deviceID) + if connector == nil { + return nil + } + if err := connector.Stop(false); err != nil { + return err + } + + d.deviceConnectors.Delete(deviceID) + d.logger.Infof("success to deactivate the device[%s]", deviceID) + return nil +} diff --git a/internal/message_bus/bus.go b/internal/message_bus/bus.go deleted file mode 100644 index c091a44..0000000 --- a/internal/message_bus/bus.go +++ /dev/null @@ -1,193 +0,0 @@ -package bus - -import ( - "fmt" - mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/thingio/edge-device-sdk/config" - "github.com/thingio/edge-device-sdk/logger" - "strconv" - "time" -) - -func NewMessageBus(opts *config.MessageBusOptions, logger *logger.Logger) (MessageBus, error) { - mb := messageBus{ - timeout: time.Millisecond * time.Duration(opts.TimeoutMillisecond), - qos: opts.QoS, - routes: make(map[string]MessageHandler), - logger: logger, - } - if err := mb.setClient(opts); err != nil { - return nil, err - } - return &mb, nil -} - -// MessageBus encapsulates all common manipulations based on MQTT. -type MessageBus interface { - IsConnected() bool - - Connect() error - - Disconnect() error - - Publish(data Data) error - - Subscribe(handler MessageHandler, topics ...string) error - - Unsubscribe(topics ...string) error - - Call(request Data) (response Data, err error) -} - -type messageBus struct { - client mqtt.Client - timeout time.Duration - qos int - - routes map[string]MessageHandler // topic -> handler - - logger *logger.Logger -} - -func (mb *messageBus) IsConnected() bool { - return mb.client.IsConnected() -} - -func (mb *messageBus) Connect() error { - if mb.IsConnected() { - return nil - } - - token := mb.client.Connect() - return mb.handleToken(token) -} - -func (mb *messageBus) Disconnect() error { - if mb.IsConnected() { - mb.client.Disconnect(2000) // waiting 2s - } - return nil -} - -func (mb *messageBus) Publish(data Data) error { - msg, err := data.ToMessage() - if err != nil { - return err - } - - token := mb.client.Publish(msg.Topic, byte(mb.qos), false, msg.Payload) - return mb.handleToken(token) -} - -func (mb *messageBus) Subscribe(handler MessageHandler, topics ...string) error { - filters := make(map[string]byte) - for _, topic := range topics { - mb.routes[topic] = handler - filters[topic] = byte(mb.qos) - } - callback := func(mc mqtt.Client, msg mqtt.Message) { - go handler(&Message{ - Topic: msg.Topic(), - Payload: msg.Payload(), - }) - } - - token := mb.client.SubscribeMultiple(filters, callback) - return mb.handleToken(token) -} - -func (mb *messageBus) Unsubscribe(topics ...string) error { - for _, topic := range topics { - delete(mb.routes, topic) - } - - token := mb.client.Unsubscribe(topics...) - return mb.handleToken(token) -} - -func (mb *messageBus) Call(request Data) (response Data, err error) { - response, err = request.Response() - if err != nil { - return nil, err - } - - // subscribe response - rspMsg, err := response.ToMessage() - if err != nil { - return nil, err - } - ch := make(chan *Message, 1) - rspTpc := rspMsg.Topic - if err = mb.Subscribe(func(msg *Message) { - ch <- msg - }, rspTpc); err != nil { - return nil, err - } - defer func() { - close(ch) - _ = mb.Unsubscribe(rspTpc) - }() - - // publish request - if err = mb.Publish(request); err != nil { - return nil, err - } - // waiting for the response - ticker := time.NewTicker(mb.timeout) - select { - case msg := <-ch: - _, fields, err := msg.Parse() - if err != nil { - return nil, fmt.Errorf("fail to parse the message: %s, got %s", - msg.ToString(), err.Error()) - } - response.SetFields(fields) - return response, nil - case <-ticker.C: - ticker.Stop() - return nil, fmt.Errorf("call timeout: %dms", mb.timeout/time.Millisecond) - } -} - -func (mb *messageBus) handleToken(token mqtt.Token) error { - if mb.timeout > 0 { - token.WaitTimeout(mb.timeout) - } else { - token.Wait() - } - return token.Error() -} - -func (mb *messageBus) setClient(options *config.MessageBusOptions) error { - opts := mqtt.NewClientOptions() - clientID := "edge-device-sub-" + strconv.FormatInt(time.Now().UnixNano(), 10) - mb.logger.Infof("the ID of client for the message bus is %s", clientID) - opts.SetClientID(clientID) - opts.AddBroker(fmt.Sprintf("%s://%s:%d", options.Protocol, options.Host, options.Port)) - opts.SetConnectTimeout(time.Duration(options.ConnectTimoutMillisecond) * time.Millisecond) - opts.SetKeepAlive(time.Minute) - opts.SetAutoReconnect(true) - opts.SetOnConnectHandler(mb.onConnect) - opts.SetConnectionLostHandler(mb.onConnectLost) - opts.SetCleanSession(options.CleanSession) - - mb.client = mqtt.NewClient(opts) - return nil -} - -func (mb *messageBus) onConnect(mc mqtt.Client) { - reader := mc.OptionsReader() - mb.logger.Infof("the connection with %s for the message bus has been established.", reader.Servers()[0].String()) - - for tpc, hdl := range mb.routes { - if err := mb.Subscribe(hdl, tpc); err != nil { - mb.logger.WithError(err).Errorf("fail to resubscribe the topic: %s", tpc) - } - } -} - -func (mb *messageBus) onConnectLost(mc mqtt.Client, err error) { - reader := mc.OptionsReader() - mb.logger.WithError(err).Errorf("the connection with %s for the message bus has lost, trying to reconnect.", - reader.Servers()[0].String()) -} diff --git a/internal/message_bus/data.go b/internal/message_bus/data.go deleted file mode 100644 index 3e9103e..0000000 --- a/internal/message_bus/data.go +++ /dev/null @@ -1,49 +0,0 @@ -package bus - -import ( - "errors" -) - -type Data interface { - SetFields(fields map[string]interface{}) - GetFields() map[string]interface{} - SetField(key string, value interface{}) - GetField(key string) interface{} - - ToMessage() (*Message, error) - - Response() (response Data, err error) -} - -type MessageData struct { - Fields map[string]interface{} -} - -func (d *MessageData) SetFields(fields map[string]interface{}) { - for key, value := range fields { - d.SetField(key, value) - } -} - -func (d *MessageData) GetFields() map[string]interface{} { - return d.Fields -} - -func (d *MessageData) SetField(key string, value interface{}) { - if d.Fields == nil { - d.Fields = make(map[string]interface{}) - } - d.Fields[key] = value -} - -func (d *MessageData) GetField(key string) interface{} { - return d.Fields[key] -} - -func (d *MessageData) ToMessage() (*Message, error) { - return nil, errors.New("implement me") -} - -func (d *MessageData) Response() (response Data, err error) { - return nil, errors.New("implement me") -} diff --git a/internal/message_bus/data_meta.go b/internal/message_bus/data_meta.go deleted file mode 100644 index 6685e52..0000000 --- a/internal/message_bus/data_meta.go +++ /dev/null @@ -1,81 +0,0 @@ -package bus - -import ( - "encoding/json" - "fmt" -) - -type ( - MetaDataType = string - - MetaDataOperation = string - MetaDataOperationMode = string -) - -const ( - MetaDataTypeProtocol MetaDataType = "protocol" - MetaDataTypeProduct MetaDataType = "product" - MetaDataTypeDevice MetaDataType = "device" - - MetaDataOperationCreate MetaDataOperation = "create" - MetaDataOperationUpdate MetaDataOperation = "update" - MetaDataOperationDelete MetaDataOperation = "delete" - MetaDataOperationGet MetaDataOperation = "get" - MetaDataOperationList MetaDataOperation = "list" - - MetaDataOperationModeRequest MetaDataOperationMode = "request" - MetaDataOperationModeResponse MetaDataOperationMode = "response" -) - -type MetaData struct { - MessageData - - MetaType MetaDataType `json:"meta_type"` - OptType MetaDataOperation `json:"opt_type"` - OptMode MetaDataOperationMode `json:"opt_mode"` - DataID string `json:"data_id"` -} - -func (d *MetaData) ToMessage() (*Message, error) { - topic := NewMetaDataTopic(d.MetaType, d.OptType, d.OptMode, d.DataID) - payload, err := json.Marshal(d.Fields) - if err != nil { - return nil, err - } - - return &Message{ - Topic: topic.String(), - Payload: payload, - }, nil -} - -func (d *MetaData) isRequest() bool { - return d.OptMode == MetaDataOperationModeRequest -} - -func (d *MetaData) Response() (Data, error) { - if !d.isRequest() { - return nil, fmt.Errorf("the device data is not a request: %+v", *d) - } - return NewMetaData(d.MetaType, d.OptType, MetaDataOperationModeResponse, d.DataID), nil -} - -func NewMetaData(metaType MetaDataType, methodType MetaDataOperation, - optMode MetaDataOperationMode, dataID string) *MetaData { - return &MetaData{ - MetaType: metaType, - OptType: methodType, - OptMode: optMode, - DataID: dataID, - } -} - -func ParseMetaData(msg *Message) (*MetaData, error) { - tags, fields, err := msg.Parse() - if err != nil { - return nil, err - } - metaData := NewMetaData(tags[0], tags[1], tags[2], tags[3]) - metaData.SetFields(fields) - return metaData, nil -} diff --git a/internal/message_bus/message.go b/internal/message_bus/message.go deleted file mode 100644 index 110abe4..0000000 --- a/internal/message_bus/message.go +++ /dev/null @@ -1,34 +0,0 @@ -package bus - -import ( - "encoding/json" - "fmt" -) - -type MessageHandler func(msg *Message) - -// Message is an intermediate data format between MQTT and Data. -type Message struct { - Topic string - Payload []byte -} - -func (m *Message) Parse() ([]string, map[string]interface{}, error) { - // parse topic - topic, err := NewTopic(m.Topic) - if err != nil { - return nil, nil, err - } - tagValues := topic.TagValues() - - // parse payload - fields := make(map[string]interface{}) - if err := json.Unmarshal(m.Payload, &fields); err != nil { - return nil, nil, err - } - return tagValues, fields, nil -} - -func (m *Message) ToString() string { - return fmt.Sprintf("%+v", *m) -} diff --git a/internal/message_bus/topic.go b/internal/message_bus/topic.go deleted file mode 100644 index c68ab9f..0000000 --- a/internal/message_bus/topic.go +++ /dev/null @@ -1,141 +0,0 @@ -package bus - -import ( - "fmt" - "github.com/thingio/edge-device-sdk/version" - "strings" -) - -const ( - TagsOffset = 2 -) - -type TopicTagKey string - -type TopicTags map[TopicTagKey]string - -type TopicType string - -// Topic returns the topic that could subscribe all messages belong to this type. -func (t TopicType) Topic() string { - return strings.Join([]string{version.Version, string(t), TopicWildcard}, TopicSep) -} - -const ( - TopicTagKeyMetaDataType TopicTagKey = "meta_type" - TopicTagKeyMetaDataMethodType TopicTagKey = "method_type" - TopicTagKeyMetaDataMethodMode TopicTagKey = "method_mode" - TopicTagKeyProductID TopicTagKey = "product_id" - TopicTagKeyDeviceID TopicTagKey = "device_id" - TopicTagKeyOptType TopicTagKey = "opt_type" - TopicTagKeyDataID TopicTagKey = "data_id" - - TopicTypeMetaData TopicType = "META" - TopicTypeDeviceData TopicType = "DATA" - - TopicSep = "/" - TopicWildcard = "#" -) - -// Schemas describes all topics' forms. Every topic is formed by //.../. -var Schemas = map[TopicType][]TopicTagKey{ - TopicTypeMetaData: {TopicTagKeyMetaDataType, TopicTagKeyMetaDataMethodType, TopicTagKeyMetaDataMethodMode, TopicTagKeyDataID}, - TopicTypeDeviceData: {TopicTagKeyProductID, TopicTagKeyDeviceID, TopicTagKeyOptType, TopicTagKeyDataID}, -} - -type Topic interface { - Type() TopicType - - String() string - - Tags() TopicTags - TagKeys() []TopicTagKey - TagValues() []string - TagValue(key TopicTagKey) (value string, ok bool) -} - -type commonTopic struct { - topicTags TopicTags - topicType TopicType -} - -func (c *commonTopic) Type() TopicType { - return c.topicType -} - -func (c *commonTopic) String() string { - topicType := string(c.topicType) - tagValues := c.TagValues() - return strings.Join(append([]string{version.Version, topicType}, tagValues...), TopicSep) -} - -func (c *commonTopic) Tags() TopicTags { - return c.topicTags -} - -func (c *commonTopic) TagKeys() []TopicTagKey { - return Schemas[c.topicType] -} - -func (c *commonTopic) TagValues() []string { - tagKeys := c.TagKeys() - values := make([]string, len(tagKeys)) - for idx, topicTagKey := range tagKeys { - values[idx] = c.topicTags[topicTagKey] - } - return values -} - -func (c *commonTopic) TagValue(key TopicTagKey) (value string, ok bool) { - tags := c.Tags() - value, ok = tags[key] - return -} - -func NewTopic(topic string) (Topic, error) { - parts := strings.Split(topic, TopicSep) - if len(parts) < TagsOffset { - return nil, fmt.Errorf("invalid topic: %s", topic) - } - topicType := TopicType(parts[1]) - keys, ok := Schemas[topicType] - if !ok { - return nil, fmt.Errorf("undefined topic type: %s", topicType) - } - if len(parts)-TagsOffset != len(keys) { - return nil, fmt.Errorf("invalid topic: %s, keys [%+v] are necessary", topic, keys) - } - - tags := make(map[TopicTagKey]string) - for i, key := range keys { - tags[key] = parts[i+TagsOffset] - } - return &commonTopic{ - topicType: topicType, - topicTags: tags, - }, nil -} - -func NewMetaDataTopic(metaType, methodType, methodMode, dataID string) Topic { - return &commonTopic{ - topicTags: map[TopicTagKey]string{ - TopicTagKeyMetaDataType: metaType, - TopicTagKeyMetaDataMethodType: methodType, - TopicTagKeyMetaDataMethodMode: methodMode, - TopicTagKeyDataID: dataID, - }, - topicType: TopicTypeMetaData, - } -} - -func NewDeviceDataTopic(productID, deviceID, optType, dataID string) Topic { - return &commonTopic{ - topicTags: map[TopicTagKey]string{ - TopicTagKeyProductID: productID, - TopicTagKeyDeviceID: deviceID, - TopicTagKeyOptType: optType, - TopicTagKeyDataID: dataID, - }, - topicType: TopicTypeDeviceData, - } -} diff --git a/internal/operations/client_manager.go b/internal/operations/client_manager.go deleted file mode 100644 index b52b2e1..0000000 --- a/internal/operations/client_manager.go +++ /dev/null @@ -1,112 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func NewDeviceManagerMetaOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerMetaOperationClient, error) { - protocolClient, err := newDeviceManagerProtocolOperationClient(mb, logger) - if err != nil { - return nil, err - } - productClient, err := newDeviceManagerProductOperationClient(mb, logger) - if err != nil { - return nil, err - } - deviceClient, err := newDeviceManagerDeviceOperationClient(mb, logger) - if err != nil { - return nil, err - } - return &deviceManagerMetaOperationClient{ - protocolClient, - productClient, - deviceClient, - }, nil -} - -type DeviceManagerMetaOperationClient interface { - DeviceManagerProtocolOperationClient - DeviceManagerProductOperationClient - DeviceManagerDeviceOperationClient -} -type deviceManagerMetaOperationClient struct { - DeviceManagerProtocolOperationClient - DeviceManagerProductOperationClient - DeviceManagerDeviceOperationClient -} - -func newDeviceManagerProtocolOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerProtocolOperationClient, error) { - return &deviceManagerProtocolOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceManagerProtocolOperationClient interface { - OnRegisterProtocols(register func(protocol *models.Protocol) error) error - OnUnregisterProtocols(unregister func(protocolID string) error) error -} -type deviceManagerProtocolOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceManagerProductOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerProductOperationClient, error) { - return &deviceManagerProductOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceManagerProductOperationClient interface { - OnListProducts(list func(protocolID string) ([]*models.Product, error)) error -} -type deviceManagerProductOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceManagerDeviceOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceManagerDeviceOperationClient, error) { - return &deviceManagerDeviceOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceManagerDeviceOperationClient interface { - OnListDevices(list func(productID string) ([]*models.Device, error)) error -} -type deviceManagerDeviceOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func NewDeviceManagerDeviceDataOperationClient(mb bus.MessageBus, logger *logger.Logger) (DeviceManagerDeviceDataOperationClient, error) { - reads := make(map[models.ProductPropertyID]chan models.DeviceData) - events := make(map[models.ProductEventID]chan models.DeviceData) - return &deviceManagerDeviceDataOperationClient{ - mb: mb, - logger: logger, - reads: reads, - events: events, - }, nil -} - -type DeviceManagerDeviceDataOperationClient interface { - Read(productID, deviceID string, - propertyID models.ProductPropertyID) (dd <-chan models.DeviceData, cc func(), err error) - - Write(productID, deviceID string, - propertyID models.ProductPropertyID, value interface{}) error - - Receive(productID, deviceID string, - eventID models.ProductEventID) (dd <-chan models.DeviceData, cc func(), err error) - - Call(productID, deviceID string, methodID models.ProductMethodID, - req map[string]interface{}) (rsp map[string]interface{}, err error) -} - -type deviceManagerDeviceDataOperationClient struct { - mb bus.MessageBus - logger *logger.Logger - - reads map[string]chan models.DeviceData - events map[string]chan models.DeviceData -} diff --git a/internal/operations/client_service.go b/internal/operations/client_service.go deleted file mode 100644 index 1fc7a52..0000000 --- a/internal/operations/client_service.go +++ /dev/null @@ -1,94 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func NewDeviceServiceMetaOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceMetaOperationClient, error) { - protocolClient, err := newDeviceServiceProtocolOperationClient(mb, logger) - if err != nil { - return nil, err - } - productClient, err := newDeviceServiceProductOperationClient(mb, logger) - if err != nil { - return nil, err - } - deviceClient, err := newDeviceServiceDeviceOperationClient(mb, logger) - if err != nil { - return nil, err - } - return &deviceServiceMetaOperationClient{ - protocolClient, - productClient, - deviceClient, - }, nil -} - -type DeviceServiceMetaOperationClient interface { - DeviceServiceProtocolOperationClient - DeviceServiceProductOperationClient - DeviceServiceDeviceOperationClient -} -type deviceServiceMetaOperationClient struct { - DeviceServiceProtocolOperationClient - DeviceServiceProductOperationClient - DeviceServiceDeviceOperationClient -} - -func newDeviceServiceProtocolOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceProtocolOperationClient, error) { - return &deviceServiceProtocolOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceProtocolOperationClient interface { - RegisterProtocol(protocol *models.Protocol) error - UnregisterProtocol(protocolID string) error -} -type deviceServiceProtocolOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceServiceProductOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceProductOperationClient, error) { - return &deviceServiceProductOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceProductOperationClient interface { - ListProducts(protocolID string) ([]*models.Product, error) -} -type deviceServiceProductOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func newDeviceServiceDeviceOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceDeviceOperationClient, error) { - return &deviceServiceDeviceOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceDeviceOperationClient interface { - ListDevices(productID string) ([]*models.Device, error) -} -type deviceServiceDeviceOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} - -func NewDeviceServiceDeviceDataOperationClient(mb bus.MessageBus, - logger *logger.Logger) (DeviceServiceDeviceDataOperationClient, error) { - return &deviceServiceDeviceDataOperationClient{mb: mb, logger: logger}, nil -} - -type DeviceServiceDeviceDataOperationClient interface { - Publish(data models.DeviceData) error - Subscribe(handler bus.MessageHandler, topic string) error -} - -type deviceServiceDeviceDataOperationClient struct { - mb bus.MessageBus - logger *logger.Logger -} diff --git a/internal/operations/device_data_publish.go b/internal/operations/device_data_publish.go deleted file mode 100644 index 42123c5..0000000 --- a/internal/operations/device_data_publish.go +++ /dev/null @@ -1,7 +0,0 @@ -package operations - -import "github.com/thingio/edge-device-sdk/pkg/models" - -func (c *deviceServiceDeviceDataOperationClient) Publish(data models.DeviceData) error { - return c.mb.Publish(&data) -} diff --git a/internal/operations/device_data_subscribe.go b/internal/operations/device_data_subscribe.go deleted file mode 100644 index 9ad13d1..0000000 --- a/internal/operations/device_data_subscribe.go +++ /dev/null @@ -1,9 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" -) - -func (c *deviceServiceDeviceDataOperationClient) Subscribe(handler bus.MessageHandler, topic string) error { - return c.mb.Subscribe(handler, topic) -} diff --git a/internal/operations/device_event_receive.go b/internal/operations/device_event_receive.go deleted file mode 100644 index 1df31b6..0000000 --- a/internal/operations/device_event_receive.go +++ /dev/null @@ -1,34 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Receive(productID, deviceID string, - eventID models.ProductEventID) (dd <-chan models.DeviceData, cc func(), err error) { - data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationEvent, eventID) - message, err := data.ToMessage() - if err != nil { - return nil, nil, err - } - topic := message.Topic - if _, ok := c.events[eventID]; !ok { - c.events[eventID] = make(chan models.DeviceData, 100) - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - _, fields, _ := msg.Parse() - data.SetFields(fields) - - c.events[eventID] <- *data - }, topic); err != nil { - return nil, nil, err - } - - return c.events[eventID], func() { - if _, ok := c.events[topic]; ok { - close(c.events[topic]) - delete(c.events, topic) - } - }, nil -} diff --git a/internal/operations/device_method_call.go b/internal/operations/device_method_call.go deleted file mode 100644 index f8698cf..0000000 --- a/internal/operations/device_method_call.go +++ /dev/null @@ -1,17 +0,0 @@ -package operations - -import ( - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Call(productID, deviceID string, - methodID models.ProductMethodID, ins map[string]interface{}) (outs map[string]interface{}, err error) { - request := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationRequest, methodID) - request.SetFields(ins) - - response, err := c.mb.Call(request) - if err != nil { - return nil, err - } - return response.GetFields(), nil -} diff --git a/internal/operations/device_property_read.go b/internal/operations/device_property_read.go deleted file mode 100644 index 795b35a..0000000 --- a/internal/operations/device_property_read.go +++ /dev/null @@ -1,34 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Read(productID, deviceID string, - propertyID models.ProductPropertyID) (dd <-chan models.DeviceData, cc func(), err error) { - data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationRead, propertyID) - message, err := data.ToMessage() - if err != nil { - return nil, nil, err - } - topic := message.Topic - if _, ok := c.reads[topic]; !ok { - c.reads[topic] = make(chan models.DeviceData, 100) - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - _, fields, _ := msg.Parse() - data.SetFields(fields) - - c.reads[propertyID] <- *data - }, topic); err != nil { - return nil, nil, err - } - - return c.reads[propertyID], func() { - if _, ok := c.reads[topic]; ok { - close(c.reads[topic]) - delete(c.reads, topic) - } - }, nil -} diff --git a/internal/operations/device_property_write.go b/internal/operations/device_property_write.go deleted file mode 100644 index 5c65fdc..0000000 --- a/internal/operations/device_property_write.go +++ /dev/null @@ -1,21 +0,0 @@ -package operations - -import ( - "fmt" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func (c *deviceManagerDeviceDataOperationClient) Write(productID, deviceID string, - propertyID models.ProductPropertyID, value interface{}) error { - data := models.NewDeviceData(productID, deviceID, models.DeviceDataOperationWrite, propertyID) - if propertyID == models.DeviceDataMultiPropsID { - fields, ok := value.(map[models.ProductPropertyID]interface{}) - if !ok { - return fmt.Errorf("%+v must be type map[models.ProductPropertyID]interface{}", value) - } - data.SetFields(fields) - } else { - data.SetField(propertyID, value) - } - return c.mb.Publish(data) -} diff --git a/internal/operations/meta_device_list.go b/internal/operations/meta_device_list.go deleted file mode 100644 index 45ea166..0000000 --- a/internal/operations/meta_device_list.go +++ /dev/null @@ -1,97 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -type ListDevicesRequest struct { - ProductID string `json:"product_id"` -} - -func (r *ListDevicesRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListDevicesRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type ListDevicesResponse struct { - Devices []*models.Device `json:"devices"` -} - -func (r *ListDevicesResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListDevicesResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnListDevices for the device manager puts devices into the message bus. -func (c *deviceManagerDeviceOperationClient) OnListDevices(list func(productID string) ([]*models.Device, error)) error { - schema := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for listing devices", msg.ToString()) - return - } - req := &ListDevicesRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for listing devices") - return - } - - productID := req.ProductID - devices, err := list(productID) - if err != nil { - c.logger.WithError(err).Error("fail to list devices") - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, - bus.MetaDataOperationModeResponse, productID) - rsp := &ListDevicesResponse{Devices: devices} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for listing devices") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for listing devices") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// ListDevices for the device service takes devices from the message bus. -func (c *deviceServiceDeviceOperationClient) ListDevices(productID string) ([]*models.Device, error) { - request := bus.NewMetaData(bus.MetaDataTypeDevice, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, productID) - fields, err := (&ListDevicesRequest{ProductID: productID}).Marshal() - if err != nil { - return nil, err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return nil, err - } - - rsp := &ListDevicesResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return nil, err - } - return rsp.Devices, nil -} diff --git a/internal/operations/meta_message.go b/internal/operations/meta_message.go deleted file mode 100644 index 7edfa5b..0000000 --- a/internal/operations/meta_message.go +++ /dev/null @@ -1,31 +0,0 @@ -package operations - -import "encoding/json" - -type MetaMessage interface { - Unmarshal(fields map[string]interface{}) error - Marshal() (map[string]interface{}, error) -} - -func Struct2Map(o interface{}) (map[string]interface{}, error) { - data, err := json.Marshal(o) - if err != nil { - return nil, err - } - fields := make(map[string]interface{}) - if err = json.Unmarshal(data, &fields); err != nil { - return nil, err - } - return fields, nil -} - -func Map2Struct(m map[string]interface{}, s interface{}) error { - data, err := json.Marshal(m) - if err != nil { - return err - } - if err = json.Unmarshal(data, s); err != nil { - return err - } - return nil -} diff --git a/internal/operations/meta_product_list.go b/internal/operations/meta_product_list.go deleted file mode 100644 index 3261897..0000000 --- a/internal/operations/meta_product_list.go +++ /dev/null @@ -1,102 +0,0 @@ -package operations - -import ( - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -type ListProductsRequest struct { - ProtocolID string `json:"protocol_id"` -} - -func (r *ListProductsRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListProductsRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type ListProductsResponse struct { - Products []*models.Product `json:"products"` -} - -func (r *ListProductsResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *ListProductsResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnListProducts for the device manager puts products into the message bus. -func (c *deviceManagerProductOperationClient) OnListProducts(list func(protocolID string) ([]*models.Product, error)) error { - schema := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for listing products", - msg.ToString()) - return - } - req := &ListProductsRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for listing products") - return - } - - protocolID := req.ProtocolID - products, err := list(protocolID) - if err != nil { - c.logger.WithError(err).Error("fail to list products") - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, - bus.MetaDataOperationModeResponse, protocolID) - if err != nil { - c.logger.WithError(err).Error("fail to construct response for the request") - return - } - rsp := &ListProductsResponse{Products: products} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for listing products") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for listing products") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// ListProducts for the device service takes products from the message bus. -func (c *deviceServiceProductOperationClient) ListProducts(protocolID string) ([]*models.Product, error) { - request := bus.NewMetaData(bus.MetaDataTypeProduct, bus.MetaDataOperationList, - bus.MetaDataOperationModeRequest, protocolID) - fields, err := (&ListProductsRequest{ProtocolID: protocolID}).Marshal() - if err != nil { - return nil, err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return nil, err - } - - rsp := &ListProductsResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return nil, err - } - return rsp.Products, nil -} diff --git a/internal/operations/meta_protocol_register.go b/internal/operations/meta_protocol_register.go deleted file mode 100644 index 0e49112..0000000 --- a/internal/operations/meta_protocol_register.go +++ /dev/null @@ -1,100 +0,0 @@ -package operations - -import ( - "errors" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -type RegisterProtocolRequest struct { - Protocol models.Protocol `json:"protocol"` -} - -func (r *RegisterProtocolRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *RegisterProtocolRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type RegisterProtocolResponse struct { - Success bool `json:"success"` -} - -func (r *RegisterProtocolResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *RegisterProtocolResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnRegisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) OnRegisterProtocols(register func(protocol *models.Protocol) error) error { - schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for registering protocol", - msg.ToString()) - return - } - req := &RegisterProtocolRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for registering protocol") - return - } - protocol := &req.Protocol - if err := register(protocol); err != nil { - c.logger.Error(err.Error()) - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, - bus.MetaDataOperationModeResponse, protocol.ID) - rsp := &RegisterProtocolResponse{Success: true} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for registering protocol") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for registering protocol") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// RegisterProtocol for the device service puts the protocol into the message bus. -func (c *deviceServiceProtocolOperationClient) RegisterProtocol(protocol *models.Protocol) error { - request := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationCreate, - bus.MetaDataOperationModeRequest, protocol.ID) - fields, err := (&RegisterProtocolRequest{Protocol: *protocol}).Marshal() - if err != nil { - return err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return err - } - - rsp := &RegisterProtocolResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return err - } - if !rsp.Success { - return errors.New("fail to register protocol") - } - return nil -} diff --git a/internal/operations/meta_protocol_unregister.go b/internal/operations/meta_protocol_unregister.go deleted file mode 100644 index 54f48cf..0000000 --- a/internal/operations/meta_protocol_unregister.go +++ /dev/null @@ -1,99 +0,0 @@ -package operations - -import ( - "errors" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" -) - -type UnregisterProtocolRequest struct { - ProtocolID string `json:"protocol_id"` -} - -func (r *UnregisterProtocolRequest) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *UnregisterProtocolRequest) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -type UnregisterProtocolResponse struct { - Success bool `json:"success"` -} - -func (r *UnregisterProtocolResponse) Unmarshal(fields map[string]interface{}) error { - return Map2Struct(fields, r) -} -func (r *UnregisterProtocolResponse) Marshal() (map[string]interface{}, error) { - return Struct2Map(*r) -} - -// OnUnregisterProtocols for the device manager takes the protocols from the message bus. -func (c *deviceManagerProtocolOperationClient) OnUnregisterProtocols(unregister func(protocolID string) error) error { - schema := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, - bus.MetaDataOperationModeRequest, bus.TopicWildcard) - message, err := schema.ToMessage() - if err != nil { - return err - } - if err = c.mb.Subscribe(func(msg *bus.Message) { - // parse request from the message - _, fields, err := msg.Parse() - if err != nil { - c.logger.WithError(err).Errorf("fail to parse the message[%s] for unregistering protocol", - msg.ToString()) - return - } - req := &UnregisterProtocolRequest{} - if err := req.Unmarshal(fields); err != nil { - c.logger.WithError(err).Error("fail to unmarshal the request for unregistering protocol") - return - } - protocolID := req.ProtocolID - if err := unregister(protocolID); err != nil { - c.logger.Error(err.Error()) - return - } - - // publish response - response := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, - bus.MetaDataOperationModeResponse, protocolID) - rsp := &UnregisterProtocolResponse{Success: true} - fields, err = rsp.Marshal() - if err != nil { - c.logger.WithError(err).Error("fail to marshal the response for unregistering protocol") - return - } - response.SetFields(fields) - if err := c.mb.Publish(response); err != nil { - c.logger.WithError(err).Error("fail to publish the response for unregistering protocol") - return - } - }, message.Topic); err != nil { - return err - } - return nil -} - -// UnregisterProtocol for the device service puts the protocol into the message bus. -func (c *deviceServiceProtocolOperationClient) UnregisterProtocol(protocolID string) error { - request := bus.NewMetaData(bus.MetaDataTypeProtocol, bus.MetaDataOperationDelete, - bus.MetaDataOperationModeRequest, protocolID) - fields, err := (&UnregisterProtocolRequest{ProtocolID: protocolID}).Marshal() - if err != nil { - return err - } - request.SetFields(fields) - response, err := c.mb.Call(request) - if err != nil { - return err - } - - rsp := &UnregisterProtocolResponse{} - if err := rsp.Unmarshal(response.GetFields()); err != nil { - return err - } - if !rsp.Success { - return errors.New("fail to unregister protocol") - } - return nil -} diff --git a/logger/logger.go b/logger/logger.go deleted file mode 100644 index 42f5737..0000000 --- a/logger/logger.go +++ /dev/null @@ -1,110 +0,0 @@ -package logger - -import ( - "github.com/sirupsen/logrus" - "os" -) - -type LogLevel string - -const ( - DebugLevel LogLevel = "debug" - InfoLevel LogLevel = "info" - WarnLevel LogLevel = "warn" - ErrorLevel LogLevel = "error" - FatalLevel LogLevel = "fatal" - PanicLevel LogLevel = "panic" -) - -func NewLogger() *Logger { - logger := &Logger{ - logger: logrus.NewEntry(logrus.New()), - } - _ = logger.SetLevel(InfoLevel) - return logger -} - -type Logger struct { - logger *logrus.Entry -} - -func (l Logger) SetLevel(level LogLevel) error { - lvl, err := logrus.ParseLevel(string(level)) - if err != nil { - return err - } - l.logger.Logger.SetLevel(lvl) - l.logger.Logger.SetOutput(os.Stdout) - l.logger.Logger.SetFormatter(&logFormatter{logrus.TextFormatter{FullTimestamp: true, ForceColors: true}}) - return nil -} - -// WithFields adds a map of fields to the Entry. -func (l Logger) WithFields(vs ...string) *logrus.Entry { - // mutex.Lock() - // defer mutex.Unlock() - fs := logrus.Fields{} - for index := 0; index < len(vs)-1; index = index + 2 { - fs[vs[index]] = vs[index+1] - } - return l.logger.WithFields(fs) -} - -// WithError adds an error as single field (using the key defined in ErrorKey) to the Entry. -func (l Logger) WithError(err error) *logrus.Entry { - if err == nil { - return l.logger - } - - return l.logger.WithField(logrus.ErrorKey, err.Error()) -} - -func (l Logger) Debugf(format string, args ...interface{}) { - l.logger.Debugf(format, args...) -} -func (l Logger) Infof(format string, args ...interface{}) { - l.logger.Infof(format, args...) -} -func (l Logger) Warnf(format string, args ...interface{}) { - l.logger.Infof(format, args...) -} -func (l Logger) Errorf(format string, args ...interface{}) { - l.logger.Errorf(format, args...) -} -func (l Logger) Fatalf(format string, args ...interface{}) { - l.logger.Fatalf(format, args...) -} -func (l Logger) Panicf(format string, args ...interface{}) { - l.logger.Panicf(format, args...) -} - -func (l Logger) Debug(args ...interface{}) { - l.logger.Debug(args...) -} -func (l Logger) Info(args ...interface{}) { - l.logger.Info(args...) -} -func (l Logger) Warn(args ...interface{}) { - l.logger.Info(args...) -} -func (l Logger) Error(args ...interface{}) { - l.logger.Error(args...) -} -func (l Logger) Fatal(args ...interface{}) { - l.logger.Fatal(args...) -} -func (l Logger) Panic(args ...interface{}) { - l.logger.Panic(args) -} - -type logFormatter struct { - logrus.TextFormatter -} - -func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) { - data, err := f.TextFormatter.Format(entry) - if err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/models/data_converter.go b/pkg/models/data_converter.go deleted file mode 100644 index 06526b0..0000000 --- a/pkg/models/data_converter.go +++ /dev/null @@ -1,35 +0,0 @@ -package models - -import ( - "strconv" -) - -var StrConverters = map[PropertyType]StrConverter{ - PropertyTypeInt: Str2Int, - PropertyTypeUint: Str2Uint, - PropertyTypeFloat: Str2Float, - PropertyTypeBool: Str2Bool, - PropertyTypeString: Str2String, -} - -type StrConverter func(s string) (interface{}, error) - -func Str2Int(s string) (interface{}, error) { - return strconv.ParseInt(s, 10, 64) -} - -func Str2Uint(s string) (interface{}, error) { - return strconv.ParseUint(s, 10, 64) -} - -func Str2Float(s string) (interface{}, error) { - return strconv.ParseFloat(s, 64) -} - -func Str2Bool(s string) (interface{}, error) { - return strconv.ParseBool(s) -} - -func Str2String(s string) (interface{}, error) { - return s, nil -} diff --git a/pkg/models/data_device.go b/pkg/models/data_device.go deleted file mode 100644 index 5e777af..0000000 --- a/pkg/models/data_device.go +++ /dev/null @@ -1,101 +0,0 @@ -package models - -import ( - "encoding/json" - "fmt" - "github.com/thingio/edge-device-sdk/internal/message_bus" -) - -type ( - DeviceDataOperation = string // the type of device data's operation - DeviceDataPropertyReportMode = string // the mode of device data's reporting - - ProductFuncID = string // product functionality ID - ProductFuncType = string // product functionality type - ProductPropertyID = ProductFuncID // product property's functionality ID - ProductEventID = ProductFuncID // product event's functionality ID - ProductMethodID = ProductFuncID // product method's functionality ID -) - -const ( - DeviceDataOperationRead DeviceDataOperation = "read" // Device Property Read - DeviceDataOperationWrite DeviceDataOperation = "write" // Device Property Write - DeviceDataOperationEvent DeviceDataOperation = "event" // Device Event - DeviceDataOperationRequest DeviceDataOperation = "request" // Device Method Request - DeviceDataOperationResponse DeviceDataOperation = "response" // Device Method Response - DeviceDataOperationError DeviceDataOperation = "error" // Device Method Error - - DeviceDataReportModePeriodical DeviceDataPropertyReportMode = "periodical" // report device data at intervals, e.g. 5s, 1m, 0.5h - DeviceDataReportModeMutated DeviceDataPropertyReportMode = "mutated" // report device data while mutated - - PropertyFunc ProductFuncType = "props" // product property's functionality - EventFunc ProductFuncType = "events" // product event's functionality - MethodFunc ProductFuncType = "methods" // product method's functionality - - DeviceDataMultiPropsID ProductFuncID = "*" - DeviceDataMultiPropsName = "多属性" -) - -// Opts2FuncType maps operation upon device data as product's functionality. -var opts2FuncType = map[DeviceDataOperation]ProductFuncType{ - DeviceDataOperationEvent: EventFunc, - DeviceDataOperationRead: PropertyFunc, - DeviceDataOperationWrite: PropertyFunc, - DeviceDataOperationRequest: MethodFunc, - DeviceDataOperationResponse: MethodFunc, - DeviceDataOperationError: MethodFunc, -} - -type DeviceData struct { - bus.MessageData - - ProductID string `json:"product_id"` - DeviceID string `json:"device_id"` - OptType DeviceDataOperation `json:"opt_type"` - FuncID ProductFuncID `json:"func_id"` - FuncType ProductFuncType `json:"func_type"` -} - -func (d *DeviceData) ToMessage() (*bus.Message, error) { - topic := bus.NewDeviceDataTopic(d.ProductID, d.DeviceID, d.OptType, d.FuncID) - payload, err := json.Marshal(d.Fields) - if err != nil { - return nil, err - } - - return &bus.Message{ - Topic: topic.String(), - Payload: payload, - }, nil -} - -func (d *DeviceData) isRequest() bool { - return d.OptType == DeviceDataOperationRequest -} - -func (d *DeviceData) Response() (response bus.Data, err error) { - if !d.isRequest() { - return nil, fmt.Errorf("the device data is not a request: %+v", *d) - } - return NewDeviceData(d.ProductID, d.DeviceID, DeviceDataOperationResponse, d.FuncID), nil -} - -func NewDeviceData(productID, deviceID string, optType DeviceDataOperation, dataID ProductFuncID) *DeviceData { - return &DeviceData{ - ProductID: productID, - DeviceID: deviceID, - OptType: optType, - FuncID: dataID, - FuncType: opts2FuncType[optType], - } -} - -func ParseDeviceData(msg *bus.Message) (*DeviceData, error) { - tags, fields, err := msg.Parse() - if err != nil { - return nil, err - } - deviceData := NewDeviceData(tags[0], tags[1], tags[2], tags[3]) - deviceData.SetFields(fields) - return deviceData, nil -} diff --git a/pkg/models/device.go b/pkg/models/device.go deleted file mode 100644 index 4d8b719..0000000 --- a/pkg/models/device.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -type Device struct { - ID string `json:"id"` // 设备 ID - Name string `json:"name"` // 设备名称 - Desc string `json:"desc"` // 设备描述 - ProductID string `json:"product_id"` // 设备所属产品 ID, 不可更新 - ProductName string `json:"product_name"` // 设备所属产品名称 - Category string `json:"category"` // 设备类型(多媒体, 时序), 不可更新 - Recording bool `json:"recording"` // 是否正在录制 - DeviceStatus string `json:"device_status"` // 设备状态 - DeviceProps map[string]string `json:"device_props"` // 设备动态属性, 取决于具体的设备协议 - DeviceLabels map[string]string `json:"device_labels"` // 设备标签 - DeviceMeta map[string]string `json:"device_meta"` // 视频流元信息 -} - -func (d *Device) GetProperty(key string) string { - return d.DeviceProps[key] -} diff --git a/pkg/models/device_connector.go b/pkg/models/device_connector.go deleted file mode 100644 index af3a31a..0000000 --- a/pkg/models/device_connector.go +++ /dev/null @@ -1,37 +0,0 @@ -package models - -import ( - "github.com/thingio/edge-device-sdk/logger" -) - -type DeviceConnector interface { - // Initialize will try to initialize a device connector to - // create the connection with device which needs to activate. - // It must always return nil if the device needn't be initialized. - Initialize(lg *logger.Logger) error - - // Start will to try to create connection with the real device. - // It must always return nil if the device needn't be initialized. - Start() error - // Stop will to try to destroy connection with the real device. - // It must always return nil if the device needn't be initialized. - Stop(force bool) error - // Ping is used to test the connectivity of the real device. - // If the device is connected, it will return true, else return false. - Ping() bool - - // Watch will read device's properties periodically with the specified policy. - Watch(bus chan<- DeviceData) error - // Write will write the specified property to the real device. - Write(propertyID ProductPropertyID, data interface{}) error - // Subscribe will subscribe the specified event, - // and put data belonging to this event into the bus. - Subscribe(eventID ProductEventID, bus chan<- DeviceData) error - // Call is used to call the specified method defined in product, - // then waiting for a while to receive its response. - // If the call is timeout, it will return a timeout error. - Call(methodID ProductMethodID, request DeviceData) (response DeviceData, err error) // method call -} - -// ConnectorBuilder is used to create a new device connector using the specified product and device. -type ConnectorBuilder func(product *Product, device *Device) (DeviceConnector, error) diff --git a/pkg/models/meta_loader.go b/pkg/models/meta_loader.go deleted file mode 100644 index 230fe96..0000000 --- a/pkg/models/meta_loader.go +++ /dev/null @@ -1,104 +0,0 @@ -package models - -import ( - "encoding/json" - "fmt" - "github.com/thingio/edge-device-sdk/config" - "gopkg.in/yaml.v2" - "io/fs" - "io/ioutil" - "path/filepath" -) - -func LoadProtocol(path string) (*Protocol, error) { - protocol := new(Protocol) - if err := load(path, protocol); err != nil { - return nil, err - } - return protocol, nil -} - -func LoadProducts(protocolID string) []*Product { - products := make([]*Product, 0) - _ = filepath.Walk(config.ProductsPath, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - var product *Product - product, err = loadProduct(path) - if err != nil { - return err - } - if product.Protocol != protocolID { - return nil - } - products = append(products, product) - return nil - }) - return products -} - -func LoadDevices(productID string) []*Device { - devices := make([]*Device, 0) - _ = filepath.Walk(config.DevicesPath, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - var device *Device - device, err = loadDevice(path) - if err != nil { - return err - } - if device.ProductID != productID { - return nil - } - devices = append(devices, device) - return nil - }) - return devices -} - -func loadProduct(path string) (*Product, error) { - product := new(Product) - if err := load(path, product); err != nil { - return nil, err - } - return product, nil -} - -func loadDevice(path string) (*Device, error) { - device := new(Device) - if err := load(path, device); err != nil { - return nil, err - } - return device, nil -} - -func load(path string, meta interface{}) error { - data, err := ioutil.ReadFile(path) - if err != nil { - return fmt.Errorf("fail to load the meta configurtion stored in %s, got %s", - path, err.Error()) - } - - var unmarshaller func([]byte, interface{}) error - switch ext := filepath.Ext(path); ext { - case ".json": - unmarshaller = json.Unmarshal - case ".yaml", ".yml": - unmarshaller = yaml.Unmarshal - default: - return fmt.Errorf("invalid meta configuration extension %s, only supporing json/yaml/yml", ext) - } - - if err := unmarshaller(data, meta); err != nil { - return fmt.Errorf("fail to unmarshal the device configuration, got %s", err.Error()) - } - return nil -} diff --git a/pkg/models/meta_operator.go b/pkg/models/meta_operator.go deleted file mode 100644 index 05cf2c5..0000000 --- a/pkg/models/meta_operator.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type MetaStore interface { - ListProducts(protocolID string) ([]*Product, error) - ListDevices(productID string) ([]*Device, error) -} diff --git a/pkg/models/product.go b/pkg/models/product.go deleted file mode 100644 index cbd6a53..0000000 --- a/pkg/models/product.go +++ /dev/null @@ -1,55 +0,0 @@ -package models - -type Product struct { - ID string `json:"id"` // 产品 ID - Name string `json:"name"` // 产品名称 - Desc string `json:"desc"` // 产品描述 - Protocol string `json:"protocol"` // 产品协议 - DataFormat string `json:"data_format,omitempty"` // 数据格式 - Properties []*ProductProperty `json:"properties,omitempty"` // 属性功能列表 - Events []*ProductEvent `json:"events,omitempty"` // 事件功能列表 - Methods []*ProductMethod `json:"methods,omitempty"` // 方法功能列表 - Topics []*ProductTopic `json:"topics,omitempty"` // 各功能对应的消息主题 -} - -type ProductProperty struct { - Id string `json:"id"` - Name string `json:"name"` - Desc string `json:"desc"` - Interval string `json:"interval"` - Unit string `json:"unit"` - FieldType string `json:"field_type"` - ReportMode string `json:"report_mode"` - Writeable bool `json:"writeable"` - AuxProps map[string]string `json:"aux_props"` -} - -type ProductEvent struct { - Id string `json:"id"` - Name string `json:"name"` - Desc string `json:"desc"` - Outs []*ProductField `json:"outs"` - AuxProps map[string]string `json:"aux_props"` -} - -type ProductMethod struct { - Id string `json:"id"` - Name string `json:"name"` - Desc string `json:"desc"` - Ins []*ProductField `json:"ins"` - Outs []*ProductField `json:"outs"` - AuxProps map[string]string `json:"aux_props"` -} - -type ProductTopic struct { - Topic string `json:"topic"` - OptType string `json:"opt_type"` - Desc string `json:"desc"` -} - -type ProductField struct { - Id string `json:"id"` - Name string `json:"name"` - FieldType string `json:"field_type"` - Desc string `json:"desc"` -} diff --git a/pkg/models/property.go b/pkg/models/property.go deleted file mode 100644 index 69e186e..0000000 --- a/pkg/models/property.go +++ /dev/null @@ -1,37 +0,0 @@ -package models - -type ( - PropertyType = string - PropertyStyle = string // UI Representation Style -) - -const ( - PropertyTypeInt PropertyType = "int" - PropertyTypeUint PropertyType = "uint" - PropertyTypeFloat PropertyType = "float" - PropertyTypeBool PropertyType = "bool" - PropertyTypeString PropertyType = "string" -) - -var ( - DeviceDataPropertyTypes = map[PropertyType]struct{}{ - PropertyTypeInt: {}, - PropertyTypeUint: {}, - PropertyTypeFloat: {}, - PropertyTypeBool: {}, - PropertyTypeString: {}, - } -) - -type Property struct { - Name string `json:"name"` // Name 为属性的展示名称 - Desc string `json:"desc"` // Desc 为属性的描述, 通常以名称旁的?形式进行展示 - Type PropertyType `json:"type"` // Type 为该属性的数据类型 - Style PropertyStyle `json:"style"` // Style 为该属性在前端的展示样式 - Default string `json:"default"` // Default 该属性默认的属性值 - Range string `json:"range"` // Range 为属性值的可选范围 - Precondition string `json:"precondition"` // Precondition 为当前属性展示的前置条件, 用来实现简单的动态依赖功能 - Required bool `json:"required"` // Required 表示该属性是否为必填项 - Multiple bool `json:"multiple"` // Multiple 表示是否支持多选(下拉框), 列表(输入), Map(K,V) - MaxLen int64 `json:"max_len"` // MaxLen 表示当Multiple为true时, 可选择的最大数量 -} diff --git a/pkg/models/protocol.go b/pkg/models/protocol.go deleted file mode 100644 index 551d083..0000000 --- a/pkg/models/protocol.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -type ( - ProtocolPropertyKey = string // common property owned by devices with the same protocol -) - -type Protocol struct { - ID string `json:"id"` // 协议 ID - Name string `json:"name"` // 协议名称 - Desc string `json:"desc"` // 协议描述 - Category string `json:"category"` - Language string `json:"language"` - SupportFuncs []string `json:"support_funcs"` - AuxProps []*Property `json:"aux_props"` - DeviceProps []*Property `json:"device_props"` -} diff --git a/pkg/startup/device_manager/device_manager.go b/pkg/startup/device_manager/device_manager.go deleted file mode 100644 index f16bfc6..0000000 --- a/pkg/startup/device_manager/device_manager.go +++ /dev/null @@ -1,88 +0,0 @@ -package startup - -import ( - "context" - "github.com/thingio/edge-device-sdk/config" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/internal/operations" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" - "github.com/thingio/edge-device-sdk/version" - "os" - "sync" -) - -type DeviceManager struct { - // manager information - Version string - - // caches - protocols sync.Map - - // operation clients - moc operations.DeviceManagerMetaOperationClient // wrap the message bus to manipulate meta - doc operations.DeviceManagerDeviceDataOperationClient // warp the message bus to manipulate device data - metaStore models.MetaStore // meta store - - // lifetime control variables for the device service - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - logger *logger.Logger -} - -func (m *DeviceManager) Initialize(ctx context.Context, cancel context.CancelFunc, metaStore models.MetaStore) { - m.logger = logger.NewLogger() - - m.Version = version.Version - - m.protocols = sync.Map{} - - m.initializeOperationClients(metaStore) - - m.ctx = ctx - m.cancel = cancel - m.wg = sync.WaitGroup{} -} - -func (m *DeviceManager) initializeOperationClients(metaStore models.MetaStore) { - mb, err := bus.NewMessageBus(&config.C.MessageBus, m.logger) - if err != nil { - m.logger.WithError(err).Error("fail to initialize the message bus") - os.Exit(1) - } - if err = mb.Connect(); err != nil { - m.logger.WithError(err).Error("fail to connect to the message bus") - os.Exit(1) - } - - moc, err := operations.NewDeviceManagerMetaOperationClient(mb, m.logger) - if err != nil { - m.logger.WithError(err).Error("fail to initialize the meta operation client for the device service") - os.Exit(1) - } - m.moc = moc - doc, err := operations.NewDeviceManagerDeviceDataOperationClient(mb, m.logger) - if err != nil { - m.logger.WithError(err).Error("fail to initialize the device data operation client for the device service") - os.Exit(1) - } - m.doc = doc - - m.metaStore = metaStore -} - -func (m *DeviceManager) Serve() { - defer m.Stop(false) - - m.wg.Add(1) - go m.watchingProtocols() - m.wg.Add(1) - go m.watchingProductOperations() - m.wg.Add(1) - go m.watchingDeviceOperations() - - m.wg.Wait() -} - -func (m *DeviceManager) Stop(force bool) {} diff --git a/pkg/startup/device_manager/handle_operations_meta.go b/pkg/startup/device_manager/handle_operations_meta.go deleted file mode 100644 index 7bd043f..0000000 --- a/pkg/startup/device_manager/handle_operations_meta.go +++ /dev/null @@ -1,72 +0,0 @@ -package startup - -import ( - "github.com/thingio/edge-device-sdk/pkg/models" - "time" -) - -func (m *DeviceManager) watchingProtocols() { - defer func() { - m.wg.Done() - }() - - if err := m.moc.OnRegisterProtocols(m.registerProtocol); err != nil { - m.logger.WithError(err).Error("fail to wait for registering protocols") - return - } - if err := m.moc.OnUnregisterProtocols(m.unregisterProtocol); err != nil { - m.logger.WithError(err).Error("fail to wait for unregistering protocols") - return - } - - ticker := time.NewTicker(1 * time.Minute) - for { - select { - case <-ticker.C: // check the connection of protocol - m.protocols.Range(func(key, value interface{}) bool { - // TODO PING - return true - }) - case <-m.ctx.Done(): - break - } - } -} - -func (m *DeviceManager) registerProtocol(protocol *models.Protocol) error { - m.protocols.Store(protocol.ID, protocol) - m.logger.Infof("the protocol[%s] has registered successfully", protocol.ID) - return nil -} - -func (m *DeviceManager) unregisterProtocol(protocolID string) error { - m.protocols.Delete(protocolID) - m.logger.Infof("the protocol[%s] has unregistered successfully", protocolID) - return nil -} - -func (m *DeviceManager) watchingProductOperations() { - defer func() { - m.wg.Done() - }() - - if err := m.moc.OnListProducts(m.metaStore.ListProducts); err != nil { - m.logger.WithError(err).Error("fail to wait for listing products") - return - } else { - m.logger.Infof("start to watch the changes for product...") - } -} - -func (m *DeviceManager) watchingDeviceOperations() { - defer func() { - m.wg.Done() - }() - - if err := m.moc.OnListDevices(m.metaStore.ListDevices); err != nil { - m.logger.WithError(err).Error("fail to wait for listing devices") - return - } else { - m.logger.Infof("start to watch the changes for device...") - } -} diff --git a/pkg/startup/device_manager/startup.go b/pkg/startup/device_manager/startup.go deleted file mode 100644 index d7b328b..0000000 --- a/pkg/startup/device_manager/startup.go +++ /dev/null @@ -1,14 +0,0 @@ -package startup - -import ( - "context" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func Startup(metaStore models.MetaStore) { - ctx, cancel := context.WithCancel(context.Background()) - - dm := &DeviceManager{} - dm.Initialize(ctx, cancel, metaStore) - dm.Serve() -} diff --git a/pkg/startup/device_service/device_service.go b/pkg/startup/device_service/device_service.go deleted file mode 100644 index 1d22c83..0000000 --- a/pkg/startup/device_service/device_service.go +++ /dev/null @@ -1,137 +0,0 @@ -package startup - -import ( - "context" - "fmt" - "github.com/thingio/edge-device-sdk/config" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/internal/operations" - "github.com/thingio/edge-device-sdk/logger" - "github.com/thingio/edge-device-sdk/pkg/models" - "github.com/thingio/edge-device-sdk/version" - "os" - "sync" -) - -type DeviceService struct { - // service information - ID string - Name string - Version string - - // driver - protocol *models.Protocol - connectorBuilder models.ConnectorBuilder - - // caches - products sync.Map - devices sync.Map - deviceConnectors sync.Map - unsupportedDeviceDataTypes map[models.DeviceDataOperation]struct{} - deviceDataHandlers map[models.DeviceDataOperation]DeviceDataHandler - - // operation clients - bus chan models.DeviceData - moc operations.DeviceServiceMetaOperationClient // wrap the message bus to manipulate - doc operations.DeviceServiceDeviceDataOperationClient // warp the message bus to manipulate device data - - // lifetime control variables for the device service - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - logger *logger.Logger -} - -func (s *DeviceService) Initialize(ctx context.Context, cancel context.CancelFunc, - protocol *models.Protocol, connectorBuilder models.ConnectorBuilder) { - s.logger = logger.NewLogger() - - s.ID = protocol.ID - s.Name = protocol.Name - s.Version = version.Version - - s.protocol = protocol - if connectorBuilder == nil { - s.logger.Error("please implement and specify the connector builder") - os.Exit(1) - } - s.connectorBuilder = connectorBuilder - - s.products = sync.Map{} - s.devices = sync.Map{} - s.deviceConnectors = sync.Map{} - - s.initializeOperationClients() - - s.ctx = ctx - s.cancel = cancel - s.wg = sync.WaitGroup{} -} - -func (s *DeviceService) initializeOperationClients() { - s.bus = make(chan models.DeviceData, 100) - - mb, err := bus.NewMessageBus(&config.C.MessageBus, s.logger) - if err != nil { - s.logger.WithError(err).Error("fail to initialize the message bus") - os.Exit(1) - } - if err = mb.Connect(); err != nil { - s.logger.WithError(err).Error("fail to connect to the message bus") - os.Exit(1) - } - - moc, err := operations.NewDeviceServiceMetaOperationClient(mb, s.logger) - if err != nil { - s.logger.WithError(err).Error("fail to initialize the meta operation client for the device service") - os.Exit(1) - } - s.moc = moc - doc, err := operations.NewDeviceServiceDeviceDataOperationClient(mb, s.logger) - if err != nil { - s.logger.WithError(err).Error("fail to initialize the device data operation client for the device service") - os.Exit(1) - } - s.doc = doc -} - -func (s *DeviceService) Serve() { - defer s.Stop(false) - - s.registerProtocol() - defer s.unregisterProtocol() - - s.activateDevices() - defer s.deactivateDevices() - - s.publishingDeviceData() - s.handlingDeviceData() - - s.wg.Wait() -} - -func (s *DeviceService) Stop(force bool) {} - -func (s *DeviceService) getProduct(productID string) (*models.Product, error) { - v, ok := s.products.Load(productID) - if ok { - return v.(*models.Product), nil - } - return nil, fmt.Errorf("the product[%s] is not found in cache", productID) -} - -func (s *DeviceService) getDevice(deviceID string) (*models.Device, error) { - v, ok := s.devices.Load(deviceID) - if ok { - return v.(*models.Device), nil - } - return nil, fmt.Errorf("the device[%s] is not found in cache", deviceID) -} - -func (s *DeviceService) getDeviceConnector(deviceID string) (models.DeviceConnector, error) { - v, ok := s.deviceConnectors.Load(deviceID) - if ok { - return v.(models.DeviceConnector), nil - } - return nil, fmt.Errorf("the device[%s] is not activated", deviceID) -} diff --git a/pkg/startup/device_service/handle_operations_device.go b/pkg/startup/device_service/handle_operations_device.go deleted file mode 100644 index 6915748..0000000 --- a/pkg/startup/device_service/handle_operations_device.go +++ /dev/null @@ -1,169 +0,0 @@ -package startup - -import ( - "fmt" - bus "github.com/thingio/edge-device-sdk/internal/message_bus" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -// publishingDeviceData tries to publish data in the bus into the MessageBus. -func (s *DeviceService) publishingDeviceData() { - s.wg.Add(1) - - go func() { - defer s.wg.Done() - - for { - select { - case data := <-s.bus: - if err := s.doc.Publish(data); err != nil { - s.logger.WithError(err).Errorf("fail to publish the device data %+v", data) - } else { - s.logger.Debugf("success to publish the device data %+v", data) - } - case <-s.ctx.Done(): - break - } - } - }() -} - -// TODO 是否可以将 DeviceDataHandler 转移到 DeviceServiceDeviceDataOperationClient 中? - -type DeviceDataHandler func(product *models.Product, device *models.Device, conn models.DeviceConnector, - dataID string, fields map[string]interface{}) error - -func (s *DeviceService) handlingDeviceData() { - s.wg.Add(1) - - s.unsupportedDeviceDataTypes = map[models.DeviceDataOperation]struct{}{ - models.DeviceDataOperationRead: {}, // property read - models.DeviceDataOperationEvent: {}, // event - models.DeviceDataOperationResponse: {}, // method response - models.DeviceDataOperationError: {}, // method error - } - s.deviceDataHandlers = map[models.DeviceDataOperation]DeviceDataHandler{ - models.DeviceDataOperationWrite: s.handleWriteData, // property write - models.DeviceDataOperationRequest: s.handleRequestData, // method request - } - - go func() { - defer s.wg.Done() - - topic := bus.TopicTypeDeviceData.Topic() - if err := s.doc.Subscribe(s.handleDeviceData, topic); err != nil { - s.logger.WithError(err).Errorf("fail to subscribe the topic: %s", topic) - return - } - }() -} - -func (s *DeviceService) handleDeviceData(msg *bus.Message) { - tags, fields, err := msg.Parse() - if err != nil { - s.logger.WithError(err).Errorf("fail to parse the message[%s]", msg.ToString()) - return - } - - productID, deviceID, optType, dataID := tags[0], tags[1], tags[2], tags[3] - product, err := s.getProduct(productID) - if err != nil { - s.logger.Error(err.Error()) - return - } - device, err := s.getDevice(deviceID) - if err != nil { - s.logger.Error(err.Error()) - return - } - connector, err := s.getDeviceConnector(deviceID) - if err != nil { - s.logger.Error(err.Error()) - return - } - - if _, ok := s.unsupportedDeviceDataTypes[optType]; ok { - return - } - handler, ok := s.deviceDataHandlers[optType] - if !ok { - s.logger.Errorf("unsupported operation type: %s", optType) - return - } - if err = handler(product, device, connector, dataID, fields); err != nil { - s.logger.WithError(err).Errorf("fail to handle the message: %+v", msg) - return - } -} - -// handleWriteData is responsible for handling the write request forwarded by the device manager. -// The fields will be written into the real device finally. -// -// This handler could be tested as follows: -// 1. Send the specified format data to the message bus: -// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/write/float" -m "{\"intf\": 100}" -// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Write("randnum_test01", "randnum_test01", "float", 100) -// 2. Observe the log of device service. -func (s *DeviceService) handleWriteData(product *models.Product, device *models.Device, conn models.DeviceConnector, - propertyID string, fields map[string]interface{}) error { - properties := map[models.ProductPropertyID]*models.ProductProperty{} - for _, property := range product.Properties { - properties[property.Id] = property - } - - var written interface{} - if propertyID == models.DeviceDataMultiPropsID { - tmp := make(map[models.ProductPropertyID]interface{}, len(fields)) - for k, v := range fields { - property, ok := properties[k] - if !ok { - s.logger.Errorf("undefined property: %s", propertyID) - continue - } - if !property.Writeable { - s.logger.Errorf("the property[%s] is read only", propertyID) - continue - } - tmp[k] = v - } - written = tmp - } else { - property, ok := properties[propertyID] - if !ok { - return fmt.Errorf("undefined property: %s", propertyID) - } - if !property.Writeable { - return fmt.Errorf("the property[%s] is read only", propertyID) - } - - v, ok := fields[propertyID] - if !ok { - return fmt.Errorf("the property[%s]'s value is missed", propertyID) - } - written = map[models.ProductPropertyID]interface{}{propertyID: v} - } - return conn.Write(propertyID, written) -} - -// handleRequestData is responsible for handling the method's request forwarded by the device manager. -// The fields will be expanded a request, and -// -// This handler could be tested as follows: -// 1. Send the specified format data to the message bus: -// (a.) Directly (mock): mosquitto_pub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/request/Intn" -m "{\"n\": 100}" -// (b.) Indirectly: invoke the DeviceManagerDeviceDataOperationClient.Call("randnum_test01", "randnum_test01", "Intn", map[string]interface{}{"n": 100}) -// 2. Observe the log of device service and subscribe the specified topic: -// mosquitto_sub -h 172.16.251.163 -p 1883 -t "v1/DATA/randnum_test01/randnum_test01/response/Intn". -func (s *DeviceService) handleRequestData(product *models.Product, device *models.Device, conn models.DeviceConnector, - methodID string, fields map[string]interface{}) error { - request := models.NewDeviceData(product.ID, device.ID, models.DeviceDataOperationRequest, methodID) - request.SetFields(fields) - response, err := conn.Call(methodID, *request) - if err != nil { - return err - } - if err = s.doc.Publish(response); err != nil { - return err - } - return nil -} diff --git a/pkg/startup/device_service/handle_operations_meta.go b/pkg/startup/device_service/handle_operations_meta.go deleted file mode 100644 index a7a0ef9..0000000 --- a/pkg/startup/device_service/handle_operations_meta.go +++ /dev/null @@ -1,119 +0,0 @@ -package startup - -import ( - "github.com/thingio/edge-device-sdk/pkg/models" - "os" -) - -// registerProtocol tries to register the protocol to the device manager. -func (s *DeviceService) registerProtocol() { - if err := s.moc.RegisterProtocol(s.protocol); err != nil { - s.logger.WithError(err).Errorf("fail to register the protocol[%s] "+ - "to the device manager", s.protocol.ID) - os.Exit(1) - } - s.logger.Infof("success to register the protocol[%s] to the device manager", s.protocol.ID) -} - -// unregisterProtocol tries to unregister the protocol from the device manager. -func (s *DeviceService) unregisterProtocol() { - if err := s.moc.UnregisterProtocol(s.protocol.ID); err != nil { - s.logger.WithError(err).Errorf("fail to unregister the protocol[%s] "+ - "from the device manager", s.protocol.ID) - } - s.logger.Infof("success to unregister the protocol[%s] from the device manager", s.protocol.ID) -} - -// activateDevices tries to activate all devices. -func (s *DeviceService) activateDevices() { - products, err := s.moc.ListProducts(s.protocol.ID) - if err != nil { - s.logger.WithError(err).Error("fail to fetch products from the device manager") - os.Exit(1) - } - for _, product := range products { - devices, err := s.moc.ListDevices(product.ID) - if err != nil { - s.logger.WithError(err).Error("fail to fetch devices from the device manager") - os.Exit(1) - } - - s.products.Store(product.ID, product) - for _, device := range devices { - if err := s.activateDevice(device); err != nil { - s.logger.WithError(err).Errorf("fail to activate the device[%s]", device.ID) - continue - } - s.devices.Store(device.ID, device) - } - } -} - -// activateDevice is responsible for establishing the connection with the real device. -func (s *DeviceService) activateDevice(device *models.Device) error { - if connector, _ := s.getDeviceConnector(device.ID); connector != nil { // the device has been activated - _ = s.deactivateDevice(device.ID) - } - - // build, initialize and start connector - product, err := s.getProduct(device.ProductID) - if err != nil { - return err - } - connector, err := s.connectorBuilder(product, device) - if err != nil { - return err - } - if err := connector.Initialize(s.logger); err != nil { - s.logger.WithError(err).Error("fail to initialize the random device connector") - return err - } - if err := connector.Start(); err != nil { - s.logger.WithError(err).Error("fail to start the random device connector") - return err - } - - if len(product.Properties) != 0 { - if err := connector.Watch(s.bus); err != nil { - s.logger.WithError(err).Error("fail to watch properties for the device") - return err - } - } - - for _, event := range product.Events { - if err := connector.Subscribe(event.Id, s.bus); err != nil { - s.logger.WithError(err).Errorf("fail to subscribe the product event[%s]", event.Id) - continue - } - } - - s.deviceConnectors.Store(device.ID, connector) - s.logger.Infof("success to activate the device[%s]", device.ID) - return nil -} - -// deactivateDevices tries to deactivate all devices. -func (s *DeviceService) deactivateDevices() { - s.deviceConnectors.Range(func(key, value interface{}) bool { - deviceID := key.(string) - if err := s.deactivateDevice(deviceID); err != nil { - s.logger.WithError(err).Errorf("fail to deactivate the device[%s]", deviceID) - } - return true - }) -} - -// deactivateDevice is responsible for breaking up the connection with the real device. -func (s *DeviceService) deactivateDevice(deviceID string) error { - connector, _ := s.getDeviceConnector(deviceID) - if connector == nil { - return nil - } - if err := connector.Stop(false); err != nil { - return err - } - - s.deviceConnectors.Delete(deviceID) - s.logger.Infof("success to deactivate the device[%s]", deviceID) - return nil -} diff --git a/pkg/startup/device_service/startup.go b/pkg/startup/device_service/startup.go deleted file mode 100644 index 1a7e113..0000000 --- a/pkg/startup/device_service/startup.go +++ /dev/null @@ -1,14 +0,0 @@ -package startup - -import ( - "context" - "github.com/thingio/edge-device-sdk/pkg/models" -) - -func Startup(protocol *models.Protocol, builder models.ConnectorBuilder) { - ctx, cancel := context.WithCancel(context.Background()) - - ds := &DeviceService{} - ds.Initialize(ctx, cancel, protocol, builder) - ds.Serve() -} diff --git a/pkg/startup/startup.go b/pkg/startup/startup.go new file mode 100644 index 0000000..43612ae --- /dev/null +++ b/pkg/startup/startup.go @@ -0,0 +1,18 @@ +package startup + +import ( + "context" + "github.com/thingio/edge-device-driver/internal/driver" + "github.com/thingio/edge-device-std/models" +) + +func Startup(protocol *models.Protocol, builder models.DeviceTwinBuilder) { + ctx, cancel := context.WithCancel(context.Background()) + + ds, err := driver.NewDeviceDriver(ctx, cancel, protocol, builder) + if err != nil { + panic(err) + } + ds.Initialize() + ds.Serve() +} diff --git a/version/version.go b/version/version.go deleted file mode 100644 index 57bfd1d..0000000 --- a/version/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package version - -const Version = "v1"