diff options
| author | sailorvii <35483373+sailorvii@users.noreply.github.com> | 2020-11-05 21:55:08 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-05 21:55:08 +0800 |
| commit | 90dfbd208021b1d248042335188be1d35994c7cb (patch) | |
| tree | a3445d6d7cc8699da4ce9722283407bdc4e3849b /mappers | |
| parent | Merge pull request #2009 from shenkonghui/feat/keadm-debug-collect (diff) | |
| download | kubeedge-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')
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() +} |
