summaryrefslogtreecommitdiff
path: root/mappers
diff options
context:
space:
mode:
authorsailorvii <35483373+sailorvii@users.noreply.github.com>2020-11-05 21:55:08 +0800
committerGitHub <noreply@github.com>2020-11-05 21:55:08 +0800
commit90dfbd208021b1d248042335188be1d35994c7cb (patch)
treea3445d6d7cc8699da4ce9722283407bdc4e3849b /mappers
parentMerge pull request #2009 from shenkonghui/feat/keadm-debug-collect (diff)
downloadkubeedge-90dfbd208021b1d248042335188be1d35994c7cb.tar.gz
Modbus mapper refactor (#2282)
* Add modbus mapper. * Add modbus mapper. * Address comments. 1. Set client to each RTU port instead of each RTU device. 2. Add README 3. Refine the dockerfile * Refine device instance & model example files. * Fix verify and lint issues. * Address lint & verify issues. * Add license * Remove whitenoise * Fix error * Address lint errors * Refine as the Fisher's comment * Remove serial license * Add more license * Address conflict
Diffstat (limited to 'mappers')
-rw-r--r--mappers/README.md4
-rw-r--r--mappers/common/configmaptype.go121
-rw-r--r--mappers/common/data_converter.go40
-rw-r--r--mappers/common/event.go156
-rw-r--r--mappers/common/event_test.go55
-rw-r--r--mappers/common/event_type.go106
-rw-r--r--mappers/common/timer.go50
-rw-r--r--mappers/modbus-go/Dockerfile10
-rw-r--r--mappers/modbus-go/config.go85
-rw-r--r--mappers/modbus-go/config.yaml6
-rw-r--r--mappers/modbus-go/config_test.go18
-rw-r--r--mappers/modbus-go/configmap/configmap_negtest.json90
-rw-r--r--mappers/modbus-go/configmap/configmap_test.json90
-rw-r--r--mappers/modbus-go/configmap/parse.go131
-rw-r--r--mappers/modbus-go/configmap/parse_test.go39
-rw-r--r--mappers/modbus-go/configmap/type.go52
-rw-r--r--mappers/modbus-go/deployment.yaml41
-rw-r--r--mappers/modbus-go/device/device.go276
-rw-r--r--mappers/modbus-go/device/devstatus.go47
-rw-r--r--mappers/modbus-go/device/twindata.go67
-rw-r--r--mappers/modbus-go/driver/client.go194
-rw-r--r--mappers/modbus-go/driver/client_test.go81
-rw-r--r--mappers/modbus-go/driver/const.go26
-rw-r--r--mappers/modbus-go/globals/globals.go30
-rw-r--r--mappers/modbus-go/main.go56
25 files changed, 1871 insertions, 0 deletions
diff --git a/mappers/README.md b/mappers/README.md
new file mode 100644
index 000000000..13d95bb9e
--- /dev/null
+++ b/mappers/README.md
@@ -0,0 +1,4 @@
+# Mappers
+There're two folders for modbus mapper. "modbus-go" is written by go language, "modbus_mapper" is by JavaScript.
+
+Note, the modbus-go could work for KubeEdge 1.4 version and later. modbus_mapper is only for KubeEdge 1.3 and before.
diff --git a/mappers/common/configmaptype.go b/mappers/common/configmaptype.go
new file mode 100644
index 000000000..67a9790f5
--- /dev/null
+++ b/mappers/common/configmaptype.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mappercommon
+
+import "encoding/json"
+
+// DeviceProfile is structure to store in configMap.
+type DeviceProfile struct {
+ DeviceInstances []DeviceInstance `json:"deviceInstances,omitempty"`
+ DeviceModels []DeviceModel `json:"deviceModels,omitempty"`
+ Protocols []Protocol `json:"protocols,omitempty"`
+}
+
+// DeviceInstance is structure to store device in deviceProfile.json in configmap.
+type DeviceInstance struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ ProtocolName string `json:"protocol,omitempty"`
+ PProtocol Protocol
+ Model string `json:"model,omitempty"`
+ Twins []Twin `json:"twins,omitempty"`
+ Datas Data `json:"data,omitempty"`
+ PropertyVisitors []PropertyVisitor `json:"propertyVisitors,omitempty"`
+}
+
+// DeviceModel is structure to store deviceModel in deviceProfile.json in configmap.
+type DeviceModel struct {
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ Properties []Property `json:"properties,omitempty"`
+}
+
+// Property is structure to store deviceModel property.
+type Property struct {
+ Name string `json:"name,omitempty"`
+ DataType string `json:"dataType,omitempty"`
+ Description string `json:"description,omitempty"`
+ AccessMode string `json:"accessMode,omitempty"`
+ DefaultValue interface{} `json:"defaultValue,omitempty"`
+ Minimum int64 `json:"minimum,omitempty"`
+ Maximum int64 `json:"maximum,omitempty"`
+ Unit string `json:"unit,omitempty"`
+}
+
+// Protocol is structure to store protocol in deviceProfile.json in configmap.
+type Protocol struct {
+ Name string `json:"name,omitempty"`
+ Protocol string `json:"protocol,omitempty"`
+ ProtocolConfigs ProtocolConfig `json:"protocolConfig,omitempty"`
+ ProtocolCommonConfig json.RawMessage `json:"protocolCommonConfig,omitempty"`
+}
+
+// ProtocolConfig is the protocol configuration.
+type ProtocolConfig struct {
+ SlaveID int16 `json:"slaveID,omitempty"`
+}
+
+// PropertyVisitor is structure to store propertyVisitor in deviceProfile.json in configmap.
+type PropertyVisitor struct {
+ Name string `json:"name,omitempty"`
+ PropertyName string `json:"propertyName,omitempty"`
+ ModelName string `json:"modelName,omitempty"`
+ CollectCycle int64 `json:"collectCycle"`
+ ReportCycle int64 `json:"reportcycle,omitempty"`
+ PProperty Property
+ Protocol string `json:"protocol,omitempty"`
+ VisitorConfig json.RawMessage `json:"visitorConfig"`
+}
+
+// Data is data structure for the message that only be subscribed in edge node internal.
+type Data struct {
+ Properties []DataProperty `json:"dataProperties,omitempty"`
+ Topic string `json:"datatopic,omitempty"`
+}
+
+// DataProperty is data property.
+type DataProperty struct {
+ Metadatas DataMetadata `json:"metadata,omitempty"`
+ PropertyName string `json:"propertyName,omitempty"`
+ PVisitor *PropertyVisitor
+}
+
+// Metadata is the metadata for data.
+type Metadata struct {
+ Timestamp string `json:"timestamp,omitempty"`
+ Type string `json:"type,omitempty"`
+}
+
+// Twin is the set/get pair to one register.
+type Twin struct {
+ PropertyName string `json:"propertyName,omitempty"`
+ PVisitor *PropertyVisitor
+ Desired DesiredData `json:"desired,omitempty"`
+ Reported ReportedData `json:"reported,omitempty"`
+}
+
+// DesiredData is the desired data.
+type DesiredData struct {
+ Value string `json:"value,omitempty"`
+ Metadatas Metadata `json:"metadata,omitempty"`
+}
+
+// ReportedData is the reported data.
+type ReportedData struct {
+ Value string `json:"value,omitempty"`
+ Metadatas Metadata `json:"metadata,omitempty"`
+}
diff --git a/mappers/common/data_converter.go b/mappers/common/data_converter.go
new file mode 100644
index 000000000..d5cb0c22f
--- /dev/null
+++ b/mappers/common/data_converter.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mappercommon
+
+import (
+ "errors"
+ "strconv"
+)
+
+// Convert string to other types
+func Convert(valueType string, value string) (result interface{}, err error) {
+ switch valueType {
+ case "int":
+ return strconv.ParseInt(value, 10, 64)
+ case "float":
+ return strconv.ParseFloat(value, 32)
+ case "double":
+ return strconv.ParseFloat(value, 64)
+ case "boolean":
+ return strconv.ParseBool(value)
+ case "string":
+ return value, nil
+ default:
+ return nil, errors.New("Convert failed")
+ }
+}
diff --git a/mappers/common/event.go b/mappers/common/event.go
new file mode 100644
index 000000000..4ee1d221c
--- /dev/null
+++ b/mappers/common/event.go
@@ -0,0 +1,156 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mappercommon
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "time"
+
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+)
+
+// Joint the topic like topic := fmt.Sprintf(TopicTwinUpdateDelta, deviceID)
+const (
+ TopicTwinUpdateDelta = "$hw/events/device/%s/twin/update/delta"
+ TopicTwinUpdate = "$hw/events/device/%s/twin/update"
+ TopicStateUpdate = "$hw/events/device/%s/state/update"
+ TopicDataUpdate = "$ke/events/device/%s/data/update"
+)
+
+// MqttClient is parameters for Mqtt client.
+type MqttClient struct {
+ Qos byte
+ Retained bool
+ IP string
+ User string
+ Passwd string
+ Cert string
+ PrivateKey string
+ Client mqtt.Client
+}
+
+// newTLSConfig new TLS configuration.
+// Only one side check. Mqtt broker check the cert from client.
+func newTLSConfig(certfile string, privateKey string) (*tls.Config, error) {
+ // Import client certificate/key pair
+ cert, err := tls.LoadX509KeyPair(certfile, privateKey)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create tls.Config with desired tls properties
+ return &tls.Config{
+ // ClientAuth = whether to request cert from server.
+ // Since the server is set up for SSL, this happens
+ // anyways.
+ ClientAuth: tls.NoClientCert,
+ // ClientCAs = certs used to validate client cert.
+ ClientCAs: nil,
+ // InsecureSkipVerify = verify that cert contents
+ // match server. IP matches what is in cert etc.
+ InsecureSkipVerify: true,
+ // Certificates = list of certs client sends to server.
+ Certificates: []tls.Certificate{cert},
+ }, nil
+}
+
+// Connect connect to the Mqtt server.
+func (mc *MqttClient) Connect() error {
+ opts := mqtt.NewClientOptions().AddBroker(mc.IP).SetClientID("").SetCleanSession(true)
+ if mc.Cert != "" {
+ tlsConfig, err := newTLSConfig(mc.Cert, mc.PrivateKey)
+ if err != nil {
+ return err
+ }
+ opts.SetTLSConfig(tlsConfig)
+ } else {
+ opts.SetUsername(mc.User)
+ opts.SetPassword(mc.Passwd)
+ }
+
+ mc.Client = mqtt.NewClient(opts)
+ // The token is used to indicate when actions have completed.
+ if tc := mc.Client.Connect(); tc.Wait() && tc.Error() != nil {
+ return tc.Error()
+ }
+
+ mc.Qos = 0 // At most 1 time
+ mc.Retained = false // Not retained
+ return nil
+}
+
+// Publish publish Mqtt message.
+func (mc *MqttClient) Publish(topic string, payload interface{}) error {
+ if tc := mc.Client.Publish(topic, mc.Qos, mc.Retained, payload); tc.Wait() && tc.Error() != nil {
+ return tc.Error()
+ }
+ return nil
+}
+
+// Subscribe subsribe a Mqtt topic.
+func (mc *MqttClient) Subscribe(topic string, onMessage mqtt.MessageHandler) error {
+ if tc := mc.Client.Subscribe(topic, mc.Qos, onMessage); tc.Wait() && tc.Error() != nil {
+ return tc.Error()
+ }
+ return nil
+}
+
+// getTimestamp get current timestamp.
+func getTimestamp() int64 {
+ return time.Now().UnixNano() / 1e6
+}
+
+// CreateMessageTwinUpdate create twin update message.
+func CreateMessageTwinUpdate(name string, valueType string, value string) (msg []byte, err error) {
+ var updateMsg DeviceTwinUpdate
+
+ updateMsg.BaseMessage.Timestamp = getTimestamp()
+ updateMsg.Twin = map[string]*MsgTwin{}
+ updateMsg.Twin[name] = &MsgTwin{}
+ updateMsg.Twin[name].Actual = &TwinValue{Value: &value}
+ updateMsg.Twin[name].Metadata = &TypeMetadata{Type: valueType}
+
+ msg, err = json.Marshal(updateMsg)
+ return
+}
+
+// CreateMessageData create data message.
+func CreateMessageData(name string, valueType string, value string) (msg []byte, err error) {
+ var dataMsg DeviceData
+
+ dataMsg.BaseMessage.Timestamp = getTimestamp()
+ dataMsg.Data = map[string]*DataValue{}
+ dataMsg.Data[name] = &DataValue{}
+ dataMsg.Data[name].Value = value
+ dataMsg.Data[name].Metadata.Type = valueType
+ dataMsg.Data[name].Metadata.Timestamp = getTimestamp()
+
+ msg, err = json.Marshal(dataMsg)
+ return
+}
+
+// CreateMessageState create device status message.
+func CreateMessageState(state string) (msg []byte, err error) {
+ var stateMsg DeviceUpdate
+
+ stateMsg.BaseMessage.Timestamp = getTimestamp()
+ stateMsg.State = state
+
+ msg, err = json.Marshal(stateMsg)
+ return
+}
diff --git a/mappers/common/event_test.go b/mappers/common/event_test.go
new file mode 100644
index 000000000..794f7a544
--- /dev/null
+++ b/mappers/common/event_test.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mappercommon
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+)
+
+func onMessage(client mqtt.Client, message mqtt.Message) {
+ fmt.Println("Get topic", message.Topic())
+}
+
+func main() {
+ var c MqttClient = MqttClient{IP: "tcp://127.0.0.1:1883"}
+ err := c.Connect()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ } else {
+ fmt.Println("Connect mqtt server success", c.IP)
+ }
+ err = c.Subscribe("$hw/events/device/#", onMessage)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ } else {
+ fmt.Println("Subscribe topic success")
+ }
+ err = c.Publish("$hw/events/device/001/data/update", "test")
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ for {
+ time.Sleep(time.Second)
+ }
+}
diff --git a/mappers/common/event_type.go b/mappers/common/event_type.go
new file mode 100644
index 000000000..75142788f
--- /dev/null
+++ b/mappers/common/event_type.go
@@ -0,0 +1,106 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mappercommon
+
+// BaseMessage the base structure of event message.
+type BaseMessage struct {
+ EventID string `json:"event_id"`
+ Timestamp int64 `json:"timestamp"`
+}
+
+// TwinValue the structure of twin value.
+type TwinValue struct {
+ Value *string `json:"value,omitempty"`
+ Metadata ValueMetadata `json:"metadata,omitempty"`
+}
+
+// ValueMetadata the meta of value.
+type ValueMetadata struct {
+ Timestamp int64 `json:"timestamp,omitempty"`
+}
+
+// TypeMetadata the meta of value type.
+type TypeMetadata struct {
+ Type string `json:"type,omitempty"`
+}
+
+// TwinVersion twin version.
+type TwinVersion struct {
+ CloudVersion int64 `json:"cloud"`
+ EdgeVersion int64 `json:"edge"`
+}
+
+// MsgTwin the structure of device twin.
+type MsgTwin struct {
+ Expected *TwinValue `json:"expected,omitempty"`
+ Actual *TwinValue `json:"actual,omitempty"`
+ Optional *bool `json:"optional,omitempty"`
+ Metadata *TypeMetadata `json:"metadata,omitempty"`
+ ExpectedVersion *TwinVersion `json:"expected_version,omitempty"`
+ ActualVersion *TwinVersion `json:"actual_version,omitempty"`
+}
+
+// DeviceTwinUpdate the structure of device twin update.
+type DeviceTwinUpdate struct {
+ BaseMessage
+ Twin map[string]*MsgTwin `json:"twin"`
+}
+
+// DeviceTwinResult device get result.
+type DeviceTwinResult struct {
+ BaseMessage
+ Twin map[string]*MsgTwin `json:"twin"`
+}
+
+// DeviceTwinDelta twin delta.
+type DeviceTwinDelta struct {
+ BaseMessage
+ Twin map[string]*MsgTwin `json:"twin"`
+ Delta map[string]string `json:"delta"`
+}
+
+// DataMetadata data metadata.
+type DataMetadata struct {
+ Timestamp int64 `json:"timestamp"`
+ Type string `json:"type"`
+}
+
+// DataValue data value.
+type DataValue struct {
+ Value string `json:"value"`
+ Metadata DataMetadata `json:"metadata"`
+}
+
+// DeviceData device data structure.
+type DeviceData struct {
+ BaseMessage
+ Data map[string]*DataValue `json:"data"`
+}
+
+//MsgAttr the struct of device attr
+type MsgAttr struct {
+ Value string `json:"value"`
+ Optional *bool `json:"optional,omitempty"`
+ Metadata *TypeMetadata `json:"metadata,omitempty"`
+}
+
+//DeviceUpdate device update.
+type DeviceUpdate struct {
+ BaseMessage
+ State string `json:"state,omitempty"`
+ Attributes map[string]*MsgAttr `json:"attributes"`
+}
diff --git a/mappers/common/timer.go b/mappers/common/timer.go
new file mode 100644
index 000000000..780202a16
--- /dev/null
+++ b/mappers/common/timer.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mappercommon
+
+import (
+ "time"
+)
+
+// Timer is to call a function periodically.
+type Timer struct {
+ Function func()
+ Duration time.Duration
+ Times int
+}
+
+// Start start a timer.
+func (t *Timer) Start() {
+ ticker := time.NewTicker(t.Duration)
+ if t.Times > 0 {
+ for i := 0; i < t.Times; i++ {
+ select {
+ case <-ticker.C:
+ t.Function()
+ default:
+ }
+ }
+ } else {
+ for {
+ select {
+ case <-ticker.C:
+ t.Function()
+ default:
+ }
+ }
+ }
+}
diff --git a/mappers/modbus-go/Dockerfile b/mappers/modbus-go/Dockerfile
new file mode 100644
index 000000000..fe51aeefe
--- /dev/null
+++ b/mappers/modbus-go/Dockerfile
@@ -0,0 +1,10 @@
+FROM ubuntu:16.04
+
+RUN mkdir -p kubeedge
+
+COPY ./modbus kubeedge/
+COPY ./config.yaml kubeedge/
+
+WORKDIR kubeedge
+
+ENTRYPOINT ["/kubeedge/modbus", "--v", "5"]
diff --git a/mappers/modbus-go/config.go b/mappers/modbus-go/config.go
new file mode 100644
index 000000000..89474504d
--- /dev/null
+++ b/mappers/modbus-go/config.go
@@ -0,0 +1,85 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+ "errors"
+ "io/ioutil"
+
+ "github.com/spf13/pflag"
+ "gopkg.in/yaml.v2"
+ "k8s.io/klog"
+)
+
+// Config is the modbus mapper configuration.
+type Config struct {
+ Mqtt Mqtt `yaml:"mqtt,omitempty"`
+ Configmap string `yaml:"configmap"`
+}
+
+// Mqtt is the Mqtt configuration.
+type Mqtt struct {
+ ServerAddress string `yaml:"server,omitempty"`
+ Username string `yaml:"username,omitempty"`
+ Password string `yaml:"password,omitempty"`
+ Cert string `yaml:"certification,omitempty"`
+ PrivateKey string `yaml:"privatekey,omitempty"`
+}
+
+// ErrConfigCert error of certification configuration.
+var ErrConfigCert = errors.New("Both certification and private key must be provided")
+
+var defaultConfigFile = "./config.yaml"
+
+// Parse parse the configuration file. If failed, return error.
+func (c *Config) Parse() error {
+ var level klog.Level
+ var loglevel string
+ var configFile string
+
+ pflag.StringVar(&loglevel, "v", "1", "log level")
+ pflag.StringVar(&configFile, "config-file", defaultConfigFile, "Config file name")
+ pflag.Parse()
+ cf, err := ioutil.ReadFile(configFile)
+ if err != nil {
+ return err
+ }
+ if err = yaml.Unmarshal(cf, c); err != nil {
+ return err
+ }
+ if err = level.Set(loglevel); err != nil {
+ return err
+ }
+
+ return c.parseFlags()
+}
+
+// parseFlags parse flags. Certification and Private key must be provided at the same time.
+func (c *Config) parseFlags() error {
+ pflag.StringVar(&c.Mqtt.ServerAddress, "mqtt-address", c.Mqtt.ServerAddress, "MQTT broker address")
+ pflag.StringVar(&c.Mqtt.Username, "mqtt-username", c.Mqtt.Username, "username")
+ pflag.StringVar(&c.Mqtt.Password, "mqtt-password", c.Mqtt.Password, "password")
+ pflag.StringVar(&c.Mqtt.Cert, "mqtt-certification", c.Mqtt.Cert, "certification file path")
+ pflag.StringVar(&c.Mqtt.PrivateKey, "mqtt-priviatekey", c.Mqtt.PrivateKey, "private key file path")
+ pflag.Parse()
+
+ if (c.Mqtt.Cert != "" && c.Mqtt.PrivateKey == "") ||
+ (c.Mqtt.Cert == "" && c.Mqtt.PrivateKey != "") {
+ return ErrConfigCert
+ }
+ return nil
+}
diff --git a/mappers/modbus-go/config.yaml b/mappers/modbus-go/config.yaml
new file mode 100644
index 000000000..0ddb6b33c
--- /dev/null
+++ b/mappers/modbus-go/config.yaml
@@ -0,0 +1,6 @@
+mqtt:
+ server: tcp://127.0.0.1:1883
+ username: ""
+ password: ""
+ certification: ""
+configmap: /opt/kubeedge/deviceProfile.json
diff --git a/mappers/modbus-go/config_test.go b/mappers/modbus-go/config_test.go
new file mode 100644
index 000000000..dedf1b160
--- /dev/null
+++ b/mappers/modbus-go/config_test.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+ config := Config{}
+ if err := config.Parse(); err != nil {
+ t.Log(err)
+ t.FailNow()
+ }
+
+ assert.Equal(t, "tcp://127.0.0.1:1883", config.Mqtt.ServerAddress)
+ assert.Equal(t, "/opt/kubeedge/deviceProfile.json", config.Configmap)
+}
diff --git a/mappers/modbus-go/configmap/configmap_negtest.json b/mappers/modbus-go/configmap/configmap_negtest.json
new file mode 100644
index 000000000..cf94b6c95
--- /dev/null
+++ b/mappers/modbus-go/configmap/configmap_negtest.json
@@ -0,0 +1,90 @@
+
+ "deviceInstances": [{
+ "id": "sensor-tag-instance-01",
+ "name": "sensor-tag-instance-01",
+ "protocol": "modbus-sensor-tag-instance-01",
+ "model": "sensor-tag-model",
+ "twins": [{
+ "propertyName": "temperature-enable",
+ "desired": {
+ "value": "OFF",
+ "metadata": {
+ "timestamp": "1550049403598",
+ "type": "string"
+ }
+ },
+ "reported": {
+ "value": "OFF",
+ "metadata": {
+ "timestamp": "1550049403598",
+ "type": "string"
+ }
+ }
+ }],
+ "propertyVisitors": [{
+ "name": "temperature",
+ "propertyName": "temperature",
+ "modelName": "sensor-tag-model",
+ "protocol": "modbus",
+ "visitorConfig": {
+ "register": "CoilRegister",
+ "offset": 2,
+ "limit": 1,
+ "scale": 1,
+ "isSwap": true,
+ "isRegisterSwap": true
+ }
+ }, {
+ "name": "temperature-enable",
+ "propertyName": "temperature-enable",
+ "modelName": "sensor-tag-model",
+ "protocol": "modbus",
+ "visitorConfig": {
+ "register": "DiscreteInputRegister",
+ "offset": 3,
+ "limit": 1,
+ "scale": 1,
+ "isSwap": true,
+ "isRegisterSwap": true
+ }
+ }]
+ }],
+ "deviceModels": [{
+ "name": "sensor-tag-model",
+ "properties": [{
+ "name": "temperature",
+ "dataType": "int",
+ "description": "temperature in degree celsius",
+ "accessMode": "ReadWrite",
+ "defaultValue": 0,
+ "minimum": 0,
+ "maximum": 100,
+ "unit": "degree celsius"
+ }, {
+ "name": "temperature-enable",
+ "dataType": "string",
+ "description": "enable data collection of temperature sensor",
+ "accessMode": "ReadWrite",
+ "defaultValue": "OFF"
+ }]
+ }],
+ "protocols": [{
+ "name": "modbus-sensor-tag-instance-01",
+ "protocol": "modbus",
+ "protocolConfig": {
+ "slaveID": 1
+ },
+ "protocolCommonConfig": {
+ "com": {
+ "serialPort": "1",
+ "baudRate": 115200,
+ "dataBits": 8,
+ "parity": "even",
+ "stopBits": 1
+ },
+ "customizedValues": {
+ "serialType": "RS485"
+ }
+ }
+ }]
+}
diff --git a/mappers/modbus-go/configmap/configmap_test.json b/mappers/modbus-go/configmap/configmap_test.json
new file mode 100644
index 000000000..a163a77a7
--- /dev/null
+++ b/mappers/modbus-go/configmap/configmap_test.json
@@ -0,0 +1,90 @@
+{
+ "deviceInstances": [{
+ "id": "sensor-tag-instance-01",
+ "name": "sensor-tag-instance-01",
+ "protocol": "modbus-sensor-tag-instance-01",
+ "model": "sensor-tag-model",
+ "twins": [{
+ "propertyName": "temperature-enable",
+ "desired": {
+ "value": "OFF",
+ "metadata": {
+ "timestamp": "1550049403598",
+ "type": "string"
+ }
+ },
+ "reported": {
+ "value": "OFF",
+ "metadata": {
+ "timestamp": "1550049403598",
+ "type": "string"
+ }
+ }
+ }],
+ "propertyVisitors": [{
+ "name": "temperature",
+ "propertyName": "temperature",
+ "modelName": "sensor-tag-model",
+ "protocol": "modbus",
+ "visitorConfig": {
+ "register": "CoilRegister",
+ "offset": 2,
+ "limit": 1,
+ "scale": 1,
+ "isSwap": true,
+ "isRegisterSwap": true
+ }
+ }, {
+ "name": "temperature-enable",
+ "propertyName": "temperature-enable",
+ "modelName": "sensor-tag-model",
+ "protocol": "modbus",
+ "visitorConfig": {
+ "register": "DiscreteInputRegister",
+ "offset": 3,
+ "limit": 1,
+ "scale": 1,
+ "isSwap": true,
+ "isRegisterSwap": true
+ }
+ }]
+ }],
+ "deviceModels": [{
+ "name": "sensor-tag-model",
+ "properties": [{
+ "name": "temperature",
+ "dataType": "int",
+ "description": "temperature in degree celsius",
+ "accessMode": "ReadWrite",
+ "defaultValue": 0,
+ "minimum": 0,
+ "maximum": 100,
+ "unit": "degree celsius"
+ }, {
+ "name": "temperature-enable",
+ "dataType": "string",
+ "description": "enable data collection of temperature sensor",
+ "accessMode": "ReadWrite",
+ "defaultValue": "OFF"
+ }]
+ }],
+ "protocols": [{
+ "name": "modbus-sensor-tag-instance-01",
+ "protocol": "modbus",
+ "protocolConfig": {
+ "slaveID": 1
+ },
+ "protocolCommonConfig": {
+ "com": {
+ "serialPort": "1",
+ "baudRate": 115200,
+ "dataBits": 8,
+ "parity": "even",
+ "stopBits": 1
+ },
+ "customizedValues": {
+ "serialType": "RS485"
+ }
+ }
+ }]
+}
diff --git a/mappers/modbus-go/configmap/parse.go b/mappers/modbus-go/configmap/parse.go
new file mode 100644
index 000000000..41ae84ac0
--- /dev/null
+++ b/mappers/modbus-go/configmap/parse.go
@@ -0,0 +1,131 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package configmap
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/globals"
+ "k8s.io/klog"
+)
+
+// Parse parse the configmap.
+func Parse(path string,
+ devices map[string]*globals.ModbusDev,
+ dms map[string]mappercommon.DeviceModel,
+ protocols map[string]mappercommon.Protocol) error {
+ var deviceProfile mappercommon.DeviceProfile
+
+ jsonFile, err := ioutil.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ if err = json.Unmarshal(jsonFile, &deviceProfile); err != nil {
+ return err
+ }
+
+ for i := 0; i < len(deviceProfile.DeviceInstances); i++ {
+ instance := deviceProfile.DeviceInstances[i]
+ j := 0
+ for j = 0; j < len(deviceProfile.Protocols); j++ {
+ if instance.ProtocolName == deviceProfile.Protocols[j].Name {
+ instance.PProtocol = deviceProfile.Protocols[j]
+ break
+ }
+ }
+ if j == len(deviceProfile.Protocols) {
+ err = errors.New("Protocol not found")
+ return err
+ }
+
+ if instance.PProtocol.Protocol != "modbus" {
+ continue
+ }
+
+ for k := 0; k < len(instance.PropertyVisitors); k++ {
+ modelName := instance.PropertyVisitors[k].ModelName
+ propertyName := instance.PropertyVisitors[k].PropertyName
+ l := 0
+ for l = 0; l < len(deviceProfile.DeviceModels); l++ {
+ if modelName == deviceProfile.DeviceModels[l].Name {
+ m := 0
+ for m = 0; m < len(deviceProfile.DeviceModels[l].Properties); m++ {
+ if propertyName == deviceProfile.DeviceModels[l].Properties[m].Name {
+ instance.PropertyVisitors[k].PProperty = deviceProfile.DeviceModels[l].Properties[m]
+ break
+ }
+ }
+
+ if m == len(deviceProfile.DeviceModels[l].Properties) {
+ err = errors.New("Property not found")
+ return err
+ }
+ break
+ }
+ }
+ if l == len(deviceProfile.DeviceModels) {
+ err = errors.New("Device model not found")
+ return err
+ }
+ }
+
+ for k := 0; k < len(instance.Twins); k++ {
+ name := instance.Twins[k].PropertyName
+ l := 0
+ for l = 0; l < len(instance.PropertyVisitors); l++ {
+ if name == instance.PropertyVisitors[l].PropertyName {
+ instance.Twins[k].PVisitor = &instance.PropertyVisitors[l]
+ break
+ }
+ }
+ if l == len(instance.PropertyVisitors) {
+ return errors.New("PropertyVisitor not found")
+ }
+ }
+
+ for k := 0; k < len(instance.Datas.Properties); k++ {
+ name := instance.Datas.Properties[k].PropertyName
+ l := 0
+ for l = 0; l < len(instance.PropertyVisitors); l++ {
+ if name == instance.PropertyVisitors[l].PropertyName {
+ instance.Datas.Properties[k].PVisitor = &instance.PropertyVisitors[l]
+ break
+ }
+ }
+ if l == len(instance.PropertyVisitors) {
+ return errors.New("PropertyVisitor not found")
+ }
+ }
+
+ devices[instance.ID] = new(globals.ModbusDev)
+ devices[instance.ID].Instance = instance
+ klog.V(4).Info("Instance: ", instance.ID, instance)
+ }
+
+ for i := 0; i < len(deviceProfile.DeviceModels); i++ {
+ dms[deviceProfile.DeviceModels[i].Name] = deviceProfile.DeviceModels[i]
+ }
+
+ for i := 0; i < len(deviceProfile.Protocols); i++ {
+ protocols[deviceProfile.Protocols[i].Name] = deviceProfile.Protocols[i]
+ }
+ return nil
+}
diff --git a/mappers/modbus-go/configmap/parse_test.go b/mappers/modbus-go/configmap/parse_test.go
new file mode 100644
index 000000000..a49521886
--- /dev/null
+++ b/mappers/modbus-go/configmap/parse_test.go
@@ -0,0 +1,39 @@
+package configmap
+
+import (
+ "encoding/json"
+ "testing"
+
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+ . "github.com/kubeedge/kubeedge/mappers/modbus-go/globals"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+ var devices map[string]*ModbusDev
+ var models map[string]mappercommon.DeviceModel
+ var protocols map[string]mappercommon.Protocol
+
+ devices = make(map[string]*ModbusDev)
+ models = make(map[string]mappercommon.DeviceModel)
+ protocols = make(map[string]mappercommon.Protocol)
+
+ assert.Nil(t, Parse("./configmap_test.json", devices, models, protocols))
+ for _, device := range devices {
+ var pcc ModbusProtocolCommonConfig
+ assert.Nil(t, json.Unmarshal([]byte(device.Instance.PProtocol.ProtocolCommonConfig), &pcc))
+ assert.Equal(t, "RS485", pcc.CustomizedValues["serialType"])
+ }
+}
+
+func TestParseNeg(t *testing.T) {
+ var devices map[string]*ModbusDev
+ var models map[string]mappercommon.DeviceModel
+ var protocols map[string]mappercommon.Protocol
+
+ devices = make(map[string]*ModbusDev)
+ models = make(map[string]mappercommon.DeviceModel)
+ protocols = make(map[string]mappercommon.Protocol)
+
+ assert.NotNil(t, Parse("./configmap_negtest.json", devices, models, protocols))
+}
diff --git a/mappers/modbus-go/configmap/type.go b/mappers/modbus-go/configmap/type.go
new file mode 100644
index 000000000..b166932cb
--- /dev/null
+++ b/mappers/modbus-go/configmap/type.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package configmap
+
+// ModbusVisitorConfig is the modbus register configuration.
+type ModbusVisitorConfig struct {
+ Register string `json:"register"`
+ Offset uint16 `json:"offset"`
+ Limit int `json:"limit"`
+ Scale int `json:"scale,omitempty"`
+ IsSwap bool `json:"isSwap,omitempty"`
+ IsRegisterSwap bool `json:"isRegisterSwap,omitempty"`
+}
+
+// ModbusProtocolCommonConfig is the modbus protocol configuration.
+type ModbusProtocolCommonConfig struct {
+ COM COMStruct `json:"com,omitempty"`
+ TCP TCPStruct `json:"tcp,omitempty"`
+ CustomizedValues CustomizedValue `json:"customizedValues,omitempty"`
+}
+
+// CustomizedValue is the customized part for modbus protocol.
+type CustomizedValue map[string]interface{}
+
+// COMStruct is the serial configuration.
+type COMStruct struct {
+ SerialPort string `json:"serialPort"`
+ BaudRate int64 `json:"baudRate"`
+ DataBits int64 `json:"dataBits"`
+ Parity string `json:"parity"`
+ StopBits int64 `json:"stopBits"`
+}
+
+// TCPStruct is the TCP configuation.
+type TCPStruct struct {
+ IP string `json:"ip"`
+ Port int64 `json:"port"`
+}
diff --git a/mappers/modbus-go/deployment.yaml b/mappers/modbus-go/deployment.yaml
new file mode 100644
index 000000000..8a3e7c621
--- /dev/null
+++ b/mappers/modbus-go/deployment.yaml
@@ -0,0 +1,41 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: modbus-mapper
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: modbusmapper
+ template:
+ metadata:
+ labels:
+ app: modbusmapper
+ spec:
+ hostNetwork: true
+ containers:
+ - name: modbus-mapper-container
+ image: modbusmapper:v1.0
+ imagePullPolicy: IfNotPresent
+ securityContext:
+ privileged: true
+ volumeMounts:
+ - name: config-volume
+ mountPath: /opt/kubeedge/
+ - mountPath: /dev/ttyS0
+ name: modbus-dev0
+ - mountPath: /dev/ttyS1
+ name: modbus-dev1
+ nodeSelector:
+ modbus: "true"
+ volumes:
+ - name: config-volume
+ configMap:
+ name: device-profile-config-test
+ - name: modbus-dev0
+ hostPath:
+ path: /dev/ttyS0
+ - name: modbus-dev1
+ hostPath:
+ path: /dev/ttyS1
+ restartPolicy: Always
diff --git a/mappers/modbus-go/device/device.go b/mappers/modbus-go/device/device.go
new file mode 100644
index 000000000..d03097a78
--- /dev/null
+++ b/mappers/modbus-go/device/device.go
@@ -0,0 +1,276 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package device
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "sync"
+ "time"
+
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/configmap"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/driver"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/globals"
+ "k8s.io/klog"
+)
+
+var devices map[string]*globals.ModbusDev
+var models map[string]mappercommon.DeviceModel
+var protocols map[string]mappercommon.Protocol
+var wg sync.WaitGroup
+
+// setVisitor check if visitory is readonly, if not then set it.
+func setVisitor(visitorConfig *configmap.ModbusVisitorConfig, twin *mappercommon.Twin, client *driver.ModbusClient) {
+ if twin.PVisitor.PProperty.AccessMode == "ReadOnly" {
+ klog.V(1).Info("Visit readonly register: ", visitorConfig.Offset)
+ return
+ }
+
+ klog.V(2).Infof("Convert type: %s, value: %s ", twin.PVisitor.PProperty.DataType, twin.Desired.Value)
+ value, err := mappercommon.Convert(twin.PVisitor.PProperty.DataType, twin.Desired.Value)
+ if err != nil {
+ klog.Error(err)
+ return
+ }
+
+ valueInt, _ := value.(int64)
+ _, err = client.Set(visitorConfig.Register, visitorConfig.Offset, uint16(valueInt))
+ if err != nil {
+ klog.Error(err, visitorConfig)
+ return
+ }
+}
+
+// getDeviceID extract the device ID from Mqtt topic.
+func getDeviceID(topic string) (id string) {
+ re := regexp.MustCompile(`hw/events/device/(.+)/twin/update/delta`)
+ return re.FindStringSubmatch(topic)[1]
+}
+
+// onMessage callback function of Mqtt subscribe message.
+func onMessage(client mqtt.Client, message mqtt.Message) {
+ klog.V(2).Info("Receive message", message.Topic())
+ // Get device ID and get device instance
+ id := getDeviceID(message.Topic())
+ if id == "" {
+ klog.Error("Wrong topic")
+ return
+ }
+ klog.V(2).Info("Device id: ", id)
+
+ var dev *globals.ModbusDev
+ var ok bool
+ if dev, ok = devices[id]; !ok {
+ klog.Error("Device not exist")
+ return
+ }
+
+ // Get twin map key as the propertyName
+ var delta mappercommon.DeviceTwinDelta
+ if err := json.Unmarshal(message.Payload(), &delta); err != nil {
+ klog.Error("Unmarshal message failed: ", err)
+ return
+ }
+ for twinName, twinValue := range delta.Delta {
+ i := 0
+ for i = 0; i < len(dev.Instance.Twins); i++ {
+ if twinName == dev.Instance.Twins[i].PropertyName {
+ break
+ }
+ }
+ if i == len(dev.Instance.Twins) {
+ klog.Error("Twin not found: ", twinName)
+ continue
+ }
+ // Desired value is not changed.
+ if dev.Instance.Twins[i].Desired.Value == twinValue {
+ continue
+ }
+ dev.Instance.Twins[i].Desired.Value = twinValue
+ var visitorConfig configmap.ModbusVisitorConfig
+ if err := json.Unmarshal([]byte(dev.Instance.Twins[i].PVisitor.VisitorConfig), &visitorConfig); err != nil {
+ klog.Error("Unmarshal visitor config failed")
+ }
+ setVisitor(&visitorConfig, &dev.Instance.Twins[i], dev.ModbusClient)
+ }
+}
+
+// isRS485Enabled is RS485 feature enabled for RTU.
+func isRS485Enabled(customizedValue configmap.CustomizedValue) bool {
+ isEnabled := false
+
+ if len(customizedValue) != 0 {
+ if value, ok := customizedValue["serialType"]; ok {
+ if value == "RS485" {
+ isEnabled = true
+ }
+ }
+ }
+ return isEnabled
+}
+
+// initModbus initialize modbus client
+func initModbus(protocolConfig configmap.ModbusProtocolCommonConfig, slaveID int16) (client *driver.ModbusClient, err error) {
+ if protocolConfig.COM.SerialPort != "" {
+ modbusRTU := driver.ModbusRTU{SlaveID: byte(slaveID),
+ SerialName: protocolConfig.COM.SerialPort,
+ BaudRate: int(protocolConfig.COM.BaudRate),
+ DataBits: int(protocolConfig.COM.DataBits),
+ StopBits: int(protocolConfig.COM.StopBits),
+ Parity: protocolConfig.COM.Parity,
+ RS485Enabled: isRS485Enabled(protocolConfig.CustomizedValues),
+ Timeout: 5 * time.Second}
+ client, _ = driver.NewClient(modbusRTU)
+ } else if protocolConfig.TCP.IP != "" {
+ modbusTCP := driver.ModbusTCP{
+ SlaveID: byte(slaveID),
+ DeviceIP: protocolConfig.TCP.IP,
+ TCPPort: strconv.FormatInt(protocolConfig.TCP.Port, 10),
+ Timeout: 5 * time.Second}
+ client, _ = driver.NewClient(modbusTCP)
+ } else {
+ return nil, errors.New("No protocol found")
+ }
+ return client, nil
+}
+
+// initTwin initialize the timer to get twin value.
+func initTwin(dev *globals.ModbusDev) {
+ for i := 0; i < len(dev.Instance.Twins); i++ {
+ var visitorConfig configmap.ModbusVisitorConfig
+ if err := json.Unmarshal([]byte(dev.Instance.Twins[i].PVisitor.VisitorConfig), &visitorConfig); err != nil {
+ klog.Error(err)
+ continue
+ }
+ setVisitor(&visitorConfig, &dev.Instance.Twins[i], dev.ModbusClient)
+
+ twinData := TwinData{Client: dev.ModbusClient,
+ Name: dev.Instance.Twins[i].PropertyName,
+ Type: dev.Instance.Twins[i].Desired.Metadatas.Type,
+ RegisterType: visitorConfig.Register,
+ Address: visitorConfig.Offset,
+ Quantity: uint16(visitorConfig.Limit),
+ Topic: fmt.Sprintf(mappercommon.TopicTwinUpdate, dev.Instance.ID)}
+ collectCycle := time.Duration(dev.Instance.Twins[i].PVisitor.CollectCycle)
+ // If the collect cycle is not set, set it to 1 second.
+ if collectCycle == 0 {
+ collectCycle = 1 * time.Second
+ }
+ timer := mappercommon.Timer{Function: twinData.Run, Duration: collectCycle, Times: 0}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ timer.Start()
+ }()
+ }
+}
+
+// initData initialize the timer to get data.
+func initData(dev *globals.ModbusDev) {
+ for i := 0; i < len(dev.Instance.Datas.Properties); i++ {
+ var visitorConfig configmap.ModbusVisitorConfig
+ if err := json.Unmarshal([]byte(dev.Instance.Datas.Properties[i].PVisitor.VisitorConfig), &visitorConfig); err != nil {
+ klog.Error("Unmarshal visitor config failed")
+ }
+ twinData := TwinData{Client: dev.ModbusClient,
+ Name: dev.Instance.Datas.Properties[i].PropertyName,
+ Type: dev.Instance.Datas.Properties[i].Metadatas.Type,
+ RegisterType: visitorConfig.Register,
+ Address: visitorConfig.Offset,
+ Quantity: uint16(visitorConfig.Limit),
+ Topic: fmt.Sprintf(mappercommon.TopicDataUpdate, dev.Instance.ID)}
+ collectCycle := time.Duration(dev.Instance.Datas.Properties[i].PVisitor.CollectCycle)
+ // If the collect cycle is not set, set it to 1 second.
+ if collectCycle == 0 {
+ collectCycle = 1 * time.Second
+ }
+ timer := mappercommon.Timer{Function: twinData.Run, Duration: collectCycle, Times: 0}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ timer.Start()
+ }()
+ }
+}
+
+// initSubscribeMqtt subscribe Mqtt topics.
+func initSubscribeMqtt(instanceID string) error {
+ topic := fmt.Sprintf(mappercommon.TopicTwinUpdateDelta, instanceID)
+ klog.V(1).Info("Subscribe topic: ", topic)
+ return globals.MqttClient.Subscribe(topic, onMessage)
+}
+
+// initGetStatus start timer to get device status and send to eventbus.
+func initGetStatus(dev *globals.ModbusDev) {
+ getStatus := GetStatus{Client: dev.ModbusClient,
+ topic: fmt.Sprintf(mappercommon.TopicStateUpdate, dev.Instance.ID)}
+ timer := mappercommon.Timer{Function: getStatus.Run, Duration: 1 * time.Second, Times: 0}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ timer.Start()
+ }()
+}
+
+// start start the device.
+func start(dev *globals.ModbusDev) {
+ var protocolConfig configmap.ModbusProtocolCommonConfig
+ if err := json.Unmarshal([]byte(dev.Instance.PProtocol.ProtocolCommonConfig), &protocolConfig); err != nil {
+ klog.Error(err)
+ return
+ }
+
+ client, err := initModbus(protocolConfig, dev.Instance.PProtocol.ProtocolConfigs.SlaveID)
+ if err != nil {
+ klog.Error(err)
+ return
+ }
+ dev.ModbusClient = client
+
+ initTwin(dev)
+ initData(dev)
+
+ if err := initSubscribeMqtt(dev.Instance.ID); err != nil {
+ klog.Error(err)
+ return
+ }
+
+ initGetStatus(dev)
+}
+
+// DevInit initialize the device datas.
+func DevInit(configmapPath string) error {
+ devices = make(map[string]*globals.ModbusDev)
+ models = make(map[string]mappercommon.DeviceModel)
+ protocols = make(map[string]mappercommon.Protocol)
+ return configmap.Parse(configmapPath, devices, models, protocols)
+}
+
+// DevStart start all devices.
+func DevStart() {
+ for id, dev := range devices {
+ klog.V(4).Info("Dev: ", id, dev)
+ start(dev)
+ }
+ wg.Wait()
+}
diff --git a/mappers/modbus-go/device/devstatus.go b/mappers/modbus-go/device/devstatus.go
new file mode 100644
index 000000000..f4b90dab4
--- /dev/null
+++ b/mappers/modbus-go/device/devstatus.go
@@ -0,0 +1,47 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package device
+
+import (
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/driver"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/globals"
+ "k8s.io/klog"
+)
+
+// GetStatus is the timer structure for getting device status.
+type GetStatus struct {
+ Client *driver.ModbusClient
+ Status string
+ topic string
+}
+
+// Run timer function.
+func (gs *GetStatus) Run() {
+ gs.Status = gs.Client.GetStatus()
+
+ var payload []byte
+ var err error
+ if payload, err = mappercommon.CreateMessageState(gs.Status); err != nil {
+ klog.Error("Create message state failed: ", err)
+ return
+ }
+ if err = globals.MqttClient.Publish(gs.topic, payload); err != nil {
+ klog.Error("Publish failed: ", err)
+ return
+ }
+}
diff --git a/mappers/modbus-go/device/twindata.go b/mappers/modbus-go/device/twindata.go
new file mode 100644
index 000000000..798516d4a
--- /dev/null
+++ b/mappers/modbus-go/device/twindata.go
@@ -0,0 +1,67 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package device
+
+import (
+ "strconv"
+ "strings"
+
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/driver"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/globals"
+ "k8s.io/klog"
+)
+
+// TwinData is the timer structure for getting twin/data.
+type TwinData struct {
+ Client *driver.ModbusClient
+ Name string
+ Type string
+ RegisterType string
+ Address uint16
+ Quantity uint16
+ Results []byte
+ Topic string
+}
+
+// Run timer function.
+func (td *TwinData) Run() {
+ var err error
+ td.Results, err = td.Client.Get(td.RegisterType, td.Address, td.Quantity)
+ if err != nil {
+ klog.Error("Get register failed: ", err)
+ return
+ }
+ // construct payload
+ var payload []byte
+ if strings.Contains(td.Topic, "$hw") {
+ if payload, err = mappercommon.CreateMessageTwinUpdate(td.Name, td.Type, strconv.Itoa(int(td.Results[0]))); err != nil {
+ klog.Error("Create message twin update failed")
+ return
+ }
+ } else {
+ if payload, err = mappercommon.CreateMessageData(td.Name, td.Type, strconv.Itoa(int(td.Results[0]))); err != nil {
+ klog.Error("Create message data failed")
+ return
+ }
+ }
+ if err = globals.MqttClient.Publish(td.Topic, payload); err != nil {
+ klog.Error(err)
+ }
+
+ klog.V(2).Infof("Update value: %s, topic: %s", strconv.Itoa(int(td.Results[0])), td.Topic)
+}
diff --git a/mappers/modbus-go/driver/client.go b/mappers/modbus-go/driver/client.go
new file mode 100644
index 000000000..49fdfa0c6
--- /dev/null
+++ b/mappers/modbus-go/driver/client.go
@@ -0,0 +1,194 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package driver
+
+import (
+ "errors"
+ "sync"
+ "time"
+
+ "github.com/sailorvii/modbus"
+ "k8s.io/klog"
+)
+
+// ModbusTCP is the configurations of modbus TCP.
+type ModbusTCP struct {
+ SlaveID byte
+ DeviceIP string
+ TCPPort string
+ Timeout time.Duration
+}
+
+// ModbusRTU is the configurations of modbus RTU.
+type ModbusRTU struct {
+ SlaveID byte
+ SerialName string
+ BaudRate int
+ DataBits int
+ StopBits int
+ Parity string
+ RS485Enabled bool
+ Timeout time.Duration
+}
+
+// ModbusClient is the structure for modbus client.
+type ModbusClient struct {
+ Client modbus.Client
+ Handler interface{}
+ Config interface{}
+
+ mu sync.Mutex
+}
+
+/*
+* In modbus RTU mode, devices could connect to one serial port on RS485. However,
+* the serial port doesn't support paralleled visit, and for one tcp device, it also doesn't support
+* paralleled visit, so we expect one client for one port.
+ */
+var clients map[string]*ModbusClient
+
+func newTCPClient(config ModbusTCP) *ModbusClient {
+ addr := config.DeviceIP + ":" + config.TCPPort
+
+ if client, ok := clients[addr]; ok {
+ return client
+ }
+
+ if clients == nil {
+ clients = make(map[string]*ModbusClient)
+ }
+
+ handler := modbus.NewTCPClientHandler(addr)
+ handler.Timeout = config.Timeout
+ handler.IdleTimeout = config.Timeout
+ handler.SlaveId = config.SlaveID
+ client := ModbusClient{Client: modbus.NewClient(handler), Handler: handler, Config: config}
+ clients[addr] = &client
+ return &client
+}
+
+func newRTUClient(config ModbusRTU) *ModbusClient {
+ if client, ok := clients[config.SerialName]; ok {
+ return client
+ }
+
+ if clients == nil {
+ clients = make(map[string]*ModbusClient)
+ }
+
+ handler := modbus.NewRTUClientHandler(config.SerialName)
+ handler.BaudRate = config.BaudRate
+ handler.DataBits = config.DataBits
+ handler.Parity = parity(config.Parity)
+ handler.StopBits = config.StopBits
+ handler.SlaveId = config.SlaveID
+ handler.Timeout = config.Timeout
+ handler.IdleTimeout = config.Timeout
+ handler.RS485.Enabled = config.RS485Enabled
+ client := ModbusClient{Client: modbus.NewClient(handler), Handler: handler, Config: config}
+ clients[config.SerialName] = &client
+ return &client
+}
+
+// NewClient allocate and return a modbus client.
+// Client type includes TCP and RTU.
+func NewClient(config interface{}) (*ModbusClient, error) {
+ switch c := config.(type) {
+ case ModbusTCP:
+ return newTCPClient(c), nil
+ case ModbusRTU:
+ return newRTUClient(c), nil
+ default:
+ return &ModbusClient{}, errors.New("Wrong modbus type")
+ }
+}
+
+// GetStatus get device status.
+// Now we could only get the connection status.
+func (c *ModbusClient) GetStatus() string {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ err := c.Client.Connect()
+ if err == nil {
+ return DEVSTOK
+ }
+ return DEVSTDISCONN
+}
+
+// Get get register.
+func (c *ModbusClient) Get(registerType string, addr uint16, quantity uint16) (results []byte, err error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ switch registerType {
+ case "CoilRegister":
+ results, err = c.Client.ReadCoils(addr, quantity)
+ case "DiscreteInputRegister":
+ results, err = c.Client.ReadDiscreteInputs(addr, quantity)
+ case "HoldingRegister":
+ results, err = c.Client.ReadHoldingRegisters(addr, quantity)
+ case "InputRegister":
+ results, err = c.Client.ReadInputRegisters(addr, quantity)
+ default:
+ return nil, errors.New("Bad register type")
+ }
+ klog.V(2).Info("Get result: ", results)
+ return results, err
+}
+
+// Set set register.
+func (c *ModbusClient) Set(registerType string, addr uint16, value uint16) (results []byte, err error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ klog.V(1).Info("Set:", registerType, addr, value)
+
+ switch registerType {
+ case "CoilRegister":
+ var valueSet uint16
+ switch value {
+ case 0:
+ valueSet = 0x0000
+ case 1:
+ valueSet = 0xFF00
+ default:
+ return nil, errors.New("Wrong value")
+ }
+ results, err = c.Client.WriteSingleCoil(addr, valueSet)
+ case "DiscreteInputRegister":
+ results, err = c.Client.WriteSingleRegister(addr, value)
+ default:
+ return nil, errors.New("Bad register type")
+ }
+ klog.V(1).Info("Set result:", err, results)
+ return results, err
+}
+
+// parity convert into the format that modbus drvier requires.
+func parity(ori string) string {
+ var p string
+ switch ori {
+ case "even":
+ p = "E"
+ case "odd":
+ p = "O"
+ default:
+ p = "N"
+ }
+ return p
+}
diff --git a/mappers/modbus-go/driver/client_test.go b/mappers/modbus-go/driver/client_test.go
new file mode 100644
index 000000000..d960df240
--- /dev/null
+++ b/mappers/modbus-go/driver/client_test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// This application needs physical devices.
+// Please edit by demand for testing.
+
+package driver
+
+import (
+ "fmt"
+ "os"
+ "time"
+)
+
+func tdriver() {
+ var modbusrtu ModbusRTU
+
+ modbusrtu.SerialName = "/dev/ttyS0"
+ modbusrtu.BaudRate = 9600
+ modbusrtu.DataBits = 8
+ modbusrtu.StopBits = 1
+ modbusrtu.SlaveID = 1
+ modbusrtu.Parity = "N"
+ modbusrtu.Timeout = 2 * time.Second
+
+ client, err := NewClient(modbusrtu)
+ if err != nil {
+ fmt.Println("New client error")
+ os.Exit(1)
+ }
+
+ results, err := client.Set("DiscreteInputRegister", 2, 1)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fmt.Println(results)
+ results, err = client.Set("CoilRegister", 2, 1)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fmt.Println(results)
+ os.Exit(0)
+}
+
+func main() {
+ /*
+ var modbustcp ModbusTCP
+
+ modbustcp.DeviceIp = "192.168.56.1"
+ modbustcp.TcpPort = "502"
+ modbustcp.SlaveId = 0x1
+ client := NewClient(modbustcp)
+ if client == nil {
+ fmt.Println("New client error")
+ os.Exit(1)
+ }
+ fmt.Println("status: ", client.GetStatus())
+
+ results, err := client.Client.ReadDiscreteInputs(0, 1)
+ if err != nil {
+ fmt.Println("Read error: ", err)
+ os.Exit(1)
+ }
+ fmt.Println("result: ", results)
+ */
+ tdriver()
+ os.Exit(0)
+}
diff --git a/mappers/modbus-go/driver/const.go b/mappers/modbus-go/driver/const.go
new file mode 100644
index 000000000..a5e1fb581
--- /dev/null
+++ b/mappers/modbus-go/driver/const.go
@@ -0,0 +1,26 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package driver
+
+// Device status definition.
+const (
+ DEVSTOK = "OK"
+ DEVSTERR = "ERROR" /* Expected value is not equal as setting */
+ DEVSTDISCONN = "DISCONNECTED" /* Disconnected */
+ DEVSTUNHEALTHY = "UNHEALTHY" /* Unhealthy status from device */
+ DEVSTUNKNOWN = "UNKNOWN"
+)
diff --git a/mappers/modbus-go/globals/globals.go b/mappers/modbus-go/globals/globals.go
new file mode 100644
index 000000000..11c5f1079
--- /dev/null
+++ b/mappers/modbus-go/globals/globals.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package globals
+
+import (
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/driver"
+)
+
+// ModbusDev is the modbus device configuration and client information.
+type ModbusDev struct {
+ Instance mappercommon.DeviceInstance
+ ModbusClient *driver.ModbusClient
+}
+
+var MqttClient mappercommon.MqttClient
diff --git a/mappers/modbus-go/main.go b/mappers/modbus-go/main.go
new file mode 100644
index 000000000..e6cef42e8
--- /dev/null
+++ b/mappers/modbus-go/main.go
@@ -0,0 +1,56 @@
+/*
+Copyright 2020 The KubeEdge Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+ "os"
+
+ mappercommon "github.com/kubeedge/kubeedge/mappers/common"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/device"
+ "github.com/kubeedge/kubeedge/mappers/modbus-go/globals"
+ "k8s.io/klog"
+)
+
+func main() {
+ var err error
+ var config Config
+
+ klog.InitFlags(nil)
+ defer klog.Flush()
+
+ if err = config.Parse(); err != nil {
+ klog.Fatal(err)
+ os.Exit(1)
+ }
+ klog.V(4).Info(config.Configmap)
+
+ globals.MqttClient = mappercommon.MqttClient{IP: config.Mqtt.ServerAddress,
+ User: config.Mqtt.Username,
+ Passwd: config.Mqtt.Password,
+ Cert: config.Mqtt.Cert,
+ PrivateKey: config.Mqtt.PrivateKey}
+ if err = globals.MqttClient.Connect(); err != nil {
+ klog.Fatal(err)
+ os.Exit(1)
+ }
+
+ if err = device.DevInit(config.Configmap); err != nil {
+ klog.Fatal(err)
+ os.Exit(1)
+ }
+ device.DevStart()
+}