From 537a84d69fe8484971ec261a5654bca129df8ce3 Mon Sep 17 00:00:00 2001 From: xufangyou Date: Thu, 25 Nov 2021 11:51:24 +0800 Subject: [PATCH] refactor code --- .gitignore | 21 ++ README.md | 6 +- config/common.go | 8 + config/define.go | 47 ++++ config/msgbus.go | 93 +++++++ docs/zh/CHANGELOG.md | 23 ++ docs/zh/README.md | 226 +++++++++++++++++ ...11\345\205\250\350\277\236\346\216\245.md" | 151 ++++++++++++ .../README.md" | 41 ++++ ...3\347\273\237\346\236\266\346\236\204.jpg" | Bin 0 -> 144073 bytes errors/errors.go | 140 +++++++++++ errors/errors_test.go | 85 +++++++ errors/type.go | 57 +++++ go.mod | 12 + logger/logger.go | 108 ++++++++ models/device.go | 19 ++ models/device_data.go | 171 +++++++++++++ models/device_twin.go | 43 ++++ models/product.go | 66 +++++ models/property.go | 27 ++ models/protocol.go | 16 ++ models/status.go | 25 ++ msgbus/define.go | 50 ++++ msgbus/message/message.go | 22 ++ msgbus/mqtt/bus.go | 184 ++++++++++++++ operations/define.go | 13 + operations/driver_client.go | 111 +++++++++ operations/driver_service.go | 231 ++++++++++++++++++ operations/manager_client.go | 225 +++++++++++++++++ operations/manager_service.go | 153 ++++++++++++ operations/operation.go | 73 ++++++ operations/operation_data.go | 90 +++++++ operations/operation_meta.go | 71 ++++++ operations/operation_topic.go | 125 ++++++++++ version/version.go | 8 + 35 files changed, 2740 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 config/common.go create mode 100644 config/define.go create mode 100644 config/msgbus.go create mode 100644 docs/zh/CHANGELOG.md create mode 100644 docs/zh/README.md create mode 100644 "docs/zh/\347\263\273\347\273\237\345\256\211\345\205\250/MQTTS: MQTT \344\275\277\347\224\250 TLS \345\273\272\347\253\213\345\256\211\345\205\250\350\277\236\346\216\245.md" create mode 100644 "docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/README.md" create mode 100644 "docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/\347\263\273\347\273\237\346\236\266\346\236\204.jpg" create mode 100644 errors/errors.go create mode 100644 errors/errors_test.go create mode 100644 errors/type.go create mode 100644 go.mod create mode 100644 logger/logger.go create mode 100644 models/device.go create mode 100644 models/device_data.go create mode 100644 models/device_twin.go create mode 100644 models/product.go create mode 100644 models/property.go create mode 100644 models/protocol.go create mode 100644 models/status.go create mode 100644 msgbus/define.go create mode 100644 msgbus/message/message.go create mode 100644 msgbus/mqtt/bus.go create mode 100644 operations/define.go create mode 100644 operations/driver_client.go create mode 100644 operations/driver_service.go create mode 100644 operations/manager_client.go create mode 100644 operations/manager_service.go create mode 100644 operations/operation.go create mode 100644 operations/operation_data.go create mode 100644 operations/operation_meta.go create mode 100644 operations/operation_topic.go create mode 100644 version/version.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae25089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Dependency +go.sum + +# IDEA +.idea \ No newline at end of file diff --git a/README.md b/README.md index 6ecb3f9..71db8a9 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# edge-device-std \ No newline at end of file +# edge-device-std + +## 文档 + +移步[中文文档](./docs/zh)。 \ No newline at end of file diff --git a/config/common.go b/config/common.go new file mode 100644 index 0000000..a452004 --- /dev/null +++ b/config/common.go @@ -0,0 +1,8 @@ +package config + +type CommonOptions struct { + DriverHealthCheckIntervalSecond int `json:"driver_health_check_interval_second" yaml:"driver_health_check_interval_second"` + DeviceHealthCheckIntervalSecond int `json:"device_health_check_interval_second" yaml:"device_health_check_interval_second"` + DeviceAutoReconnect bool `json:"device_auto_reconnect" yaml:"device_auto_reconnect"` + DeviceAutoReconnectIntervalSecond int `json:"device_auto_reconnect_interval_second" yaml:"device_auto_reconnect_interval_second"` +} diff --git a/config/define.go b/config/define.go new file mode 100644 index 0000000..de58818 --- /dev/null +++ b/config/define.go @@ -0,0 +1,47 @@ +package config + +import ( + "flag" + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" + "github.com/thingio/edge-device-std/errors" +) + +const ( + EnvPrefix = "eds" + FilePath = "etc" + FileName = "config" + FileFormat = "yaml" +) + +type Configuration struct { + CommonOptions CommonOptions `json:"common" yaml:"common"` + MessageBus MessageBusOptions `json:"msgbus" yaml:"msgbus"` +} + +func NewConfiguration() (*Configuration, errors.EdgeError) { + // read the configuration file path + var configPath string + flag.StringVar(&configPath, "cp", FilePath, "config file path, e.g. \"/etc\"") + var configName string + flag.StringVar(&configName, "cn", FileName, "config file name, e.g. \"config\", excluding the suffix") + flag.Parse() + + viper.SetEnvPrefix(EnvPrefix) + viper.AutomaticEnv() + viper.AddConfigPath(configPath) + viper.SetConfigName(configName) + viper.SetConfigType(FileFormat) + if err := viper.ReadInConfig(); err != nil { + return nil, errors.Configuration.Cause(err, "fail to read the configuration file") + } + + cfg := new(Configuration) + if err := viper.Unmarshal(cfg, func(dc *mapstructure.DecoderConfig) { + dc.TagName = FileFormat + }); err != nil { + return nil, errors.Configuration.Cause(err, "fail to unmarshal the configuration file") + } + + return cfg, nil +} diff --git a/config/msgbus.go b/config/msgbus.go new file mode 100644 index 0000000..058c8f9 --- /dev/null +++ b/config/msgbus.go @@ -0,0 +1,93 @@ +package config + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" +) + +type ( + MessageBusType = string + MessageBusProtocolType = string +) + +const ( + MessageBusTypeMQTT MessageBusType = "MQTT" + + MessageBusProtocolTypeTCP MessageBusProtocolType = "tcp" + MessageBusProtocolTypeSSL MessageBusProtocolType = "ssl" +) + +type MessageBusOptions struct { + Type MessageBusType `json:"type" yaml:"type"` + MQTT MQTTMessageBusOptions `json:"mqtt" yaml:"mqtt"` +} + +type MQTTMessageBusOptions 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"` + // Username is the username of the MQTT broker. + Username string `json:"username" yaml:"username"` + // Password is the password of the MQTT broker. + Password string `json:"password" yaml:"password"` + + // ConnectTimoutMillisecond indicates the timeout of connecting to the MQTT broker. + ConnectTimoutMillisecond int `json:"connect_timout_millisecond" yaml:"connect_timout_millisecond"` + // TokenTimeoutMillisecond indicates the timeout of mqtt token. + TokenTimeoutMillisecond int `json:"token_timeout_millisecond" yaml:"token_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"` + + // MethodCallTimeoutMillisecond indicates the timeout of method call. + MethodCallTimeoutMillisecond int `json:"method_call_timeout_millisecond" yaml:"method_call_timeout_millisecond"` + + WithTLS bool `json:"with_tls" yaml:"with_tls"` + CAPath string `json:"ca_path" yaml:"ca_path"` + CertPath string `json:"cert_path" yaml:"cert_path"` + KeyPath string `json:"key_path" yaml:"key_path"` +} + +func (o *MQTTMessageBusOptions) NewTLSConfig() (*tls.Config, error) { + cert, rootPool, err := o.loadTLSConfig() + if err != nil { + return nil, err + } + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: rootPool, + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, + }, nil +} + +func (o *MQTTMessageBusOptions) loadTLSConfig() (tls.Certificate, *x509.CertPool, error) { + cert, err := tls.LoadX509KeyPair(o.CertPath, o.KeyPath) + if err != nil { + return cert, nil, fmt.Errorf("fail to load the certificate files: %s", err.Error()) + } + rootPool := x509.NewCertPool() + caCert, err := ioutil.ReadFile(o.CAPath) + if err != nil { + return cert, nil, fmt.Errorf("fail to load the root ca file: %s", err.Error()) + } + ok := rootPool.AppendCertsFromPEM(caCert) + if !ok { + return cert, nil, fmt.Errorf("fail to parse the root ca file") + } + return cert, rootPool, nil +} + +func (o *MQTTMessageBusOptions) GetBroker() string { + var protocolType MessageBusProtocolType + if o.WithTLS { + protocolType = MessageBusProtocolTypeSSL + } else { + protocolType = MessageBusProtocolTypeTCP + } + return fmt.Sprintf("%s://%s:%d", protocolType, o.Host, o.Port) +} diff --git a/docs/zh/CHANGELOG.md b/docs/zh/CHANGELOG.md new file mode 100644 index 0000000..b2a8bca --- /dev/null +++ b/docs/zh/CHANGELOG.md @@ -0,0 +1,23 @@ +# CHANGELOG + +## 2021.12 + +1. 确定[系统基本架构](./系统架构/README.md) +2. [设备驱动示例(生成随机数)](https://github.com/thingio/edge-randnum-driver) +3. 新增特性: + - 支持 command-line flag 指定配置文件路径,如 `xxx -cp etc -cn config` 表示指定配置文件为 `./etc/config.yaml` + - 定义 MessageBus 接口封装 MQ 操作逻辑,并提供了 MQTTMessageBus 作为默认支持 + - 定义 MetaStore 接口封装元数据操作逻辑,并提供了 FileMetaStore 作为默认支持 + - 区分单向和双向操作,对于双向操作(基于 MessageBus 的 Call 方法实现)而言,使用全局唯一的 UUID - `ReqID` 标识同一组操作 + - 区分属性软读(soft-read)和硬读(hard-read),前者会从设备影子(DeviceTwin)中获取该属性的缓存值,后者会直接从真实设备中读取该属性的值 + - 支持 MQTTS 配置,可参考 [MQTT 使用 TLS 建立安全连接](./系统安全/MQTTS:%20MQTT%20使用%20TLS%20建立安全连接.md) + - 将设备数据封装为类似于 DeviceData 的结构体,包含属性名、数据类型、数据值、数据采集时间戳等信息 + +## TODO + +1. 支持设备级别的 MQTT(S) QoS 配置 +2. 为 `edge-device-manager` & `edge-device-accessor` 提供 HTTP(S) & WS(S) 访问接口及 Client,与业务无关的代码封装在 `edge-device-std` + 中提供基础服务 +3. `edge-device-accessor` 确定业务边界、架构设计与代码实现 +4. 调整 Protocol、Product 与 Device 元数据结构的定义,如 DeviceStatus 是动态消息,只能由系统更改,不应该放在 Device 这样的元数据中 +5. 调研 TCP & MQTT(mosquitto) 如何切分数据包,主要是要确定当传输一个大数据文件(如图片)时,MQTT 如何将其拆分,TCP 如何将其拆分? \ No newline at end of file diff --git a/docs/zh/README.md b/docs/zh/README.md new file mode 100644 index 0000000..cfc283f --- /dev/null +++ b/docs/zh/README.md @@ -0,0 +1,226 @@ +# EDS + +**EDS**(Edge Device Std)是为 IOT 场景设计并开发的设备接入层 SDK,提供快速将多种协议的设备接入平台的能力。 + +## 特性 + +1. 轻量:只使用 MQTT 作为设备驱动服务与设备管理服务之间数据交换的中间件,无需引入多余组件; +2. 通用: + 1. 将 MQTT 封装为 MessageBus,向上层模块提供支持; + 2. 基于 MessageBus 定义和实现了通用的**元数据操作**与**物模型操作**规范。 + +## 术语 + +### 物模型 + +[物模型](https://blog.csdn.net/zjccoder/article/details/107050046),作为一类设备的抽象,描述了设备的: + +- 属性(property):用于描述设备状态,支持读取和写入; +- 方法(method):设备可被外部调用的能力或方法,可设置输入和输出参数,相比与属性,方法可通过一条指令实现更复杂的业务逻辑; +- 事件(event):用于描述设备主动上报的事件,可包含多个输出参数,参数必须是某个“属性”。 + +产品(Product):即物模型。 + +设备(Device):与真实设备一一对应,必须属于并且仅属于某一个产品。 + +### 元数据 + +元数据包括协议(Protocol)、产品(Product)及设备(Device): + +- 协议对应了一个特定协议的描述信息; +- 产品对应了一个特定产品的描述信息; +- 设备对应了一个特定设备的描述信息。 + +### 设备驱动服务 + +负责一种特定设备协议的接入实现,主要功能包括: + +- 协议注册:将当前设备协议注册到设备管理服务; +- 设备初始化:从设备管理服务获取产品 & 设备元数据,加载设备驱动; +- 元数据监听:监听设备管理服务产品 & 设备元数据的变更,加载/卸载/重加载驱动; +- 设备属性读写:从真实设备或驱动缓存中读取产品定义的属性数据; +- 设备方法调用:调用设备支持的方法; +- 设备事件监听:将设备主动推送的数据转发到 MessageBus 中; +- 设备状态检查:检查真实设备的健康状态,并周期性推送到 MessageBus 中; +- 设备驱动服务状态检查:检查设备驱动服务的健康状态,并周期性推送到 MessageBus 中。 + +### 设备管理服务 + +负责管理接入的设备驱动服务,主要功能包括: + +- 协议管理:接收设备服务的注册请求; +- 产品管理:基于特定协议定义产品 & 产品 CRUD; +- 设备管理:基于特定产品定义设备 & 设备 CRUD。 + +## Topic 约定 + +因为 EDS 基于 MQTT 实现数据交换,所以我们基于 MQTT 的 Topic & Payload 概念定义了我们自己的数据格式及通信规范。 + +### 物模型操作 + +对于物模型来说,Topic 的一般格式为 `DATA/${Version}/${OptMode}/${ProtocolID}/${ProductID}/${DeviceID}/${FuncID}/${OptType}[/${ReqID}]` +: + +- `Version`:DATA Topic 的版本(与 META Topic 的版本相互独立),在 `edge-device-std/version` 中定义; +- `OptMode`,操作模式,可选 `DOWN | UP | UP-ERR`: + - `DOWN`:对应设备管理服务(北向)向设备驱动服务(南向)发起的操作; + - `UP` 对应设备驱动服务(南向)向设备管理服务(北向)反馈的正常数据; + - `UP-ERR` 对应设备驱动服务(南向)向设备管理服务(北向)反馈的错误信息; +- `ProtocolID`:协议元数据的 UUID,协议(设备驱动服务)唯一; +- `ProductID`:产品元数据的 UUID,产品唯一; +- `DeviceID`:设备元数据的 UUID,设备唯一; +- `FuncID`:当前操作的 ID,对应 `property | method | event` 的 ID; +- `OptType`,操作类型: + - 双向操作: + - 对于 `property`,可选 `READ | HARD-READ | WRITE`,分别对应于设备属性的软读、硬读和写入操作; + - 对于 `method`,可选 `CALL`,对应设备方法的调用操作; + - 单向操作: + - 对于 `property`,可选 `PROP`,对应设备属性的上报操作; + - 对于 `event`,可选 `EVENT`,对应设备事件的上报操作(与 `PROP` 的区别在于,`PROP` 由设备驱动主动向真实设备获取,而 `EVENT` 由真实设备主动向设备驱动推送); + - 特别地,对于设备状态检测功能而言,定义了特定的操作类型 `STATUS`,用于设备状态的上报操作; +- `ReqID`:对于双向操作而言,如果不能绑定属于该操作的请求与响应,则当多个操作并发执行时,会导致请求与响应的混乱, 因此需要一个唯一的 UUID 来表示标识同一组操作。 + +#### 示例 + +1. 从设备读取属性(软读): + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ READ/+ +PUB: DATA/${VERSION}/ DOWN/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ READ/${req_id} + +# 设备驱动服务(南向) +SUB: DATA/${VERSION}/ DOWN/${prot_id}/ +/+/+/ READ/+ +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ READ/${req_id} --payload ${props} +``` + +2. 从设备读取属性(硬读): + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ HARD-READ/+ +PUB: DATA/${VERSION}/ DOWN/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ HARD-READ/${req_id} + +# 设备驱动服务(南向) +SUB: DATA/${VERSION}/ DOWN/${prot_id}/ +/+/+/ HARD-READ/+ +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ HARD-READ/${req_id} --payload ${props} +``` + +3. 往设备写入属性: + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ WRITE/+ +PUB: DATA/${VERSION}/ DOWN/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ WRITE/${req_id} --payload ${props} + +# 设备驱动服务(南向) +SUB: DATA/${VERSION}/ DOWN/${prot_id}/ +/+/+/ WRITE/+ +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ WRITE/${req_id} --payload ${result} +``` + +4. 调用设备方法: + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ CALL/+ +PUB: DATA/${VERSION}/ DOWN/${prot_id}/ ${prod_id}/${dev_id}/${method_id}/ CALL/${req_id} --payload ${ins} + +# 设备驱动服务(南向) +SUB: DATA/${VERSION}/ DOWN/${prot_id}/ +/+/+/ CALL/+ +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/${method_id}/ CALL/${req_id} --payload ${outs} +``` + +5. 设备属性推送: + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ PROP/+ + +# 设备驱动服务(南向) +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/${prop_id}/ PROP/* --payload ${props} +``` + +6. 设备事件推送: + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ EVENT/+ + +# 设备驱动服务(南向) +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/${event_id}/ EVENT --payload ${props} +``` + +7. 设备状态推送: + +```text +# 设备管理服务(北向) +SUB: DATA/${VERSION}/ UP/${prot_id}/ +/+/+/ STATUS/+ + +# 设备驱动服务(南向) +PUB: DATA/${VERSION}/ UP/${prot_id}/ ${prod_id}/${dev_id}/-/ STATUS/* --payload ${driver_status} +``` + +### 元数据操作 + +对于元数据操作来说,Topic 的一般格式为 `META/${Version}/${OptMode}/${ProtocolID}/${OptType}[/${ID}]`: + +- `Version`:META Topic 的版本(与 DATA Topic 的版本相互独立),在 `edge-device-std/version` 中定义; +- `OptMode`,操作模式,可选 `DOWN | UP`: + - `DOWN`:对应设备管理服务(北向)向设备驱动服务(南向)发起的操作; + - `UP` 对应设备驱动服务(南向)向设备管理服务(北向)反馈的正常数据; +- `OptType`,操作类型,可选 `INIT | PRODUCT | DEVICE`: + - 单向操作: + - `PRODUCT`,对应于产品的增/删/改操作,由设备驱动服务根据缓存判断具体操作类型: + - 首先判断 Payload 是否为空,如果为空则表示产品删除; + - 否则判断设备驱动服务缓存是否存在 `${prod_id}`,如果不存在则表示产品创建; + - 否则表示产品更新; + - `DEVICE`,对应于设备的增/删/改操作,由设备驱动服务根据缓存判断具体操作类型: + - 首先判断 Payload 是否为空,如果为空则表示设备删除; + - 否则判断设备驱动服务缓存是否存在 `${prod_id}`,如果不存在则表示设备创建; + - 否则表示设备更新; + - `STATUS`,对应于设备驱动服务的状态上报操作,需要包含状态码、协议元数据、上次 `APPEND` 操作的时间戳(初始为 0); + - `APPEND`,对应于设备管理服务向设备驱动服务发起的产品 & 设备元数据追加操作,设备管理服务会根据 `STATUS` 中的时间戳向设备驱动服务增量发送更新的产品 & 设备; +- `ID`: + - 对于单向操作而言,用于指定产品/设备的增/删/改的操作对象。 + +#### 示例 + +1. 产品元数据更新: + +```text +# 设备管理服务(北向) +PUB: META/${VERSION}/ DOWN/${prot_id}/ PRODUCT/${prod_id} --payload ${product} + +# 设备驱动服务(南向) +SUB: META/${VERSION}/ DOWN/${prot_id}/ PRODUCT/+ +``` + +2. 设备元数据更新: + +```text +# 设备管理服务(北向) +PUB: META/${VERSION}/ DOWN/${prot_id}/ DEVICE/${dev_id} --payload ${device} + +# 设备驱动服务(南向) +SUB: META/${VERSION}/ DOWN/${prot_id}/ DEVICE/+ +``` + +3. 设备驱动服务状态推送: + +```text +# 设备管理服务(北向) +SUB: META/${VERSION}/ UP/${prot_id}/ STATUS/+ + +# 设备驱动服务(南向) +PUB: META/${VERSION}/ UP/${prot_id}/ STATUS/* --payload ${driver_status} +``` + +4. 驱动服务元数据初始化: + +```text +# 设备管理服务(北向) +PUB: META/${VERSION}/ DOWN/${prot_id}/ INIT/* --payload ${products & devices} + +# 设备驱动服务(南向) +SUB: META/${VERSION}/ DOWN/${prot_id}/ INIT/+ +``` \ No newline at end of file diff --git "a/docs/zh/\347\263\273\347\273\237\345\256\211\345\205\250/MQTTS: MQTT \344\275\277\347\224\250 TLS \345\273\272\347\253\213\345\256\211\345\205\250\350\277\236\346\216\245.md" "b/docs/zh/\347\263\273\347\273\237\345\256\211\345\205\250/MQTTS: MQTT \344\275\277\347\224\250 TLS \345\273\272\347\253\213\345\256\211\345\205\250\350\277\236\346\216\245.md" new file mode 100644 index 0000000..407a171 --- /dev/null +++ "b/docs/zh/\347\263\273\347\273\237\345\256\211\345\205\250/MQTTS: MQTT \344\275\277\347\224\250 TLS \345\273\272\347\253\213\345\256\211\345\205\250\350\277\236\346\216\245.md" @@ -0,0 +1,151 @@ +# MQTTS:MQTT 使用 TLS 建立安全连接 + +本文以 Docker 镜像的形式部署 Mosquitto 为例进行演示。 + +测试环境: + +- ~~ARCH : x86_64~~ +- ~~OS : Ubuntu 20.04.3 LTS~~ +- Docker : Docker version 20.10.11, build dea9396 +- Docker Compose : docker-compose version 1.25.5, build 8a1c60f6 +- Mosquitto Docker Image : eclipse-mosquitto:2.0-openssl + +## TLS 证书生成 + +```shell + +mkdir ca && cd ca +# 生成 CA 私钥 +# -des3 表示给生成的 RSA 私钥加密,需要设置密码,在使用该私钥时需要输入密码 +openssl genrsa -des3 -out ca.key 2048 +# 生成 CA 证书 +openssl req -new -x509 -days 3650 -key ca.key -out ca.crt + +mkdir broker && cd broker +# 生成 broker 端私钥 +# 该命令将产生一个不加密的 RSA 私钥,其中参数 2048 表示私钥的长度 +# 如果需要为产生的 RSA 私钥加密,需加上 -des3,对私钥文件加密之后,后续使用该密钥的时候都要求输入密码 +openssl genrsa -out broker.key 2048 +# 生成 broker 端请求文件 +# 该命令使用上一步产生的私钥生成一个签发证书所需要的请求文件 broker.csr,使用该文件向 CA 发送请求才会得到 CA 签发的证书 +openssl req -new -out broker.csr -key broker.key +# 生成 broker 端证书 +# 该命令将使用 broker 私钥、CA 证书、CA 私钥向 CA 请求生成一个证书文件 broker.crt +openssl x509 -req -days 3650 -in broker.csr -CA ../ca.crt -CAkey ../ca.key -CAcreateserial -out broker.crt + +cd .. && mkdir client && cd client +# 生成 client 端私钥 +openssl genrsa -out client.key 2048 +# 生成 client 端请求文件 +openssl req -new -out client.csr -key client.key +# 生成 client 端证书 +openssl x509 -req -days 3650 -in client.csr -CA ../ca.crt -CAkey ../ca.key -CAcreateserial -out client.crt + +cd .. +# 验证证书 +openssl x509 -noout -text -in ca.crt +openssl x509 -noout -text -in ./broker/broker.crt +openssl x509 -noout -text -in ./client/client.crt +``` + +`注:生成 CA 证书时指定的 Common Name 必须和 Broker & client 不一样。此外,当使用双向认证时,Broker & client 的 Common Name 需要统一为 broker 所在服务器域名,这里使用 mosquitto。` + +## Mosquitto Broker 开安全 + +### 创建用户 + +```shell +# 使用 mosquitto_passwd 命令创建用户,需要在 mosquitto.conf 指定 password_file +mosquitto_passwd -c ./passwd_file admin +``` + +### Mosquitto 配置 + +> mosquitto.conf + +```text +listener 8883 # TLS 端口 + +cafile /mosquitto/config/ca/ca.crt +certfile /mosquitto/config/ca/broker.crt +keyfile /mosquitto/config/ca/broker.key +password_file /mosquitto/config/passwd_file +require_certificate true +allow_anonymous false +``` + +### Mosquitto 启动脚本 + +> 目录结构 + +```shell +. +├── conf +│   ├── ca +│   │   ├── broker +│   │   │   ├── broker.crt +│   │   │   ├── broker.key +│   │   │   └── ca.crt +│   ├── mosquitto.conf +│   ├── mosquitto.conf.template +│   └── passwd_file +├── data +│   └── mosquitto.db +├── docker-compose.yml +└── log + └── mosquitto.log + +6 directories, 21 files +``` + +> docker-compose.yml + +```yaml +version: "2" + +services: + mosquitto-openssl: + image: eclipse-mosquitto:2.0-openssl + container_name: mosquitto-openssl + volumes: + - ./conf/mosquitto.conf:/mosquitto/config/mosquitto.conf + - ./conf/passwd_file:/mosquitto/config/passwd_file + - ./conf/ca/broker/:/mosquitto/config/ca/ + - ./data/:/mosquitto/data/ + - ./log/:/mosquitto/log/ + ports: + - 8883:8883 + privileged: true + restart: on-failure +``` + + + +### 测试 + +#### 单向认证(实际使用时单向认证即可) + +> 客户端不对服务器的证书链和 Host 进行校验。 + +```shell +mosquitto_sub -h 172.16.251.163 -p 8883 -u admin -P 123456 --tls-version tlsv1.3 --debug --cafile ca.crt --cert client.crt --key client.key -t /greet --insecure + +mosquitto_pub -h 172.16.251.163 -p 8883 -u admin -P 123456 --tls-version tlsv1.3 --debug --cafile ca.crt --cert client.crt --key client.key -t /greet -m "hello world" --insecure +``` + +#### 双向认证 + +> 客户端须对服务器的证书链和 Host 进行校验,客户端需要设置 DNS:`127.16.251.163 mosquitto`。 + +```shell +# 设置 DNS +# sudo vim /etc/hosts +# 172.16.251.163 mosquitto +mosquitto_sub -h mosquitto -p 8883 -u admin -P 123456 --tls-version tlsv1.3 --debug --cafile ca.crt --cert client.crt --key client.key -t /greet + +mosquitto_pub -h mosquitto -p 8883 -u admin -P 123456 --tls-version tlsv1.3 --debug --cafile ca.crt --cert client.crt --key client.key -t /greet -m "hello world" +``` + +## 参考 + +1. [MQTTS: How to use MQTT with TLS?](https://openest.io/en/2020/01/03/mqtts-how-to-use-mqtt-with-tls/) \ No newline at end of file diff --git "a/docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/README.md" "b/docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/README.md" new file mode 100644 index 0000000..6cce5a5 --- /dev/null +++ "b/docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/README.md" @@ -0,0 +1,41 @@ +# 系统架构 + +![系统架构图](./系统架构.jpg) + +## 系统组件 + +1. [edge-device-std](https://github.com/thingio/edge-device-std) 为系统其它组件提供基础服务: + + - `config` 提供配置支持,基于 [viper](https://github.com/spf13/viper) 实现读取配置文件的功能; + - `version` 定义版本信息,版本信息可用来标识通信格式等; + - `logger` 提供日志支持; + - `models` 定义公共接口; + - `msgbus` 封装了 MQ 的操作逻辑,向上层数据操作提供基础通信能力; + - `operations` 基于底层 MessageBus 提供的基础通信能力封装了元数据操作和物模型操作,并分别为 `manager` 及 `driver` 提供了客户端实现; + +2. [edge-device-driver](https://github.com/thingio/edge-device-driver) 提供设备驱动服务快速构建能力: + + - `interval/driver` 定义设备驱动服务; + - `pkg/startup` 初始化并启动设备驱动服务; + +3. [edge-device-manager](https://github.com/thingio/edge-device-manager) 提供设备管理服务快速构建能力: + + - `etc`: + - `resources`: + - `products`:产品元数据的默认存放路径; + - `devices`:设备元数据的默认存放路径; + - `pkg`: + - `manager`: 定义设备管理服务; + - `metastore` 定义元数据管理接口(提供基于本地文件系统的默认实现),用户可基于该接口指定其它元数据托管媒介; + - `startup` 初始化并启动设备管理服务; + +4. [edge-device-accessor](https://github.com/thingio/edge-device-accessor) 提供持久化和访问(已持久化)设备数据的能力; + +5. [edge-randnum-driver](https://github.com/thingio/edge-randnum-driver) 设备驱动示例(生成随机数): + + - `driver`: + - `protocol.go` 定义驱动服务的设备协议元数据; + - `driver.go` 实现 [DeviceTwin](../../../models/device_twin.go) + 接口,并提供设备影子构建方法 [DeviceTwinBuilder](../../../models/device_twin.go); + - `etc/config.yaml`:设备驱动服务配置文件的默认路径; + - `main.go`:使用 `edge-device-driver` 的 `startup.Startup()` 初始化并启动驱动服务。 \ No newline at end of file diff --git "a/docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/\347\263\273\347\273\237\346\236\266\346\236\204.jpg" "b/docs/zh/\347\263\273\347\273\237\346\236\266\346\236\204/\347\263\273\347\273\237\346\236\266\346\236\204.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..6235f707e30b0633edcce2ce6782583284c8b789 GIT binary patch literal 144073 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 v{{nE%FTT@Zn " + e.wrapped.Error() + } else { + return e.wrapped.Error() + } +} + +// Message returns the first level error message without further details. +func (e *CommonEdgeError) Message() string { + if e.Msg == "" && e.wrapped != nil { + if w, ok := e.wrapped.(*CommonEdgeError); ok { + return w.Message() + } else { + return e.wrapped.Error() + } + } + + return e.Msg +} + +// StackMessages returns the call stack information of the error function for debugging. +func (e *CommonEdgeError) StackMessages() string { + if e.wrapped == nil { + return fmt.Sprintf("%s\n\n%s", e.Msg, e.stackMessages) + } + + if w, ok := e.wrapped.(*CommonEdgeError); ok { + return fmt.Sprintf("%s\n\n%s%s", e.Msg, e.stackMessages, w.StackMessages()) + } else { + return fmt.Sprintf("%s\n\n%s%s", e.Msg, e.stackMessages, e.wrapped.Error()) + } +} + +// Type returns the type of this CommonEdgeError. +func (e *CommonEdgeError) Type() *ErrType { + return e.ErrType +} + +// Unwrap returns an errors wrapped in this CommonEdgeError. +func (e *CommonEdgeError) Unwrap() error { + return e.wrapped +} + +func (e *CommonEdgeError) addMsg(format string, args ...interface{}) *CommonEdgeError { + if format != "" { + e.Msg = fmt.Sprintf(format, args...) + "\n\t -> " + e.Msg + } + return e +} + +func Unwrap(err error) *CommonEdgeError { + if err == nil { + return nil + } + + if e, ok := err.(*CommonEdgeError); ok { + return e + } + return Unknown.Error(err.Error()).(*CommonEdgeError) +} + +func TypeOf(err error) ErrType { + var e *CommonEdgeError + if !errors.As(err, &e) { + return Unknown + } + if e.ErrType.Code != Unknown.Code || e.wrapped == nil { + return *e.ErrType + } + return TypeOf(e.wrapped) +} + +func getStackMessages() string { + pc, filename, line, _ := runtime.Caller(3) + return fmt.Sprintf("%s\n\t-[%s:%d]\n", runtime.FuncForPC(pc).Name(), filename, line) +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 0000000..45b2fb5 --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,85 @@ +package errors + +import ( + "fmt" + "testing" +) + +var ( + RawEmptyError error = nil + L1EmptyErrorWithoutMessage = NewCommonEdgeError(Unknown, "", RawEmptyError) + L1EmptyErrorWithMessage = NewCommonEdgeError(Unknown, "with message", RawEmptyError) + L2EmptyErrorWithoutMessage = NewCommonEdgeError(Unknown, "", L1EmptyErrorWithoutMessage) + L2EmptyErrorWithMessage = NewCommonEdgeError(Unknown, "with message", L1EmptyErrorWithMessage) + + RawError = fmt.Errorf("raw errors") + L1ErrorWithoutMessage = NewCommonEdgeError(Unknown, "", RawError) + L1ErrorWithMessage = NewCommonEdgeError(Unknown, "with message", RawError) + L2ErrorWithoutMessage = NewCommonEdgeError(Unknown, "", L1ErrorWithoutMessage) + L2ErrorWithMessage = NewCommonEdgeError(Unknown, "with message", L1ErrorWithMessage) + L2ErrorMixedWithMessage = NewCommonEdgeError(Unknown, "message", L1ErrorWithoutMessage) + L3ErrorMixedWithoutMessage = NewCommonEdgeError(Unknown, "", L2ErrorMixedWithMessage) + L4ErrorMixedWithMessage = NewCommonEdgeError(Unknown, "message", L3ErrorMixedWithoutMessage) +) + +func TestCommonEdgeError_Error(t *testing.T) { + tests := []struct { + name string + err EdgeError + want string + }{ + {"Get all levels of error messages from an empty error without message", L1EmptyErrorWithoutMessage, L1EmptyErrorWithoutMessage.Msg}, + {"Get all levels of error messages from an empty error with message", L1EmptyErrorWithMessage, L1EmptyErrorWithMessage.Msg}, + {"Get all levels of error messages from an EdgeError with 1 empty error wrapped without message", L2EmptyErrorWithoutMessage, L2EmptyErrorWithoutMessage.Msg}, + {"Get all levels of error messages from an EdgeError with 1 empty error wrapped with message", L2EmptyErrorWithMessage, + fmt.Sprintf("%s\n\t -> %s", L2EmptyErrorWithMessage.Msg, L1EmptyErrorWithMessage.Error())}, + + {"Get all levels of error messages from an error without message", L1ErrorWithoutMessage, RawError.Error()}, + {"Get all levels of error messages from an error with message", L1ErrorWithMessage, + fmt.Sprintf("%s\n\t -> %s", L1ErrorWithMessage.Msg, RawError.Error())}, + {"Get all levels of error message from an EdgeError with 1 error wrapped without message", L2ErrorWithoutMessage, L2ErrorWithoutMessage.Error()}, + {"Get all levels of error message from an EdgeError with 1 error wrapped with message", L2ErrorWithMessage, + fmt.Sprintf("%s\n\t -> %s", L2ErrorWithMessage.Msg, L1ErrorWithMessage.Error())}, + {"Get all levels of error message from an EdgeError with 1 error wrapped with message fixed", L2ErrorMixedWithMessage, + fmt.Sprintf("%s\n\t -> %s", L2ErrorMixedWithMessage.Msg, L1ErrorWithoutMessage.Error())}, + {"Get all levels of error message from an EdgeError with 2 error wrapped with message", L3ErrorMixedWithoutMessage, L2ErrorMixedWithMessage.Error()}, + {"Get all levels of error message from an EdgeError with 3 error wrapped with message", L4ErrorMixedWithMessage, + fmt.Sprintf("%s\n\t -> %s", L4ErrorMixedWithMessage.Msg, L3ErrorMixedWithoutMessage.Error())}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCommonEdgeError_Message(t *testing.T) { + tests := []struct { + name string + err EdgeError + want string + }{ + {"Get the first level error message from an empty error without message", L1EmptyErrorWithoutMessage, L1EmptyErrorWithoutMessage.Msg}, + {"Get the first level error message from an empty error with message", L1EmptyErrorWithMessage, L1EmptyErrorWithMessage.Msg}, + {"Get the first level error message from an EdgeError with 1 empty error wrapped without message", L2EmptyErrorWithoutMessage, L2EmptyErrorWithoutMessage.Msg}, + {"Get the first level error message from an EdgeError with 1 empty error wrapped with message", L2EmptyErrorWithMessage, L2EmptyErrorWithMessage.Msg}, + + {"Get the first level error message from an error without message", L1ErrorWithoutMessage, L1ErrorWithoutMessage.wrapped.Error()}, + {"Get the first level error message from an error with message", L1ErrorWithMessage, L1ErrorWithMessage.Msg}, + {"Get the first level error message from an EdgeError with 1 error wrapped without message", L2ErrorWithoutMessage, L2ErrorWithoutMessage.wrapped.Error()}, + {"Get the first level error message from an EdgeError with 1 error wrapped with message", L2ErrorWithMessage, L2ErrorWithMessage.Msg}, + {"Get the first level error message from an EdgeError with 1 error wrapped with message fixed", L2ErrorMixedWithMessage, L2ErrorMixedWithMessage.Msg}, + {"Get the first level error message from an EdgeError with 2 error wrapped with message", L3ErrorMixedWithoutMessage, L2ErrorMixedWithMessage.Msg}, + {"Get the first level error message from an EdgeError with 3 error wrapped with message", L4ErrorMixedWithMessage, L4ErrorMixedWithMessage.Msg}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.Message(); got != tt.want { + t.Errorf("Msg() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/errors/type.go b/errors/type.go new file mode 100644 index 0000000..53edd26 --- /dev/null +++ b/errors/type.go @@ -0,0 +1,57 @@ +package errors + +import ( + "fmt" + "net/http" +) + +var ( + Types = map[int]ErrType{} + + BadRequest = NewType(http.StatusBadRequest, "BadRequest") + NotFound = NewType(http.StatusNotFound, "BadRequest") + MethodNotAllowed = NewType(http.StatusMethodNotAllowed, "MethodNotAllowed") + Internal = NewType(http.StatusInternalServerError, "Internal") + Unknown = NewType(999, "Unknown") + Configuration = NewType(100000, "Configuration") + MessageBus = NewType(200000, "MessageBus") + Driver = NewType(300000, "Driver") + DeviceTwin = NewType(400000, "DeviceTwin") + MetaStore = NewType(500000, "MetaStore") +) + +type ErrType struct { + Code int `json:"code"` + Msg string `json:"Msg"` +} + +func (t *ErrType) Error(format string, args ...interface{}) EdgeError { + return &CommonEdgeError{ + Msg: fmt.Sprintf(format, args...), + wrapped: nil, + ErrType: t, + stackMessages: getStackMessages(), + } +} + +func (t *ErrType) Cause(err error, format string, args ...interface{}) EdgeError { + if err == nil { + return t.Error(format, args...) + } + + e := Unwrap(err) + if e.Type().Code == Unknown.Code { + e.Type().Code = t.Code + } + e.stackMessages += getStackMessages() + return e.addMsg(format, args...) +} + +func NewType(code int, msg string) ErrType { + if _, ok := Types[code]; ok { + panic(fmt.Errorf("duplicated type code: %d", code)) + } + tp := ErrType{code, msg} + Types[code] = tp + return tp +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d2d0940 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/thingio/edge-device-std + +require ( + github.com/banzaicloud/logrus-runtime-formatter v0.0.0-20190729070250-5ae5475bae5e // indirect + github.com/eclipse/paho.mqtt.golang v1.3.5 + github.com/mitchellh/mapstructure v1.4.2 + github.com/rs/xid v1.3.0 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/viper v1.9.0 +) + +go 1.16 diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..b9961dc --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,108 @@ +package logger + +import ( + "github.com/sirupsen/logrus" + "os" +) + +type LogLevel string + +const ( + DebugLevel LogLevel = "debug" + InfoLevel LogLevel = "info" + WarnLevel LogLevel = "warn" + ErrorLevel LogLevel = "errors" + 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 { + 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/models/device.go b/models/device.go new file mode 100644 index 0000000..4d8b719 --- /dev/null +++ b/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/models/device_data.go b/models/device_data.go new file mode 100644 index 0000000..67c9656 --- /dev/null +++ b/models/device_data.go @@ -0,0 +1,171 @@ +package models + +import ( + "fmt" + "reflect" + "time" +) + +type DeviceDataWrapper struct { + ProductID string + DeviceID string + FuncID string + Properties map[ProductPropertyID]*DeviceData +} + +type DeviceData struct { + Name string `json:"name"` // the name of the data + Type string `json:"type"` // the type of the raw value + Value interface{} `json:"value"` // raw value + Ts time.Time `json:"ts"` // the timestamp of reading the raw value from the real device +} + +func NewDeviceData(name string, valueType PropertyValueType, value interface{}) (*DeviceData, error) { + if err := validate(valueType, value); err != nil { + return nil, fmt.Errorf("fail to new DeviceData, because %s", err.Error()) + } + + return &DeviceData{ + Name: name, + Type: valueType, + Value: value, + Ts: time.Now(), + }, nil +} + +func (d *DeviceData) String() string { + return fmt.Sprintf("Data: %s, %s:%s", d.Name, d.Type, d.ValueToString()) +} + +// ValueToString returns the string format of the Value. +func (d *DeviceData) ValueToString() string { + return fmt.Sprintf("%v", d.Value) +} + +// IntValue returns the Value in string type, and returns errors if the Type is not PropertyValueTypeInt. +func (d *DeviceData) IntValue() (int64, error) { + var value int64 + if d.Type != PropertyValueTypeInt { + return value, fmt.Errorf("the expecting type is %s, but the pre-defined type is %s", + PropertyValueTypeInt, d.Type) + } + + switch d.Value.(type) { + case int, int8, int16, int32, int64: + return d.Value.(int64), nil + case float32, float64: + return int64(d.Value.(float64)), nil + default: + return value, fmt.Errorf("fail to parse value '%v' using type %s, raw type is %s", + d.Value, d.Type, reflect.TypeOf(d.Value)) + } +} + +// UintValue returns the Value in string type, and returns errors if the Type is not PropertyValueTypeUint. +func (d *DeviceData) UintValue() (uint64, error) { + var value uint64 + if d.Type != PropertyValueTypeUint { + return value, fmt.Errorf("the expecting type is %s, but the pre-defined type is %s", + PropertyValueTypeUint, d.Type) + } + switch d.Value.(type) { + case uint, uint8, uint16, uint32, uint64: + return d.Value.(uint64), nil + case float32, float64: + return uint64(d.Value.(float64)), nil + default: + return value, fmt.Errorf("fail to parse value '%v' using type %s, raw type is %s", + d.Value, d.Type, reflect.TypeOf(d.Value)) + } +} + +// FloatValue returns the Value in string type, and returns errors if the Type is not PropertyValueTypeFloat. +func (d *DeviceData) FloatValue() (float64, error) { + var value float64 + if d.Type != PropertyValueTypeFloat { + return value, fmt.Errorf("the expecting type is %s, but the pre-defined type is %s", + PropertyValueTypeFloat, d.Type) + } + + switch d.Value.(type) { + case float32, float64: + return d.Value.(float64), nil + default: + return value, fmt.Errorf("fail to parse value '%v' using type %s, raw type is %s", + d.Value, d.Type, reflect.TypeOf(d.Value)) + } +} + +// BoolValue returns the Value in string type, and returns errors if the Type is not PropertyValueTypeBool. +func (d *DeviceData) BoolValue() (bool, error) { + var value bool + if d.Type != PropertyValueTypeBool { + return value, fmt.Errorf("the expecting type is %s, but the pre-defined type is %s", + PropertyValueTypeBool, d.Type) + } + + switch d.Value.(type) { + case bool: + return d.Value.(bool), nil + default: + return value, fmt.Errorf("fail to parse value '%v' using type %s, raw type is %s", + d.Value, d.Type, reflect.TypeOf(d.Value)) + } +} + +// StringValue returns the Value in string type, and returns errors if the Type is not PropertyValueTypeString. +func (d *DeviceData) StringValue() (string, error) { + var value string + if d.Type != PropertyValueTypeString { + return value, fmt.Errorf("the expecting type is %s, but the pre-defined type is %s", + PropertyValueTypeString, d.Type) + } + + switch d.Value.(type) { + case string: + return d.Value.(string), nil + default: + return value, fmt.Errorf("fail to parse value '%v' using type %s, raw type is %s", + d.Value, d.Type, reflect.TypeOf(d.Value)) + } +} + +// validate checks whether value's real type is the given valueType. +func validate(valueType PropertyValueType, value interface{}) error { + var ok bool + switch valueType { + case PropertyValueTypeInt: + switch value.(type) { + case int, int8, int16, int32, int64: + ok = true + } + case PropertyValueTypeUint: + switch value.(type) { + case uint, uint8, uint16, uint32, uint64: + ok = true + } + case PropertyValueTypeFloat: + switch value.(type) { + case float32, float64: + ok = true + } + case PropertyValueTypeBool: + switch value.(type) { + case bool: + ok = true + } + case PropertyValueTypeString: + switch value.(type) { + case string: + ok = true + } + default: + return fmt.Errorf("unsupported value's type: %s", valueType) + } + + if !ok { + return fmt.Errorf("fail to parse value '%v' using type %s, raw type is %s", + value, valueType, reflect.TypeOf(value)) + } + return nil +} diff --git a/models/device_twin.go b/models/device_twin.go new file mode 100644 index 0000000..179e821 --- /dev/null +++ b/models/device_twin.go @@ -0,0 +1,43 @@ +package models + +import ( + "github.com/thingio/edge-device-std/logger" +) + +// DeviceTwin indicates a connection with a real device. +type DeviceTwin 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 + // HealthCheck is used to bin the connectivity with the real device. + HealthCheck() (*DeviceStatus, error) + + // Watch will read device's properties periodically with the specified policy. + Watch(bus chan<- *DeviceDataWrapper) error + // Read indicates soft read, it will read the specified property from the cache with TTL. + // Specially, when propertyID is "*", it indicates read all properties. + Read(propertyID ProductPropertyID) (map[ProductPropertyID]*DeviceData, error) + // HardRead indicates head read, it will read the specified property from the real device. + // Specially, when propertyID is "*", it indicates read all properties. + HardRead(propertyID ProductPropertyID) (map[ProductPropertyID]*DeviceData, error) + // Write will write the specified property to the real device. + Write(propertyID ProductPropertyID, values map[ProductPropertyID]*DeviceData) error + // Subscribe will subscribe the specified event, + // and put DataOperation including properties specified by the event into the bus. + Subscribe(eventID ProductEventID, bus chan<- *DeviceDataWrapper) 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 errors. + Call(methodID ProductMethodID, ins map[ProductPropertyID]*DeviceData) (outs map[ProductPropertyID]*DeviceData, err error) +} + +// DeviceTwinBuilder is used to create a new device twin using the specified product and device. +type DeviceTwinBuilder func(product *Product, device *Device) (DeviceTwin, error) diff --git a/models/product.go b/models/product.go new file mode 100644 index 0000000..fe06863 --- /dev/null +++ b/models/product.go @@ -0,0 +1,66 @@ +package models + +type ( + ProductFuncID = string // product functionality ID + ProductPropertyID = ProductFuncID // product property's functionality ID + ProductEventID = ProductFuncID // product event's functionality ID + ProductMethodID = ProductFuncID // product method's functionality ID +) + +const ( + DeviceDataMultiPropsID ProductFuncID = "*" +) + +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 ProductPropertyID `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 ProductEventID `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 ProductMethodID `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/models/property.go b/models/property.go new file mode 100644 index 0000000..6f26655 --- /dev/null +++ b/models/property.go @@ -0,0 +1,27 @@ +package models + +type ( + PropertyValueType = string + PropertyUIStyle = string +) + +const ( + PropertyValueTypeInt PropertyValueType = "int" + PropertyValueTypeUint PropertyValueType = "uint" + PropertyValueTypeFloat PropertyValueType = "float" + PropertyValueTypeBool PropertyValueType = "bool" + PropertyValueTypeString PropertyValueType = "string" +) + +type Property struct { + Name string `json:"name"` // Name 为属性的展示名称 + Desc string `json:"desc"` // Desc 为属性的描述, 通常以名称旁的?形式进行展示 + Type PropertyValueType `json:"type"` // Type 为该属性的数据类型 + UIStyle PropertyUIStyle `json:"style"` // UIStyle 为该属性在前端的展示样式 + 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/models/protocol.go b/models/protocol.go new file mode 100644 index 0000000..551d083 --- /dev/null +++ b/models/protocol.go @@ -0,0 +1,16 @@ +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/models/status.go b/models/status.go new file mode 100644 index 0000000..68d0186 --- /dev/null +++ b/models/status.go @@ -0,0 +1,25 @@ +package models + +type State = string + +const ( + DeviceStateConnected State = "connected" + DeviceStateReconnecting State = "reconnecting" + DeviceStateDisconnected State = "disconnected" + DeviceStateException State = "exception" + + DriverStateRunning State = "running" +) + +type DeviceStatus struct { + Device *Device `json:"device"` + State State `json:"state"` + StateDetail string `json:"state_detail"` +} + +type DriverStatus struct { + Hello bool `json:"hello"` + Protocol *Protocol `json:"protocol"` + State State `json:"state"` + StateDetail string `json:"state_detail"` +} diff --git a/msgbus/define.go b/msgbus/define.go new file mode 100644 index 0000000..b2291e2 --- /dev/null +++ b/msgbus/define.go @@ -0,0 +1,50 @@ +package msgbus + +import ( + "fmt" + "github.com/pkg/errors" + "github.com/thingio/edge-device-std/config" + "github.com/thingio/edge-device-std/logger" + "github.com/thingio/edge-device-std/msgbus/message" + "github.com/thingio/edge-device-std/msgbus/mqtt" +) + +func NewMessageBus(opts *config.MessageBusOptions, lg *logger.Logger) (MessageBus, error) { + var mb MessageBus + switch opts.Type { + case config.MessageBusTypeMQTT: + mqttOpts := &opts.MQTT + if mqttOpts == nil { + return nil, fmt.Errorf("the configuration for MQTT is required") + } + mmb, err := mqtt.NewMQTTMessageBus(mqttOpts, lg) + if err != nil { + return nil, err + } + mb = mmb + default: + return nil, fmt.Errorf("unsupported message bus type: %s", opts.Type) + } + + if err := mb.Connect(); err != nil { + return nil, errors.Wrap(err, "fail to connect to the message bus") + } + return mb, nil +} + +// MessageBus encapsulates all common manipulations based on MQTT. +type MessageBus interface { + IsConnected() bool + + Connect() error + + Disconnect() error + + Publish(o *message.Message) error + + Subscribe(handler message.Handler, topics ...string) error + + Unsubscribe(topics ...string) error + + Call(request *message.Message, rspTpc, errTpc string) (response *message.Message, err error) +} diff --git a/msgbus/message/message.go b/msgbus/message/message.go new file mode 100644 index 0000000..37ad8bb --- /dev/null +++ b/msgbus/message/message.go @@ -0,0 +1,22 @@ +package message + +import ( + "encoding/json" + "fmt" +) + +type Handler func(msg *Message) + +// Message is an intermediate data format between MQ and MessageBus. +type Message struct { + Topic string + Payload []byte +} + +func (m *Message) String() string { + return fmt.Sprintf("%s: %dbytes", m.Topic, len(m.Payload)) +} + +func (m *Message) Unmarshal(v interface{}) error { + return json.Unmarshal(m.Payload, v) +} diff --git a/msgbus/mqtt/bus.go b/msgbus/mqtt/bus.go new file mode 100644 index 0000000..e661273 --- /dev/null +++ b/msgbus/mqtt/bus.go @@ -0,0 +1,184 @@ +package mqtt + +import ( + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/thingio/edge-device-std/config" + "github.com/thingio/edge-device-std/errors" + "github.com/thingio/edge-device-std/logger" + "github.com/thingio/edge-device-std/msgbus/message" + "strconv" + "time" +) + +func NewMQTTMessageBus(opts *config.MQTTMessageBusOptions, lg *logger.Logger) (*MessageBus, errors.EdgeError) { + mmb := &MessageBus{ + tokenTimeout: time.Millisecond * time.Duration(opts.TokenTimeoutMillisecond), + callTimeout: time.Millisecond * time.Duration(opts.MethodCallTimeoutMillisecond), + qos: opts.QoS, + routes: make(map[string]message.Handler), + logger: lg, + } + if err := mmb.setClient(opts); err != nil { + return nil, errors.MessageBus.Cause(err, "fail to initialize the MQTT client") + } + return mmb, nil +} + +type MessageBus struct { + client mqtt.Client + tokenTimeout time.Duration + callTimeout time.Duration + qos int + + routes map[string]message.Handler // 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(msg *message.Message) error { + mb.logger.Debugf("send message: %s", msg) + token := mb.client.Publish(msg.Topic, byte(mb.qos), false, msg.Payload) + return mb.handleToken(token) +} + +func (mb *MessageBus) Subscribe(handler message.Handler, 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.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) +} + +// Call needs to bind request and response belonging to the same operation, +// otherwise it will cause confusion when multiple operations are executed concurrently. +func (mb *MessageBus) Call(request *message.Message, rspTpc, errTpc string) (response *message.Message, err error) { + // subscribe response + ch := make(chan *message.Message) + if err = mb.Subscribe(func(msg *message.Message) { + ch <- msg + }, rspTpc); err != nil { + return + } + errCh := make(chan *message.Message) + if err = mb.Subscribe(func(msg *message.Message) { + errCh <- msg + }, errTpc); err != nil { + return + } + defer func() { + _ = mb.Unsubscribe(rspTpc) + close(ch) + _ = mb.Unsubscribe(errTpc) + close(errCh) + }() + + // publish request + if err = mb.Publish(request); err != nil { + return + } + // waiting for the response + ticker := time.NewTicker(mb.callTimeout) + select { + case msg := <-ch: + return msg, nil + case msg := <-errCh: + return nil, errors.Unmarshal(msg.Payload) + case <-ticker.C: + ticker.Stop() + return nil, errors.MessageBus.Error("call timeout: %dms", mb.tokenTimeout/time.Millisecond) + } +} + +func (mb *MessageBus) handleToken(token mqtt.Token) error { + if mb.tokenTimeout > 0 { + token.WaitTimeout(mb.tokenTimeout) + } else { + token.Wait() + } + if err := token.Error(); err != nil { + return errors.MessageBus.Cause(token.Error(), "") + } + return nil +} + +func (mb *MessageBus) setClient(options *config.MQTTMessageBusOptions) error { + opts := mqtt.NewClientOptions() + clientID := "edge-device-sub-" + strconv.FormatInt(time.Now().UnixNano(), 10) + opts.SetClientID(clientID) + opts.AddBroker(options.GetBroker()) + mb.logger.Infof("the ID of client for the message bus is %s, connecting to %s", clientID, options.GetBroker()) + opts.SetUsername(options.Username) + opts.SetPassword(options.Password) + 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) + + if options.WithTLS { + tlsConfig, err := options.NewTLSConfig() + if err != nil { + return err + } + opts.SetTLSConfig(tlsConfig) + } + + 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/operations/define.go b/operations/define.go new file mode 100644 index 0000000..eed2a0c --- /dev/null +++ b/operations/define.go @@ -0,0 +1,13 @@ +package operations + +import "github.com/thingio/edge-device-std/models" + +type DriverInitialization struct { + Products []*models.Product `json:"products"` + Devices []*models.Device `json:"devices"` +} + +type ProductMutation = models.Product +type DeviceMutation = models.Device +type DeviceStatus = models.DeviceStatus +type DriverStatus = models.DriverStatus diff --git a/operations/driver_client.go b/operations/driver_client.go new file mode 100644 index 0000000..7f063dd --- /dev/null +++ b/operations/driver_client.go @@ -0,0 +1,111 @@ +package operations + +import ( + "github.com/thingio/edge-device-std/logger" + "github.com/thingio/edge-device-std/models" + bus "github.com/thingio/edge-device-std/msgbus" +) + +func NewDriverClient(mb bus.MessageBus, lg *logger.Logger) (DriverClient, error) { + mdc, err := newMetaDriverClient(mb, lg) + if err != nil { + return nil, err + } + ddc, err := newDataDriverClient(mb, lg) + if err != nil { + return nil, err + } + return &driverClient{ + mdc, + ddc, + }, nil +} + +type ( + DriverClient interface { + MetaDriverClient + DataDriverClient + } + driverClient struct { + MetaDriverClient + DataDriverClient + } +) + +type ( + MetaDriverClient interface { + PublishDriverStatus(status *models.DriverStatus) error + } + metaDriverClient struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newMetaDriverClient(mb bus.MessageBus, lg *logger.Logger) (MetaDriverClient, error) { + return &metaDriverClient{mb: mb, lg: lg}, nil +} + +func (m *metaDriverClient) PublishDriverStatus(status *models.DriverStatus) error { + o := NewMetaOperation(OperationModeUp, status.Protocol.ID, + MetaOperationTypeDriverHealthCheck, EmptyReqID()) + o.SetValue(status) + msg, err := o.ToMessage() + if err != nil { + return err + } + return m.mb.Publish(msg) +} + +type ( + DataDriverClient interface { + PublishDeviceStatus(protocolID, productID, deviceID string, status *models.DeviceStatus) error + PublishDeviceProps(protocolID, productID, deviceID string, propertyID models.ProductPropertyID, + props map[models.ProductPropertyID]*models.DeviceData) error + PublishDeviceEvent(protocolID, productID, deviceID string, eventID models.ProductEventID, + props map[models.ProductPropertyID]*models.DeviceData) error + } + dataDriverClient struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newDataDriverClient(mb bus.MessageBus, lg *logger.Logger) (DataDriverClient, error) { + return &dataDriverClient{mb: mb, lg: lg}, nil +} + +func (d *dataDriverClient) PublishDeviceStatus(protocolID, productID, deviceID string, status *models.DeviceStatus) error { + o := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, "-", + DataOperationTypeHealthCheck, EmptyReqID()) + o.SetValue(status) + msg, err := o.ToMessage() + if err != nil { + return err + } + return d.mb.Publish(msg) +} + +func (d *dataDriverClient) PublishDeviceProps(protocolID, productID, deviceID string, propertyID models.ProductPropertyID, + props map[models.ProductPropertyID]*models.DeviceData) error { + o := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, propertyID, + DataOperationTypeWatch, EmptyReqID()) + o.SetValue(props) + msg, err := o.ToMessage() + if err != nil { + return err + } + return d.mb.Publish(msg) +} + +func (d *dataDriverClient) PublishDeviceEvent(protocolID, productID, deviceID string, eventID models.ProductEventID, + props map[models.ProductPropertyID]*models.DeviceData) error { + o := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, eventID, + DataOperationTypeEvent, EmptyReqID()) + o.SetValue(props) + msg, err := o.ToMessage() + if err != nil { + return err + } + return d.mb.Publish(msg) +} diff --git a/operations/driver_service.go b/operations/driver_service.go new file mode 100644 index 0000000..42288fd --- /dev/null +++ b/operations/driver_service.go @@ -0,0 +1,231 @@ +package operations + +import ( + "github.com/thingio/edge-device-std/errors" + "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/msgbus/message" +) + +func NewDriverService(mb bus.MessageBus, lg *logger.Logger) (DriverService, error) { + mds, err := newMetaDriverService(mb, lg) + if err != nil { + return nil, err + } + dds, err := newDataDriverService(mb, lg) + if err != nil { + return nil, err + } + return &driverService{ + mds, + dds, + }, nil +} + +type ( + DriverService interface { + MetaDriverService + DataDriverService + } + driverService struct { + MetaDriverService + DataDriverService + } +) + +type ( + MetaDriverService interface { + InitializeDriverHandler(protocolID string, handler func(products []*models.Product, devices []*models.Device) error) error + + UpdateProductHandler(protocolID string, handler func(product *models.Product) error) error + DeleteProductHandler(protocolID string, handler func(productID string) error) error + + UpdateDeviceHandler(protocolID string, handler func(device *models.Device) error) error + DeleteDeviceHandler(protocolID string, handler func(deviceID string) error) error + } + metaDriverService struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newMetaDriverService(mb bus.MessageBus, lg *logger.Logger) (MetaDriverService, error) { + return &metaDriverService{mb: mb, lg: lg}, nil +} + +func (m *metaDriverService) InitializeDriverHandler(protocolID string, + handler func(products []*models.Product, devices []*models.Device) error) error { + return m.metaHandler(protocolID, MetaOperationTypeDriverInit, func(o *MetaOperation) error { + v := new(DriverInitialization) + if err := o.Unmarshal(v); err != nil { + return err + } + return handler(v.Products, v.Devices) + }) +} + +func (m *metaDriverService) UpdateProductHandler(protocolID string, handler func(product *models.Product) error) error { + return m.metaHandler(protocolID, MetaOperationTypeProductMutation, func(o *MetaOperation) error { + v := new(ProductMutation) + if err := o.Unmarshal(v); err != nil { + return err + } + return handler(v) + }) +} + +func (m *metaDriverService) DeleteProductHandler(protocolID string, handler func(productID string) error) error { + return m.metaHandler(protocolID, MetaOperationTypeProductMutation, func(o *MetaOperation) error { + return handler(o.reqID) + }) +} + +func (m *metaDriverService) UpdateDeviceHandler(protocolID string, handler func(device *models.Device) error) error { + return m.metaHandler(protocolID, MetaOperationTypeDeviceMutation, func(o *MetaOperation) error { + v := new(DeviceMutation) + if err := o.Unmarshal(v); err != nil { + return err + } + return handler(v) + }) +} + +func (m *metaDriverService) DeleteDeviceHandler(protocolID string, handler func(deviceID string) error) error { + return m.metaHandler(protocolID, MetaOperationTypeDeviceMutation, func(o *MetaOperation) error { + return handler(o.reqID) + }) +} + +func (m *metaDriverService) metaHandler(protocolID string, optType MetaOperationType, + handler func(o *MetaOperation) error) error { + schema := NewMetaOperation(OperationModeDown, protocolID, optType, TopicSingleLevelWildcard) + topic := schema.Topic().String() + if err := m.mb.Subscribe(func(msg *message.Message) { + o, err := ParseMetaOperation(msg) + if err != nil { + m.lg.WithError(err).Errorf("fail to parse the meta operation: %s", topic) + return + } + if err = handler(o); err != nil { + m.lg.Errorf(err.Error()) + return + } + }, topic); err != nil { + return err + } + return nil +} + +type ( + DataDriverService interface { + ReadHandler(protocolID string, handler func(productID, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error)) error + HardReadHandler(protocolID string, handler func(productID, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error)) error + WriteHandler(protocolID string, handler func(productID, deviceID string, + propertyID models.ProductPropertyID, props map[models.ProductPropertyID]*models.DeviceData) error) error + CallHandler(protocolID string, handler func(productID, deviceID string, methodID models.ProductMethodID, + ins map[string]*models.DeviceData) (outs map[string]*models.DeviceData, err error)) error + } + dataDriverService struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newDataDriverService(mb bus.MessageBus, lg *logger.Logger) (DataDriverService, error) { + return &dataDriverService{mb: mb, lg: lg}, nil +} + +func (d *dataDriverService) ReadHandler(protocolID string, handler func(productID string, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error)) error { + return d.dataHandler(protocolID, DataOperationTypeRead, + func(o *DataOperation) (outs map[string]*models.DeviceData, err error) { + productID, deviceID, propertyID := o.productID, o.deviceID, o.funcID + return handler(productID, deviceID, propertyID) + }, + ) +} + +func (d *dataDriverService) HardReadHandler(protocolID string, handler func(productID string, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error)) error { + return d.dataHandler(protocolID, DataOperationTypeHardRead, + func(o *DataOperation) (outs map[string]*models.DeviceData, err error) { + productID, deviceID, propertyID := o.productID, o.deviceID, o.funcID + return handler(productID, deviceID, propertyID) + }, + ) +} + +func (d *dataDriverService) WriteHandler(protocolID string, handler func(productID string, deviceID string, + propertyID models.ProductPropertyID, props map[models.ProductPropertyID]*models.DeviceData) error) error { + return d.dataHandler(protocolID, DataOperationTypeWrite, + func(o *DataOperation) (outs map[string]*models.DeviceData, err error) { + productID, deviceID, propertyID := o.productID, o.deviceID, o.funcID + props := make(map[models.ProductPropertyID]*models.DeviceData) + if err = o.Unmarshal(&props); err != nil { + d.lg.WithError(err).Errorf("fail to unmarshal the property[%s] "+ + "from the device[%s]", propertyID, deviceID) + return + } + return map[models.ProductPropertyID]*models.DeviceData{}, handler(productID, deviceID, propertyID, props) + }, + ) +} + +func (d *dataDriverService) CallHandler(protocolID string, handler func(productID string, deviceID string, methodID models.ProductMethodID, + ins map[string]*models.DeviceData) (outs map[string]*models.DeviceData, err error)) error { + return d.dataHandler(protocolID, DataOperationTypeCall, + func(o *DataOperation) (outs map[string]*models.DeviceData, err error) { + productID, deviceID, methodID := o.productID, o.deviceID, o.funcID + ins := make(map[string]*models.DeviceData) + if err = o.Unmarshal(&ins); err != nil { + d.lg.WithError(err).Errorf("fail to unmarshal the ins of the method[%s] "+ + "of the device[%s]", methodID, deviceID) + return + } + return handler(productID, deviceID, methodID, ins) + }, + ) +} + +func (d *dataDriverService) dataHandler(protocolID string, optType DataOperationType, + handler func(o *DataOperation) (outs map[string]*models.DeviceData, err error)) error { + schema := NewDataOperation(OperationModeDown, protocolID, + TopicSingleLevelWildcard, TopicSingleLevelWildcard, TopicSingleLevelWildcard, + optType, TopicSingleLevelWildcard) + topic := schema.Topic().String() + if err := d.mb.Subscribe(func(msg *message.Message) { + var err error + var request, response *DataOperation + var outs map[string]*models.DeviceData + defer func() { + response = NewDataOperation(OperationModeUp, request.protocolID, request.productID, request.deviceID, + request.funcID, optType, request.reqID) + if err != nil { + response.optMode = OperationModeUpErr + response.SetValue(errors.NewCommonEdgeErrorWrapper(err)) + } else { + response.SetValue(outs) + } + rspMsg, err := response.ToMessage() + if err != nil { + d.lg.WithError(err).Errorf("fail to parse the message of the response") + return + } + _ = d.mb.Publish(rspMsg) + }() + + if request, err = ParseDataOperation(msg); err != nil { + d.lg.WithError(err).Errorf("fail to parse the data operation") + return + } + if outs, err = handler(request); err != nil { + return + } + }, topic); err != nil { + return err + } + return nil +} diff --git a/operations/manager_client.go b/operations/manager_client.go new file mode 100644 index 0000000..0e68380 --- /dev/null +++ b/operations/manager_client.go @@ -0,0 +1,225 @@ +package operations + +import ( + "fmt" + "github.com/thingio/edge-device-std/errors" + "github.com/thingio/edge-device-std/logger" + "github.com/thingio/edge-device-std/models" + bus "github.com/thingio/edge-device-std/msgbus" +) + +func NewManagerClient(mb bus.MessageBus, lg *logger.Logger) (ManagerClient, error) { + mmc, err := newMetaManagerClient(mb, lg) + if err != nil { + return nil, err + } + dmc, err := newDataManagerClient(mb, lg) + if err != nil { + return nil, err + } + return &managerClient{ + mmc, + dmc, + }, nil +} + +type ( + ManagerClient interface { + MetaManagerClient + DataManagerClient + } + managerClient struct { + MetaManagerClient + DataManagerClient + } +) + +type ( + MetaManagerClient interface { + InitDriver(protocolID string, products []*models.Product, devices []*models.Device) error + + UpdateProduct(protocolID string, product *models.Product) error + DeleteProduct(protocolID string, productID string) error + + UpdateDevice(protocolID string, device *models.Device) error + DeleteDevice(protocolID string, deviceID string) error + } + metaManagerClient struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newMetaManagerClient(mb bus.MessageBus, lg *logger.Logger) (MetaManagerClient, error) { + return &metaManagerClient{mb: mb, lg: lg}, nil +} + +func (m *metaManagerClient) InitDriver(protocolID string, products []*models.Product, devices []*models.Device) error { + o := NewMetaOperation(OperationModeDown, protocolID, + MetaOperationTypeDriverInit, EmptyReqID()) + o.SetValue(&DriverInitialization{ + Products: products, + Devices: devices, + }) + + msg, err := o.ToMessage() + if err != nil { + return err + } + return m.mb.Publish(msg) +} + +func (m *metaManagerClient) UpdateProduct(protocolID string, product *models.Product) error { + o := NewMetaOperation(OperationModeDown, protocolID, + MetaOperationTypeProductMutation, product.ID) + o.SetValue(product) + + msg, err := o.ToMessage() + if err != nil { + return err + } + return m.mb.Publish(msg) +} + +func (m *metaManagerClient) DeleteProduct(protocolID string, productID string) error { + o := NewMetaOperation(OperationModeDown, protocolID, + MetaOperationTypeProductMutation, productID) + + msg, err := o.ToMessage() + if err != nil { + return err + } + return m.mb.Publish(msg) +} + +func (m *metaManagerClient) UpdateDevice(protocolID string, device *models.Device) error { + o := NewMetaOperation(OperationModeDown, protocolID, + MetaOperationTypeDeviceMutation, device.ID) + o.SetValue(device) + + msg, err := o.ToMessage() + if err != nil { + return err + } + return m.mb.Publish(msg) +} + +func (m metaManagerClient) DeleteDevice(protocolID string, deviceID string) error { + o := NewMetaOperation(OperationModeDown, protocolID, + MetaOperationTypeDeviceMutation, deviceID) + + msg, err := o.ToMessage() + if err != nil { + return err + } + return m.mb.Publish(msg) +} + +type ( + DataManagerClient interface { + Read(protocolID, productID, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error) + HardRead(protocolID, productID, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error) + Write(protocolID, productID, deviceID string, + propertyID models.ProductPropertyID, props map[models.ProductPropertyID]*models.DeviceData) error + Call(protocolID, productID, deviceID string, methodID models.ProductMethodID, + ins map[string]*models.DeviceData) (outs map[string]*models.DeviceData, err error) + } + dataManagerClient struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newDataManagerClient(mb bus.MessageBus, lg *logger.Logger) (DataManagerClient, error) { + return &dataManagerClient{mb: mb, lg: lg}, nil +} + +func (d *dataManagerClient) Read(protocolID, productID, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error) { + reqID := NewReqID() + request := NewDataOperation(OperationModeDown, protocolID, productID, deviceID, propertyID, + DataOperationTypeRead, reqID) + reqMsg, err := request.ToMessage() + if err != nil { + return nil, err + } + rspTpc := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, propertyID, DataOperationTypeRead, reqID).Topic().String() + errTpc := NewDataOperation(OperationModeUpErr, protocolID, productID, deviceID, propertyID, DataOperationTypeRead, reqID).Topic().String() + rspMsg, err := d.mb.Call(reqMsg, rspTpc, errTpc) + if err != nil { + return nil, errors.NewCommonEdgeErrorWrapper(err) + } + props = make(map[models.ProductPropertyID]*models.DeviceData) + if err = rspMsg.Unmarshal(props); err != nil { + return nil, errors.NewCommonEdgeError(errors.Internal, + fmt.Sprintf("fail to unmarshal the payload of the response"), err) + } + return props, nil +} + +func (d *dataManagerClient) HardRead(protocolID, productID, deviceID string, + propertyID models.ProductPropertyID) (props map[models.ProductPropertyID]*models.DeviceData, err error) { + reqID := NewReqID() + request := NewDataOperation(OperationModeDown, protocolID, productID, deviceID, propertyID, + DataOperationTypeHardRead, reqID) + reqMsg, err := request.ToMessage() + if err != nil { + return nil, err + } + rspTpc := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, propertyID, DataOperationTypeRead, reqID).Topic().String() + errTpc := NewDataOperation(OperationModeUpErr, protocolID, productID, deviceID, propertyID, DataOperationTypeRead, reqID).Topic().String() + rspMsg, err := d.mb.Call(reqMsg, rspTpc, errTpc) + if err != nil { + return nil, errors.NewCommonEdgeErrorWrapper(err) + } + props = make(map[models.ProductPropertyID]*models.DeviceData) + if err = rspMsg.Unmarshal(&props); err != nil { + return nil, errors.NewCommonEdgeError(errors.Internal, + fmt.Sprintf("fail to unmarshal the payload of the response"), err) + } + return props, nil +} + +func (d *dataManagerClient) Write(protocolID, productID, deviceID string, + propertyID models.ProductPropertyID, props map[models.ProductPropertyID]*models.DeviceData) error { + reqID := NewReqID() + request := NewDataOperation(OperationModeDown, protocolID, productID, deviceID, propertyID, + DataOperationTypeWrite, reqID) + request.SetValue(props) + reqMsg, err := request.ToMessage() + if err != nil { + return err + } + rspTpc := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, propertyID, DataOperationTypeWrite, reqID).Topic().String() + errTpc := NewDataOperation(OperationModeUpErr, protocolID, productID, deviceID, propertyID, DataOperationTypeWrite, reqID).Topic().String() + if _, err = d.mb.Call(reqMsg, rspTpc, errTpc); err != nil { + return errors.NewCommonEdgeErrorWrapper(err) + } + return nil +} + +func (d *dataManagerClient) Call(protocolID, productID, deviceID string, methodID models.ProductMethodID, + ins map[string]*models.DeviceData) (outs map[string]*models.DeviceData, err error) { + reqID := NewReqID() + request := NewDataOperation(OperationModeDown, protocolID, productID, deviceID, methodID, + DataOperationTypeCall, NewReqID()) + request.SetValue(ins) + reqMsg, err := request.ToMessage() + if err != nil { + return nil, err + } + rspTpc := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, methodID, DataOperationTypeWrite, reqID).Topic().String() + errTpc := NewDataOperation(OperationModeUpErr, protocolID, productID, deviceID, methodID, DataOperationTypeWrite, reqID).Topic().String() + rspMsg, err := d.mb.Call(reqMsg, rspTpc, errTpc) + if err != nil { + return nil, errors.NewCommonEdgeErrorWrapper(err) + } + outs = make(map[models.ProductPropertyID]*models.DeviceData) + if err = rspMsg.Unmarshal(&outs); err != nil { + return nil, errors.NewCommonEdgeError(errors.Internal, + fmt.Sprintf("fail to unmarshal the payload of the response"), err) + } + return outs, nil +} diff --git a/operations/manager_service.go b/operations/manager_service.go new file mode 100644 index 0000000..dd863b6 --- /dev/null +++ b/operations/manager_service.go @@ -0,0 +1,153 @@ +package operations + +import ( + "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/msgbus/message" +) + +func NewManagerService(mb bus.MessageBus, lg *logger.Logger) (ManagerService, error) { + mms, err := newMetaManagerService(mb, lg) + if err != nil { + return nil, err + } + dms, err := newDataManagerService(mb, lg) + if err != nil { + return nil, err + } + return &managerService{ + mms, + dms, + }, nil +} + +type ( + ManagerService interface { + MetaManagerService + DataManagerService + } + managerService struct { + MetaManagerService + DataManagerService + } +) + +type ( + MetaManagerService interface { + SubscribeDriverStatus() (bus <-chan interface{}, stop func(), err error) + } + metaManagerService struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newMetaManagerService(mb bus.MessageBus, lg *logger.Logger) (MetaManagerService, error) { + return &metaManagerService{mb: mb, lg: lg}, nil +} + +func (m *metaManagerService) SubscribeDriverStatus() (<-chan interface{}, func(), error) { + return m.subscribe(MetaOperationTypeDriverHealthCheck, func(o *MetaOperation) (interface{}, error) { + status := new(DriverStatus) + if err := o.Unmarshal(status); err != nil { + return nil, err + } + return status, nil + }) +} + +func (m *metaManagerService) subscribe(optType MetaOperationType, + parser func(o *MetaOperation) (interface{}, error)) (<-chan interface{}, func(), error) { + schema := NewMetaOperation(OperationModeUp, TopicSingleLevelWildcard, optType, TopicSingleLevelWildcard) + return subscribe(m.mb, m.lg, schema.Topic().String(), func(msg *message.Message) (interface{}, error) { + o, err := ParseMetaOperation(msg) + if err != nil { + return nil, err + } + return parser(o) + }) +} + +type ( + DataManagerService interface { + SubscribeDeviceStatus(protocolID string) (<-chan interface{}, func(), error) + SubscribeDeviceProps(protocolID, productID, deviceID string, propertyID models.ProductPropertyID) (<-chan interface{}, func(), error) + SubscribeDeviceEvent(protocolID, productID, deviceID string, eventID models.ProductEventID) (<-chan interface{}, func(), error) + } + dataManagerService struct { + mb bus.MessageBus + lg *logger.Logger + } +) + +func newDataManagerService(mb bus.MessageBus, lg *logger.Logger) (DataManagerService, error) { + return &dataManagerService{mb: mb, lg: lg}, nil +} + +func (d *dataManagerService) SubscribeDeviceStatus(protocolID string) (<-chan interface{}, func(), error) { + return d.subscribe(protocolID, TopicSingleLevelWildcard, TopicSingleLevelWildcard, TopicSingleLevelWildcard, DataOperationTypeHealthCheck, + func(o *DataOperation) (interface{}, error) { + status := new(DeviceStatus) + if err := o.Unmarshal(status); err != nil { + return nil, err + } + return status, nil + }, + ) +} + +func (d *dataManagerService) SubscribeDeviceProps(protocolID, productID, deviceID string, propertyID models.ProductPropertyID) (<-chan interface{}, func(), error) { + return d.subscribe(protocolID, productID, deviceID, propertyID, DataOperationTypeWatch, + func(o *DataOperation) (interface{}, error) { + props := make(map[models.ProductPropertyID]*models.DeviceData) + if err := o.Unmarshal(&props); err != nil { + return nil, err + } + return props, nil + }, + ) +} + +func (d *dataManagerService) SubscribeDeviceEvent(protocolID, productID, deviceID string, eventID models.ProductEventID) (<-chan interface{}, func(), error) { + return d.subscribe(protocolID, productID, deviceID, eventID, DataOperationTypeEvent, + func(o *DataOperation) (interface{}, error) { + props := make(map[models.ProductPropertyID]*models.DeviceData) + if err := o.Unmarshal(&props); err != nil { + return nil, err + } + return props, nil + }, + ) +} + +func (d *dataManagerService) subscribe(protocolID, productID, deviceID string, funcID models.ProductEventID, + optType DataOperationType, parser func(o *DataOperation) (interface{}, error)) (<-chan interface{}, func(), error) { + schema := NewDataOperation(OperationModeUp, protocolID, productID, deviceID, funcID, optType, TopicSingleLevelWildcard) + return subscribe(d.mb, d.lg, schema.Topic().String(), + func(msg *message.Message) (interface{}, error) { + o, err := ParseDataOperation(msg) + if err != nil { + return nil, err + } + return parser(o) + }, + ) +} + +func subscribe(mb bus.MessageBus, lg *logger.Logger, topic string, + parser func(msg *message.Message) (interface{}, error)) (<-chan interface{}, func(), error) { + buffer := make(chan interface{}, 1000) + if err := mb.Subscribe(func(msg *message.Message) { + v, err := parser(msg) + if err != nil { + lg.WithError(err).Errorf("fail to parse the payload of the data operation") + } + buffer <- v + }, topic); err != nil { + return nil, nil, err + } + return buffer, func() { + close(buffer) + }, nil +} diff --git a/operations/operation.go b/operations/operation.go new file mode 100644 index 0000000..e24f9e4 --- /dev/null +++ b/operations/operation.go @@ -0,0 +1,73 @@ +package operations + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/rs/xid" + "github.com/thingio/edge-device-std/msgbus/message" + "github.com/thingio/edge-device-std/version" +) + +type ( + OperationCategory string + + OperationType string + OperationMode string +) + +const ( + OperationCategoryMeta OperationCategory = "META" + OperationCategoryData OperationCategory = "DATA" + + OperationModeUp OperationMode = "UP" + OperationModeUpErr OperationMode = "UP-ERR" + OperationModeDown OperationMode = "DOWN" +) + +type Operation interface { + Topic() Topic + ToMessage() (*message.Message, error) + + SetValue(v interface{}) +} + +type operation struct { + optCategory OperationCategory + ver version.Version + optMode OperationMode + protocolID string + + optType OperationType + reqID string + + value interface{} + payload []byte +} + +func (o *operation) Topic() Topic { + return nil +} + +func (o *operation) ToMessage() (*message.Message, error) { + return nil, errors.New("implement me") +} + +func (o *operation) SetValue(v interface{}) { + o.value = v +} + +func (o *operation) Unmarshal(v interface{}) error { + if len(o.payload) == 0 { + return fmt.Errorf("the payload the operation may not be filled yet") + } + return json.Unmarshal(o.payload, v) +} + +func NewReqID() string { + return xid.New().String() +} + +func EmptyReqID() string { + return "" +} diff --git a/operations/operation_data.go b/operations/operation_data.go new file mode 100644 index 0000000..c300b51 --- /dev/null +++ b/operations/operation_data.go @@ -0,0 +1,90 @@ +package operations + +import ( + "encoding/json" + "github.com/thingio/edge-device-std/models" + "github.com/thingio/edge-device-std/msgbus/message" + "github.com/thingio/edge-device-std/version" +) + +type ( + DataOperationType = OperationType // the type of device data's operation + DevicePropertyReportMode = string // the mode of device property's reporting +) + +const ( + DataOperationTypeHealthCheck DataOperationType = "STATUS" // Device Health Check + DataOperationTypeRead DataOperationType = "READ" // Device Property Soft Read + DataOperationTypeHardRead DataOperationType = "HARD-READ" // Device Property Hard Read + DataOperationTypeWrite DataOperationType = "WRITE" // Device Property Write + DataOperationTypeWatch DataOperationType = "PROPS" // Device Property Watch + DataOperationTypeEvent DataOperationType = "EVENT" // Device Event + DataOperationTypeCall DataOperationType = "CALL" // Device Method + + DeviceDataReportModePeriodical DevicePropertyReportMode = "periodical" // report device data at intervals, e.g. 5s, 1m, 0.5h + DeviceDataReportModeOnChange DevicePropertyReportMode = "onchange" // report device data on change +) + +type DataOperation struct { + operation + + productID string + deviceID string + funcID models.ProductFuncID +} + +func (o *DataOperation) Topic() Topic { + return &commonTopic{ + category: OperationCategoryData, + version: o.ver, + tags: map[TopicTagKey]string{ + TopicTagKeyOptMode: string(o.optMode), + TopicTagKeyProtocolID: o.protocolID, + TopicTagKeyProductID: o.productID, + TopicTagKeyDeviceID: o.deviceID, + TopicTagKeyFuncID: o.funcID, + TopicTagKeyOptType: string(o.optType), + TopicTagKeyReqID: o.reqID, + }, + } +} + +func (o *DataOperation) ToMessage() (*message.Message, error) { + payload, err := json.Marshal(o.value) + if err != nil { + return nil, err + } + + return &message.Message{ + Topic: o.Topic().String(), + Payload: payload, + }, nil +} + +func NewDataOperation(optMode OperationMode, protocolID, productID, deviceID string, funcID models.ProductFuncID, + optType DataOperationType, reqID string) *DataOperation { + return &DataOperation{ + operation: operation{ + optCategory: OperationCategoryData, + ver: version.DataVersion, + optMode: optMode, + protocolID: protocolID, + optType: optType, + reqID: reqID, + }, + productID: productID, + deviceID: deviceID, + funcID: funcID, + } +} + +func ParseDataOperation(msg *message.Message) (*DataOperation, error) { + topic, err := ParseTopic(msg) + if err != nil { + return nil, err + } + tags := topic.TagValues() + o := NewDataOperation(OperationMode(tags[0]), tags[1], tags[2], tags[3], tags[4], OperationType(tags[5]), tags[6]) + o.payload = msg.Payload + return o, nil +} diff --git a/operations/operation_meta.go b/operations/operation_meta.go new file mode 100644 index 0000000..8a6e983 --- /dev/null +++ b/operations/operation_meta.go @@ -0,0 +1,71 @@ +package operations + +import ( + "encoding/json" + "github.com/thingio/edge-device-std/msgbus/message" + "github.com/thingio/edge-device-std/version" +) + +type ( + MetaOperationType = OperationType +) + +const ( + MetaOperationTypeProductMutation MetaOperationType = "PRODUCT" + MetaOperationTypeDeviceMutation MetaOperationType = "DEVICE" + MetaOperationTypeDriverInit MetaOperationType = "INIT" + MetaOperationTypeDriverHealthCheck MetaOperationType = "STATUS" +) + +type MetaOperation struct { + operation +} + +func (o *MetaOperation) Topic() Topic { + return &commonTopic{ + category: OperationCategoryMeta, + version: o.ver, + tags: map[TopicTagKey]string{ + TopicTagKeyOptMode: string(o.optMode), + TopicTagKeyProtocolID: o.protocolID, + TopicTagKeyOptType: string(o.optType), + TopicTagKeyReqID: o.reqID, + }, + } +} + +func (o *MetaOperation) ToMessage() (*message.Message, error) { + payload, err := json.Marshal(o.value) + if err != nil { + return nil, err + } + return &message.Message{ + Topic: o.Topic().String(), + Payload: payload, + }, nil +} + +func NewMetaOperation(optMode OperationMode, protocolID string, optType MetaOperationType, reqID string) *MetaOperation { + o := &MetaOperation{ + operation: operation{ + optCategory: OperationCategoryMeta, + ver: version.MetaVersion, + optMode: optMode, + protocolID: protocolID, + optType: optType, + reqID: reqID, + }, + } + return o +} + +func ParseMetaOperation(msg *message.Message) (*MetaOperation, error) { + topic, err := ParseTopic(msg) + if err != nil { + return nil, err + } + tags := topic.TagValues() + o := NewMetaOperation(OperationMode(tags[0]), tags[1], OperationType(tags[2]), tags[3]) + o.payload = msg.Payload + return o, nil +} diff --git a/operations/operation_topic.go b/operations/operation_topic.go new file mode 100644 index 0000000..3d444e1 --- /dev/null +++ b/operations/operation_topic.go @@ -0,0 +1,125 @@ +package operations + +import ( + "fmt" + "github.com/thingio/edge-device-std/msgbus/message" + "github.com/thingio/edge-device-std/version" + "strings" +) + +type ( + TopicTagKey string + TopicTags map[TopicTagKey]string +) + +const ( + TagsOffset = 2 // // + + TopicTagKeyOptType TopicTagKey = "opt_type" + TopicTagKeyOptMode TopicTagKey = "opt_mode" + TopicTagKeyProtocolID TopicTagKey = "protocol_id" + TopicTagKeyProductID TopicTagKey = "product_id" + TopicTagKeyDeviceID TopicTagKey = "device_id" + TopicTagKeyFuncID TopicTagKey = "func_id" + TopicTagKeyReqID TopicTagKey = "req_id" + + TopicLevelSeparator = "/" + TopicMultiLevelWildcard = "#" + TopicSingleLevelWildcard = "+" +) + +var ( + // Schemas describes all topics' forms. Every topic is formed by ///.../. + Schemas = map[OperationCategory][]TopicTagKey{ + OperationCategoryMeta: {TopicTagKeyOptMode, TopicTagKeyProtocolID, TopicTagKeyOptType, TopicTagKeyReqID}, + OperationCategoryData: {TopicTagKeyOptMode, TopicTagKeyProtocolID, TopicTagKeyProductID, TopicTagKeyDeviceID, + TopicTagKeyFuncID, TopicTagKeyOptType, TopicTagKeyReqID}, + } +) + +type Topic interface { + Category() OperationCategory + + String() string + + Tags() TopicTags + TagKeys() []TopicTagKey + TagValues() []string + TagValue(key TopicTagKey) (value string, ok bool) +} + +type commonTopic struct { + version version.Version + category OperationCategory + + tags TopicTags +} + +func (c *commonTopic) Version() version.Version { + return c.version +} + +func (c *commonTopic) Category() OperationCategory { + return c.category +} + +func (c *commonTopic) String() string { + topicVersion := c.version + topicType := string(c.category) + tagValues := c.TagValues() + return strings.Join(append([]string{topicType, string(topicVersion)}, tagValues...), TopicLevelSeparator) +} + +func (c *commonTopic) Tags() TopicTags { + return c.tags +} + +func (c *commonTopic) TagKeys() []TopicTagKey { + return Schemas[c.category] +} + +func (c *commonTopic) TagValues() []string { + tagKeys := c.TagKeys() + values := make([]string, len(tagKeys)) + for idx, topicTagKey := range tagKeys { + values[idx] = c.tags[topicTagKey] + } + return values +} + +func (c *commonTopic) TagValue(key TopicTagKey) (value string, ok bool) { + tags := c.Tags() + value, ok = tags[key] + return +} + +func ParseTopic(msg *message.Message) (Topic, error) { + if msg == nil || msg.Topic == "" { + return nil, fmt.Errorf("invalid message: %s", msg.String()) + } + topic := msg.Topic + + parts := strings.Split(topic, TopicLevelSeparator) + if len(parts) <= TagsOffset { + return nil, fmt.Errorf("invalid topic: %s", topic) + } + topicCategory, topicVersion := OperationCategory(parts[0]), version.Version(parts[1]) + + keys, ok := Schemas[topicCategory] + if !ok { + return nil, fmt.Errorf("undefined operation category: %s", topicCategory) + } + 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{ + version: topicVersion, + category: topicCategory, + tags: tags, + }, nil +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..a4c1542 --- /dev/null +++ b/version/version.go @@ -0,0 +1,8 @@ +package version + +type Version string + +const ( + MetaVersion Version = "v1" + DataVersion Version = "v1" +)