diff options
| author | fisherxu <fisherxu1@gmail.com> | 2019-07-26 11:23:33 +0800 |
|---|---|---|
| committer | fisherxu <fisherxu1@gmail.com> | 2019-07-26 14:33:54 +0800 |
| commit | 26902a3027f10d4b66e8b72aabd2bc1e58066865 (patch) | |
| tree | f6b5e1736bb9ec7e11a6b3dbbd822290d694244e /mappers | |
| parent | Merge pull request #935 from sujithsimon22/ble (diff) | |
| download | kubeedge-26902a3027f10d4b66e8b72aabd2bc1e58066865.tar.gz | |
rename device dir to mappers
Diffstat (limited to 'mappers')
30 files changed, 3943 insertions, 0 deletions
diff --git a/mappers/bluetooth_mapper/Dockerfile b/mappers/bluetooth_mapper/Dockerfile new file mode 100644 index 000000000..b98cdf959 --- /dev/null +++ b/mappers/bluetooth_mapper/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:16.04 + +CMD mkdir -p kubeedge + +COPY . kubeedge/ + +WORKDIR kubeedge + +ENTRYPOINT ["/kubeedge/main","-logtostderr=true"]
\ No newline at end of file diff --git a/mappers/bluetooth_mapper/Makefile b/mappers/bluetooth_mapper/Makefile new file mode 100644 index 000000000..fa2e96698 --- /dev/null +++ b/mappers/bluetooth_mapper/Makefile @@ -0,0 +1,12 @@ + +# make bluetooth_mapper +.PHONY: default bluetooth_mapper lint bluetooth_mapper_image +bluetooth_mapper: + go build main.go + +lint: + golangci-lint run --disable-all -E golint ./... + go vet ./... + +bluetooth_mapper_image: bluetooth_mapper + docker build -t bluetooth_mapper:v1.0 . diff --git a/mappers/bluetooth_mapper/README.md b/mappers/bluetooth_mapper/README.md new file mode 100644 index 000000000..80b586a16 --- /dev/null +++ b/mappers/bluetooth_mapper/README.md @@ -0,0 +1,4 @@ + +# Bluetooth Mapper + +Click [here](https://github.com/kubeedge/kubeedge/blob/master/docs/mappers/bluetooth_mapper.md#bluetooth-mapper) for detailed documentation of the bluetooth mapper.
\ No newline at end of file diff --git a/mappers/bluetooth_mapper/action_manager/action_manager.go b/mappers/bluetooth_mapper/action_manager/action_manager.go new file mode 100644 index 000000000..4336e19ca --- /dev/null +++ b/mappers/bluetooth_mapper/action_manager/action_manager.go @@ -0,0 +1,133 @@ +/* +Copyright 2019 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 actionmanager + +import ( + "errors" + "fmt" + "strings" + + "github.com/golang/glog" + "github.com/paypal/gatt" + + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/data_converter" +) + +const ( + ActionWrite = "WRITE" + ActionRead = "READ" +) + +// GattPeripheral represents the remote gatt peripheral device +var GattPeripheral gatt.Peripheral + +// CharacteristicsList contains the set of characteristics exposed by the device +var CharacteristicsList = make([]*gatt.Characteristic, 0) + +// Operation is structure to define device operation +type Operation struct { + // Action can be one of read/write corresponding to get/set respectively + Action string `yaml:"action" json:"action"` + // Characteristic refers to the characteristic on which the operation needs to be performed + CharacteristicUUID string `yaml:"characteristic-uuid" json:"characteristic-uuid"` + // Value is the value to be written in case of write action and value read from the device in case of read action + Value []byte `yaml:"value" json:"value"` +} + +// Action is structure to define a device action +type Action struct { + // PerformImmediately indicates whether the action is to be performed immediately or not + PerformImmediately bool `yaml:"perform-immediately" json:"perform-immediately"` + // Name is the name of the Action + Name string `yaml:"name" json:"name"` + // Operation specifies the operation to be performed for this action + Operation Operation `yaml:"operation" json:"operation"` +} + +//ActionManager is a structure that contains a list of actions +type ActionManager struct { + Actions []Action `yaml:"actions"` +} + +//PerformOperation executes the operation +func (action *Action) PerformOperation(readConverter ...dataconverter.DataRead) { + glog.Infof("Performing operations associated with action: %s", action.Name) + characteristic, err := FindCharacteristic(action.Operation.CharacteristicUUID) + if err != nil { + glog.Errorf("Error in finding characteristics: %s", err) + } + if strings.ToUpper(action.Operation.Action) == ActionRead { + readValue, err := ReadCharacteristic(GattPeripheral, characteristic) + if err != nil { + glog.Errorf("Error in reading characteristic: %s", err) + return + } + converted := false + for _, conversionAction := range readConverter[0].Actions { + if strings.ToUpper(conversionAction.ActionName) == strings.ToUpper(action.Name) { + convertedValue := fmt.Sprintf("%f", conversionAction.ConversionOperation.ConvertReadData(readValue)) + action.Operation.Value = []byte(convertedValue) + converted = true + } + } + if !converted { + action.Operation.Value = readValue + } + glog.Infof("Read Successful") + } else if strings.ToUpper(action.Operation.Action) == ActionWrite { + if action.Operation.Value == nil { + glog.Errorf("Please provide a value to be written") + return + } + err := WriteCharacteristic(GattPeripheral, characteristic, action.Operation.Value) + if err != nil { + glog.Errorf("Error in writing characteristic: %s", err) + return + } + glog.Infof("Write Successful") + } +} + +//FindCharacteristic is used to find the bluetooth characteristic +func FindCharacteristic(characteristicUUID string) (*gatt.Characteristic, error) { + for _, c := range CharacteristicsList { + if c.UUID().String() == characteristicUUID { + return c, nil + } + } + return nil, errors.New("unable to find the specified characteristic: " + characteristicUUID) +} + +//ReadCharacteristic is used to read the value of the characteristic +func ReadCharacteristic(p gatt.Peripheral, c *gatt.Characteristic) ([]byte, error) { + value, err := p.ReadCharacteristic(c) + if err != nil { + glog.Errorf("Error in reading characteristic, err: %s\n", err) + return nil, err + } + return value, nil +} + +//WriteCharacteristic is used to write some value into the characteristic +func WriteCharacteristic(p gatt.Peripheral, c *gatt.Characteristic, b []byte) error { + err := p.WriteCharacteristic(c, b, false) + if err != nil { + glog.Errorf("Error in writing characteristic, err: %s\n", err) + return err + } + return nil +} diff --git a/mappers/bluetooth_mapper/configuration/config.go b/mappers/bluetooth_mapper/configuration/config.go new file mode 100644 index 000000000..eb216ccae --- /dev/null +++ b/mappers/bluetooth_mapper/configuration/config.go @@ -0,0 +1,217 @@ +/* +Copyright 2019 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 configuration + +import ( + "encoding/json" + "errors" + "io/ioutil" + "reflect" + "strings" + + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/action_manager" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/data_converter" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/scheduler" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/watcher" + + "gopkg.in/yaml.v2" +) + +//ConfigFilePath contains the location of the configuration file +var ConfigFilePath = "configuration/config.yaml" + +//ConfigMapPath contains the location of the configuration file +var ConfigMapPath = "/opt/kubeedge/deviceProfile.json" + +//Config is the global configuration used by all the modules of the mapper +var Config *BLEConfig + +//Blutooth protocol name +const ( + ProtocolName string = "BLUETOOTH" + READWRITE string = "ReadWrite" + READ string = "ReadOnly" +) + +//BLEConfig is the main structure that stores the configuration information read from both the config file as well as the config map +type BLEConfig struct { + Mqtt Mqtt `yaml:"mqtt"` + Device Device `yaml:"device"` + Watcher watcher.Watcher `yaml:"watcher"` + Scheduler scheduler.Scheduler `yaml:"scheduler"` + ActionManager actionmanager.ActionManager `yaml:"action-manager"` + Converter dataconverter.Converter `yaml:"data-converter"` +} + +//ReadConfigFile is the structure that is used to read the config file to get configuration information from the user +type ReadConfigFile struct { + Mqtt Mqtt `yaml:"mqtt"` + DeviceModelName string `yaml:"device-model-name"` + ActionManager ActionManagerConfig `yaml:"action-manager"` + Watcher watcher.Watcher `yaml:"watcher"` + Scheduler scheduler.Scheduler `yaml:"scheduler"` +} + +//ActionManagerConfig is a structure that contains a list of actions +type ActionManagerConfig struct { + Actions []Action `yaml:"actions"` +} + +//Action is structure to define a device action +type Action struct { + //PerformImmediately signifies whether the action is to be performed by the action-manager immediately or not + PerformImmediately bool `yaml:"perform-immediately" json:"perform-immediately"` + //Name is the name of the Action + Name string `yaml:"name" json:"name"` + //PropertyName is the name of the property defined in the device CRD + PropertyName string `yaml:"device-property-name" json:"device-property-name"` +} + +//Mqtt structure contains the MQTT specific configurations +type Mqtt struct { + Mode int `yaml:"mode"` + InternalServer string `yaml:"internal-server"` + Server string `yaml:"server"` +} + +//Device structure contains the device specific configurations +type Device struct { + ID string `yaml:"id"` + Name string `yaml:"name"` +} + +//ReadFromConfigFile is used to load the information from the configuration file +func (readConfigFile *ReadConfigFile) ReadFromConfigFile() error { + yamlFile, err := ioutil.ReadFile(ConfigFilePath) + if err != nil { + return err + } + err = yaml.Unmarshal(yamlFile, readConfigFile) + if err != nil { + return err + } + return nil +} + +//Load is used to consolidate the information loaded from the configuration file and the configmaps +func (b *BLEConfig) Load() error { + readConfigFile := ReadConfigFile{} + readConfigMap := DeviceProfile{} + err := readConfigFile.ReadFromConfigFile() + if err != nil { + return errors.New("Error while reading from configuration file " + err.Error()) + } + err = readConfigMap.ReadFromConfigMap() + if err != nil { + return errors.New("Error while reading from config map " + err.Error()) + } + b.Mqtt = readConfigFile.Mqtt + b.Scheduler = readConfigFile.Scheduler + b.Watcher = readConfigFile.Watcher + // Assign device information obtained from config file + for _, device := range readConfigMap.DeviceInstances { + if strings.ToUpper(device.Model) == strings.ToUpper(readConfigFile.DeviceModelName) { + b.Device.ID = device.ID + b.Device.Name = device.Model + } + } + // Assign information required by action manager + for _, actionConfig := range readConfigFile.ActionManager.Actions { + action := actionmanager.Action{} + action.Name = actionConfig.Name + action.PerformImmediately = actionConfig.PerformImmediately + + for _, propertyVisitor := range readConfigMap.PropertyVisitors { + if strings.ToUpper(propertyVisitor.ModelName) == strings.ToUpper(b.Device.Name) && strings.ToUpper(propertyVisitor.PropertyName) == strings.ToUpper(actionConfig.PropertyName) && strings.ToUpper(propertyVisitor.Protocol) == ProtocolName { + propertyVisitorBytes, err := json.Marshal(propertyVisitor.VisitorConfig) + if err != nil { + return errors.New("Error in marshalling data property visitor configuration: " + err.Error()) + } + bluetoothPropertyVisitor := VisitorConfigBluetooth{} + err = json.Unmarshal(propertyVisitorBytes, &bluetoothPropertyVisitor) + if err != nil { + return errors.New("Error in unmarshalling data property visitor configuration: " + err.Error()) + } + action.Operation.CharacteristicUUID = bluetoothPropertyVisitor.CharacteristicUUID + newBluetoothVisitorConfig := VisitorConfigBluetooth{} + if !reflect.DeepEqual(bluetoothPropertyVisitor.BluetoothDataConverter, newBluetoothVisitorConfig.BluetoothDataConverter) { + readAction := dataconverter.ReadAction{} + readAction.ActionName = actionConfig.Name + readAction.ConversionOperation.StartIndex = bluetoothPropertyVisitor.BluetoothDataConverter.StartIndex + readAction.ConversionOperation.EndIndex = bluetoothPropertyVisitor.BluetoothDataConverter.EndIndex + readAction.ConversionOperation.ShiftRight = bluetoothPropertyVisitor.BluetoothDataConverter.ShiftRight + readAction.ConversionOperation.ShiftLeft = bluetoothPropertyVisitor.BluetoothDataConverter.ShiftLeft + for _, readOperations := range bluetoothPropertyVisitor.BluetoothDataConverter.OrderOfOperations { + readAction.ConversionOperation.OrderOfExecution = append(readAction.ConversionOperation.OrderOfExecution, readOperations.BluetoothOperationType) + switch strings.ToUpper(readOperations.BluetoothOperationType) { + case strings.ToUpper(BluetoothAdd): + readAction.ConversionOperation.Add = readOperations.BluetoothOperationValue + case strings.ToUpper(BluetoothSubtract): + readAction.ConversionOperation.Subtract = readOperations.BluetoothOperationValue + case strings.ToUpper(BluetoothMultiply): + readAction.ConversionOperation.Multiply = readOperations.BluetoothOperationValue + case strings.ToUpper(BluetoothDivide): + readAction.ConversionOperation.Divide = readOperations.BluetoothOperationValue + } + } + b.Converter.DataRead.Actions = append(b.Converter.DataRead.Actions, readAction) + } + if bluetoothPropertyVisitor.DataWriteToBluetooth != nil { + writeAttribute := dataconverter.WriteAttribute{} + writeAttribute.Operations = make(map[string]dataconverter.DataMap, 1) + dataMap := dataconverter.DataMap{} + dataMap.DataMapping = bluetoothPropertyVisitor.DataWriteToBluetooth + writeAttribute.Operations[actionConfig.Name] = dataMap + writeAttribute.Name = propertyVisitor.PropertyName + b.Converter.DataWrite.Attributes = append(b.Converter.DataWrite.Attributes, writeAttribute) + } + } + } + for _, deviceModel := range readConfigMap.DeviceModels { + if strings.ToUpper(deviceModel.Name) == strings.ToUpper(b.Device.Name) { + for _, property := range deviceModel.Properties { + if strings.ToUpper(property.Name) == strings.ToUpper(actionConfig.PropertyName) { + if property.AccessMode == READWRITE { + action.Operation.Action = "Write" + if strings.ToUpper(property.DataType) == "INT" { + value := string(int(property.DefaultValue.(float64))) + action.Operation.Value = []byte(value) + } else if strings.ToUpper(property.DataType) == "STRING" { + for _, converterAttribute := range b.Converter.DataWrite.Attributes { + if strings.ToUpper(converterAttribute.Name) == strings.ToUpper(actionConfig.PropertyName) { + for operationName, dataMap := range converterAttribute.Operations { + if action.Name == operationName { + if _, ok := dataMap.DataMapping[property.DefaultValue.(string)]; ok { + action.Operation.Value = dataMap.DataMapping[property.DefaultValue.(string)] + } + } + } + } + } + } + } else if property.AccessMode == READ { + action.Operation.Action = "Read" + } + } + } + } + } + b.ActionManager.Actions = append(b.ActionManager.Actions, action) + } + Config = b + return nil +} diff --git a/mappers/bluetooth_mapper/configuration/config.yaml b/mappers/bluetooth_mapper/configuration/config.yaml new file mode 100644 index 000000000..b536bcf44 --- /dev/null +++ b/mappers/bluetooth_mapper/configuration/config.yaml @@ -0,0 +1,40 @@ +mqtt: + mode: 0 # 0 -internal mqtt broker 1 - external mqtt broker + server: tcp://127.0.0.1:1883 # external mqtt broker url. + internal-server: tcp://127.0.0.1:1884 # internal mqtt broker url. +device-model-name: cc2650-sensortag +action-manager: + actions: + - name: IRTemperatureConfiguration + perform-immediately: true + device-property-name: temperature-enable #property-name defined in the device model + - name: IRTemperatureData + perform-immediately: false + device-property-name: temperature #property-name defined in the device model + - name: IOConfigurationInitialize + perform-immediately: true + device-property-name: io-config-initialize #property-name defined in the device model + - name: IODataInitialize + perform-immediately: true + device-property-name: io-data-initialize #property-name defined in the device model + - name: IOConfiguration + perform-immediately: true + device-property-name: io-config #property-name defined in the device model + - name: IOData + perform-immediately: false + device-property-name: io-data #property-name defined in the device model +scheduler: + schedules: + - name: temperature + interval: 3000 + occurrence-limit: 10 # if it is 0, then the event will execute infinitely + actions: + - IRTemperatureData # Action name defined in the action-manager section +watcher: + device-twin-attributes : + - device-property-name: io-data # the twin attribute name defined while creating device + actions: # list of action names, defined in the action-manager section, to be executed on the device + - IOConfigurationInitialize + - IODataInitialize + - IOConfiguration + - IOData diff --git a/mappers/bluetooth_mapper/configuration/config_map_types.go b/mappers/bluetooth_mapper/configuration/config_map_types.go new file mode 100644 index 000000000..855b8d930 --- /dev/null +++ b/mappers/bluetooth_mapper/configuration/config_map_types.go @@ -0,0 +1,134 @@ +/* +Copyright 2019 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 configuration + +import ( + "encoding/json" + "io/ioutil" +) + +// Bluetooth Protocol Operation type +const ( + BluetoothAdd string = "Add" + BluetoothSubtract string = "Subtract" + BluetoothMultiply string = "Multiply" + BluetoothDivide string = "Divide" +) + +// 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"` + PropertyVisitors []PropertyVisitor `json:"propertyVisitors,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"` + Protocol string `json:"protocol,omitempty"` + Model string `json:"model,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"` + ProtocolConfig interface{} `json:"protocol_config,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"` + Protocol string `json:"protocol,omitempty"` + VisitorConfig interface{} `json:"visitorConfig,omitempty"` +} + +// Common visitor configurations for bluetooth protocol +type VisitorConfigBluetooth struct { + // Required: Unique ID of the corresponding operation + CharacteristicUUID string `json:"characteristicUUID,omitempty"` + // Responsible for converting the data coming from the platform into a form that is understood by the bluetooth device + // For example: "ON":[1], "OFF":[0] + //+optional + DataWriteToBluetooth map[string][]byte `json:"dataWrite,omitempty"` + // Responsible for converting the data being read from the bluetooth device into a form that is understandable by the platform + //+optional + BluetoothDataConverter BluetoothReadConverter `json:"dataConverter,omitempty"` +} + +// Specifies the operations that may need to be performed to convert the data +type BluetoothReadConverter struct { + // Required: Specifies the start index of the incoming byte stream to be considered to convert the data. + // For example: start-index:2, end-index:3 concatenates the value present at second and third index of the incoming byte stream. If we want to reverse the order we can give it as start-index:3, end-index:2 + StartIndex int `json:"startIndex,omitempty"` + // Required: Specifies the end index of incoming byte stream to be considered to convert the data + // the value specified should be inclusive for example if 3 is specified it includes the third index + EndIndex int `json:"endIndex,omitempty"` + // Refers to the number of bits to shift left, if left-shift operation is necessary for conversion + // +optional + ShiftLeft uint `json:"shiftLeft,omitempty"` + // Refers to the number of bits to shift right, if right-shift operation is necessary for conversion + // +optional + ShiftRight uint `json:"shiftRight,omitempty"` + // Specifies in what order the operations(which are required to be performed to convert incoming data into understandable form) are performed + //+optional + OrderOfOperations []BluetoothOperations `json:"orderOfOperations,omitempty"` +} + +// Specify the operation that should be performed to convert incoming data into understandable form +type BluetoothOperations struct { + // Required: Specifies the operation to be performed to convert incoming data + BluetoothOperationType string `json:"operationType,omitempty"` + // Required: Specifies with what value the operation is to be performed + BluetoothOperationValue float64 `json:"operationValue,omitempty"` +} + +//ReadFromConfigMap is used to load the information from the configmaps that are provided from the cloud +func (deviceProfile *DeviceProfile) ReadFromConfigMap() error { + jsonFile, err := ioutil.ReadFile(ConfigMapPath) + if err != nil { + return err + } + err = json.Unmarshal(jsonFile, deviceProfile) + if err != nil { + return err + } + return nil +} diff --git a/mappers/bluetooth_mapper/controller/controller.go b/mappers/bluetooth_mapper/controller/controller.go new file mode 100644 index 000000000..6bbc1238c --- /dev/null +++ b/mappers/bluetooth_mapper/controller/controller.go @@ -0,0 +1,228 @@ +/* +Copyright 2019 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 controller + +import ( + "encoding/json" + "strings" + + MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/golang/glog" + "github.com/paypal/gatt" + "github.com/paypal/gatt/examples/option" + + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/action_manager" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/configuration" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/data_converter" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/helper" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/scheduler" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/watcher" +) + +// constants which can be used to convey topic information +const ( + MapperTopicPrefix = "$ke/device/bluetooth-mapper/" + WatcherTopicSuffix = "/watcher/create" + SchedulerCreateTopicSuffix = "/scheduler/create" + SchedulerDeleteTopicSuffix = "/scheduler/delete" + ActionManagerCreateTopicSuffix = "/action-manager/create" + ActionManagerDeleteTopicSuffix = "/action-manager/delete" +) + +var topicMap = make(map[string]MQTT.MessageHandler) + +//Config contains the configuration used by the controller +type Config struct { + Mqtt configuration.Mqtt `yaml:"mqtt"` + Device configuration.Device `yaml:"device"` + Watcher watcher.Watcher `yaml:"watcher"` + Scheduler scheduler.Scheduler `yaml:"scheduler"` + ActionManager actionmanager.ActionManager `yaml:"action-manager"` + Converter dataconverter.Converter `yaml:"data-converter"` +} + +// initTopicMap initializes topics to their respective handler functions +func (c *Config) initTopicMap() { + topicMap[MapperTopicPrefix+c.Device.ID+WatcherTopicSuffix] = c.handleWatchMessage + topicMap[MapperTopicPrefix+c.Device.ID+SchedulerCreateTopicSuffix] = c.handleScheduleCreateMessage + topicMap[MapperTopicPrefix+c.Device.ID+SchedulerDeleteTopicSuffix] = c.handleScheduleDeleteMessage + topicMap[MapperTopicPrefix+c.Device.ID+ActionManagerCreateTopicSuffix] = c.handleActionCreateMessage + topicMap[MapperTopicPrefix+c.Device.ID+ActionManagerDeleteTopicSuffix] = c.handleActionDeleteMessage +} + +//Start starts the controller of the mapper +func (c *Config) Start() { + c.initTopicMap() + helper.MqttConnect(c.Mqtt.Mode, c.Mqtt.InternalServer, c.Mqtt.Server) + subscribeAllTopics() + helper.ControllerWg.Add(1) + device, err := gatt.NewDevice(option.DefaultClientOptions...) + if err != nil { + glog.Fatalf("Failed to open device, err: %s\n", err) + return + } + go c.Watcher.Initiate(device, c.Device.Name, c.Device.ID, c.ActionManager.Actions, c.Converter) + + <-watcher.DeviceConnected + for _, action := range c.ActionManager.Actions { + if action.PerformImmediately { + action.PerformOperation(c.Converter.DataRead) + } + } + + for _, schedule := range c.Scheduler.Schedules { + helper.ControllerWg.Add(1) + go schedule.ExecuteSchedule(c.ActionManager.Actions, c.Converter.DataRead, c.Device.ID) + } + helper.ControllerWg.Wait() +} + +//subscribeAllTopics subscribes to mqtt topics associated with mapper +func subscribeAllTopics() { + for key, value := range topicMap { + helper.TokenClient = helper.Client.Subscribe(key, 0, value) + if helper.TokenClient.Wait() && helper.TokenClient.Error() != nil { + glog.Errorf("subscribe() Error in topic: %s is: %s", key, helper.TokenClient.Error()) + } + } +} + +//handleWatchMessage is the MQTT handler function for changing watcher configuration at runtime +func (c *Config) handleWatchMessage(client MQTT.Client, message MQTT.Message) { + newWatch := watcher.Watcher{} + err := json.Unmarshal(message.Payload(), &newWatch) + if err != nil { + glog.Errorf("Error in unmarshalling: %s", err) + } + c.Watcher = newWatch + configuration.Config.Watcher = c.Watcher + glog.Infof("New watcher has been started") + glog.Infof("New Watcher: %v", c.Watcher) +} + +//handleScheduleCreateMessage is the MQTT handler function for adding schedules at runtime +func (c *Config) handleScheduleCreateMessage(client MQTT.Client, message MQTT.Message) { + newSchedules := []scheduler.Schedule{} + err := json.Unmarshal(message.Payload(), &newSchedules) + if err != nil { + glog.Errorf("Error in unmarshalling: %s", err) + } + for _, newSchedule := range newSchedules { + scheduleExists := false + for scheduleIndex, schedule := range c.Scheduler.Schedules { + if schedule.Name == newSchedule.Name { + c.Scheduler.Schedules[scheduleIndex] = newSchedule + scheduleExists = true + break + } + } + if scheduleExists { + c.Scheduler.Schedules = append(c.Scheduler.Schedules, newSchedule) + glog.Infof("Schedule: %s has been updated", newSchedule.Name) + glog.Infof("Updated Schedule: %v", newSchedule) + } else { + glog.Infof("Schedule: %s has been added", newSchedule.Name) + glog.Infof("New Schedule: %v", newSchedule) + } + configuration.Config.Scheduler = c.Scheduler + helper.ControllerWg.Add(1) + newSchedule.ExecuteSchedule(c.ActionManager.Actions, c.Converter.DataRead, c.Device.ID) + } +} + +//handleScheduleDeleteMessage is the MQTT handler function for deleting schedules at runtime +func (c *Config) handleScheduleDeleteMessage(client MQTT.Client, message MQTT.Message) { + schedulesToBeDeleted := []scheduler.Schedule{} + err := json.Unmarshal(message.Payload(), &schedulesToBeDeleted) + if err != nil { + glog.Errorf("Error in unmarshalling: %s", err) + } + for _, scheduleToBeDeleted := range schedulesToBeDeleted { + scheduleExists := false + for index, schedule := range c.Scheduler.Schedules { + if strings.ToUpper(schedule.Name) == strings.ToUpper(scheduleToBeDeleted.Name) { + scheduleExists = true + copy(c.Scheduler.Schedules[index:], c.Scheduler.Schedules[index+1:]) + c.Scheduler.Schedules = c.Scheduler.Schedules[:len(c.Scheduler.Schedules)-1] + break + } + } + configuration.Config.Scheduler = c.Scheduler + if !scheduleExists { + glog.Errorf("Schedule: %s does not exist", scheduleToBeDeleted.Name) + } else { + glog.Infof("Schedule: %s has been deleted ", scheduleToBeDeleted.Name) + } + } +} + +//handleActionCreateMessage MQTT handler function for adding actions at runtime +func (c *Config) handleActionCreateMessage(client MQTT.Client, message MQTT.Message) { + newActions := []actionmanager.Action{} + err := json.Unmarshal(message.Payload(), &newActions) + if err != nil { + glog.Errorf("Error in unmarshalling: %s", err) + } + for _, newAction := range newActions { + actionExists := false + for actionIndex, action := range c.ActionManager.Actions { + if action.Name == newAction.Name { + c.ActionManager.Actions[actionIndex] = newAction + actionExists = true + break + } + } + if actionExists { + c.ActionManager.Actions = append(c.ActionManager.Actions, newAction) + glog.Infof("Action: %s has been updated", newAction.Name) + glog.Infof("Updated Action: %v", newAction) + } else { + glog.Infof("Action: %s has been added ", newAction.Name) + glog.Infof("New Action: %v", newAction) + } + configuration.Config.ActionManager = c.ActionManager + if newAction.PerformImmediately { + newAction.PerformOperation(c.Converter.DataRead) + } + } +} + +//handleActionDeleteMessage MQTT handler function for deleting actions at runtime +func (c *Config) handleActionDeleteMessage(client MQTT.Client, message MQTT.Message) { + actionsToBeDeleted := []actionmanager.Action{} + err := json.Unmarshal(message.Payload(), &actionsToBeDeleted) + if err != nil { + glog.Errorf("Error in unmarshalling: %s", err) + } + for _, actionToBeDeleted := range actionsToBeDeleted { + actionExists := false + for index, action := range c.ActionManager.Actions { + if strings.ToUpper(action.Name) == strings.ToUpper(actionToBeDeleted.Name) { + actionExists = true + copy(c.ActionManager.Actions[index:], c.ActionManager.Actions[index+1:]) + c.ActionManager.Actions = c.ActionManager.Actions[:len(c.ActionManager.Actions)-1] + break + } + } + configuration.Config.ActionManager = c.ActionManager + if !actionExists { + glog.Errorf("Action: %s did not exist", actionToBeDeleted.Name) + } else { + glog.Infof("Action: %s has been deleted ", actionToBeDeleted.Name) + } + } +} diff --git a/mappers/bluetooth_mapper/data_converter/data_converter.go b/mappers/bluetooth_mapper/data_converter/data_converter.go new file mode 100644 index 000000000..833bfc38f --- /dev/null +++ b/mappers/bluetooth_mapper/data_converter/data_converter.go @@ -0,0 +1,117 @@ +/* +Copyright 2019 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 dataconverter + +import ( + "strconv" + "strings" +) + +// Bluetooth Protocol Operation type +const ( + BluetoothAdd string = "Add" + BluetoothSubtract string = "Subtract" + BluetoothMultiply string = "Multiply" + BluetoothDivide string = "Divide" +) + +//Converter is the structure that contains data conversion specific configuration +type Converter struct { + DataWrite DataWrite `yaml:"write"` + DataRead DataRead `yaml:"read"` +} + +//dataWrite structure contains configuration information specific to data-writes +type DataWrite struct { + Attributes []WriteAttribute `yaml:"attributes"` +} + +//WriteAttribute structure contains the name of the attribute as well as a data-map of values to be written +type WriteAttribute struct { + Name string `yaml:"name"` + Operations map[string]DataMap `yaml:"operations"` +} + +//DataMap structure contains a mapping between the value that arrives from the platform (expected value) and +// the byte value to be written into the device +type DataMap struct { + DataMapping map[string][]byte `yaml:"data-map"` +} + +//dataRead structure contains configuration information specific to data-read +type DataRead struct { + Actions []ReadAction `yaml:"actions"` +} + +//ReadAction specifies the name of the action along with the conversion operations to be performed in case of data-read +type ReadAction struct { + ActionName string `yaml:"action-name"` + ConversionOperation ReadOperation `yaml:"conversion-operation"` +} + +//ReadOperation specifies how to convert the data received from the device into meaningful data +type ReadOperation struct { + StartIndex int `yaml:"start-index"` + EndIndex int `yaml:"end-index"` + ShiftLeft uint `yaml:"shift-left"` + ShiftRight uint `yaml:"shift-right"` + Multiply float64 `yaml:"multiply"` + Divide float64 `yaml:"divide"` + Add float64 `yaml:"add"` + Subtract float64 `yaml:"subtract"` + OrderOfExecution []string `yaml:"order-of-execution"` +} + +//ConvertReadData is the function responsible to convert the data read from the device into meaningful data +func (operation *ReadOperation) ConvertReadData(data []byte) float64 { + var intermediateResult uint64 + var initialValue []byte + var initialStringValue = "" + if operation.StartIndex <= operation.EndIndex { + for index := operation.StartIndex; index <= operation.EndIndex; index++ { + initialValue = append(initialValue, data[index]) + } + } else { + for index := operation.StartIndex; index >= operation.EndIndex; index-- { + initialValue = append(initialValue, data[index]) + } + } + for _, value := range initialValue { + initialStringValue = initialStringValue + strconv.Itoa(int(value)) + } + initialByteValue, _ := strconv.ParseUint(initialStringValue, 16, 16) + + if operation.ShiftLeft != 0 { + intermediateResult = initialByteValue << operation.ShiftLeft + } else if operation.ShiftRight != 0 { + intermediateResult = initialByteValue >> operation.ShiftRight + } + finalResult := float64(intermediateResult) + for _, executeOperation := range operation.OrderOfExecution { + switch strings.ToUpper(executeOperation) { + case strings.ToUpper(BluetoothAdd): + finalResult = finalResult + operation.Add + case strings.ToUpper(BluetoothSubtract): + finalResult = finalResult - operation.Subtract + case strings.ToUpper(BluetoothMultiply): + finalResult = finalResult * operation.Multiply + case strings.ToUpper(BluetoothDivide): + finalResult = finalResult / operation.Divide + } + } + return finalResult +} diff --git a/mappers/bluetooth_mapper/deployment.yaml b/mappers/bluetooth_mapper/deployment.yaml new file mode 100644 index 000000000..3d236ec84 --- /dev/null +++ b/mappers/bluetooth_mapper/deployment.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bluetooth-device-mapper-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: bluetooth-mapper + template: + metadata: + labels: + app: bluetooth-mapper + spec: + hostNetwork: true + containers: + - name: bluetooth-mapper-container + image: <your_dockerhub_username>/bluetooth_mapper:v1.0 + imagePullPolicy: Always + securityContext: + privileged: true + volumeMounts: + - name: config-volume + mountPath: /opt/kubeedge/ + nodeSelector: + bluetooth: "true" + volumes: + - name: config-volume + configMap: + name: device-profile-config-<edge_node_name> + restartPolicy: Always diff --git a/mappers/bluetooth_mapper/helper/helper.go b/mappers/bluetooth_mapper/helper/helper.go new file mode 100644 index 000000000..6ef5ed394 --- /dev/null +++ b/mappers/bluetooth_mapper/helper/helper.go @@ -0,0 +1,215 @@ +/* +Copyright 2019 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 helper + +import ( + "crypto/tls" + "encoding/json" + "sync" + "time" + + MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/golang/glog" +) + +var ( + DeviceETPrefix = "$hw/events/device/" + DeviceETStateUpdateSuffix = "/state/update" + TwinETUpdateSuffix = "/twin/update" + TwinETCloudSyncSuffix = "/twin/cloud_updated" + TwinETGetSuffix = "/twin/get" + TwinETGetResultSuffix = "/twin/get/result" +) + +var TwinResult DeviceTwinResult +var Wg sync.WaitGroup +var ControllerWg sync.WaitGroup +var TwinAttributes []string + +var TokenClient Token +var ClientOpts *MQTT.ClientOptions +var Client MQTT.Client + +//Token interface to validate the MQTT connection. +type Token interface { + Wait() bool + WaitTimeout(time.Duration) bool + Error() error +} + +//DeviceStateUpdate is the structure used in updating the device state +type DeviceStateUpdate struct { + State string `json:"state,omitempty"` +} + +//BaseMessage the base struct of event message +type BaseMessage struct { + EventID string `json:"event_id"` + Timestamp int64 `json:"timestamp"` +} + +//TwinValue the struct 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 struct 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 struct 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"` +} + +// HubclientInit create mqtt client config +func HubClientInit(server, clientID, username, password string) *MQTT.ClientOptions { + opts := MQTT.NewClientOptions().AddBroker(server).SetClientID(clientID).SetCleanSession(true) + if username != "" { + opts.SetUsername(username) + if password != "" { + opts.SetPassword(password) + } + } + tlsConfig := &tls.Config{InsecureSkipVerify: true, ClientAuth: tls.NoClientCert} + opts.SetTLSConfig(tlsConfig) + return opts +} + +//MqttConnect function felicitates the MQTT connection +func MqttConnect(mqttMode int, mqttInternalServer, mqttServer string) { + // Initiate the MQTT connection + if mqttMode == 0 { + ClientOpts = HubClientInit(mqttInternalServer, "eventbus", "", "") + } else if mqttMode == 1 { + ClientOpts = HubClientInit(mqttServer, "eventbus", "", "") + } + Client = MQTT.NewClient(ClientOpts) + if TokenClient = Client.Connect(); TokenClient.Wait() && TokenClient.Error() != nil { + glog.Errorf("client.Connect() Error is %s", TokenClient.Error()) + } +} + +//ChangeTwinValue sends the updated twin value to the edge through the MQTT broker +func ChangeTwinValue(updateMessage DeviceTwinUpdate, deviceID string) { + twinUpdateBody, err := json.Marshal(updateMessage) + if err != nil { + glog.Errorf("Error in marshalling: %s", err) + } + deviceTwinUpdate := DeviceETPrefix + deviceID + TwinETUpdateSuffix + TokenClient = Client.Publish(deviceTwinUpdate, 0, false, twinUpdateBody) + if TokenClient.Wait() && TokenClient.Error() != nil { + glog.Errorf("client.publish() Error in device twin update is %s", TokenClient.Error()) + } +} + +//SyncToCloud function syncs the updated device twin information to the cloud +func SyncToCloud(updateMessage DeviceTwinUpdate, deviceID string) { + deviceTwinResultUpdate := DeviceETPrefix + deviceID + TwinETCloudSyncSuffix + twinUpdateBody, err := json.Marshal(updateMessage) + if err != nil { + glog.Errorf("Error in marshalling: %s", err) + } + TokenClient = Client.Publish(deviceTwinResultUpdate, 0, false, twinUpdateBody) + if TokenClient.Wait() && TokenClient.Error() != nil { + glog.Errorf("client.publish() Error in device twin update is: %s", TokenClient.Error()) + } +} + +//GetTwin function is used to get the device twin details from the edge +func GetTwin(updateMessage DeviceTwinUpdate, deviceID string) { + getTwin := DeviceETPrefix + deviceID + TwinETGetSuffix + twinUpdateBody, err := json.Marshal(updateMessage) + if err != nil { + glog.Errorf("Error in marshalling: %s", err) + } + TokenClient = Client.Publish(getTwin, 0, false, twinUpdateBody) + if TokenClient.Wait() && TokenClient.Error() != nil { + glog.Errorf("client.publish() Error in device twin get is: %s ", TokenClient.Error()) + } +} + +//subscribe function subscribes the device twin information through the MQTT broker +func TwinSubscribe(deviceID string) { + getTwinResult := DeviceETPrefix + deviceID + TwinETGetResultSuffix + TokenClient = Client.Subscribe(getTwinResult, 0, OnTwinMessageReceived) + if TokenClient.Wait() && TokenClient.Error() != nil { + glog.Errorf("subscribe() Error in device twin result get is: %s", TokenClient.Error()) + } + for { + time.Sleep(1 * time.Second) + if TwinResult.Twin != nil { + for k := range TwinResult.Twin { + TwinAttributes = append(TwinAttributes, k) + } + Wg.Done() + break + } + } +} + +// OnTwinMessageReceived callback function which is called when message is received +func OnTwinMessageReceived(client MQTT.Client, message MQTT.Message) { + err := json.Unmarshal(message.Payload(), &TwinResult) + if err != nil { + glog.Errorf("Error in unmarshalling: %s", err) + } +} + +//CreateActualUpdateMessage function is used to create the device twin update message +func CreateActualUpdateMessage(updatedTwinAttributes map[string]string) DeviceTwinUpdate { + var deviceTwinUpdateMessage DeviceTwinUpdate + deviceTwinUpdateMessage.Twin = map[string]*MsgTwin{} + for _, twinAttribute := range TwinAttributes { + if actualValue, ok := updatedTwinAttributes[twinAttribute]; ok { + deviceTwinUpdateMessage.Twin[twinAttribute] = &MsgTwin{} + deviceTwinUpdateMessage.Twin[twinAttribute].Actual = &TwinValue{Value: &actualValue} + deviceTwinUpdateMessage.Twin[twinAttribute].Metadata = &TypeMetadata{Type: "Updated"} + } + } + return deviceTwinUpdateMessage +} diff --git a/mappers/bluetooth_mapper/main.go b/mappers/bluetooth_mapper/main.go new file mode 100644 index 000000000..1c433c61f --- /dev/null +++ b/mappers/bluetooth_mapper/main.go @@ -0,0 +1,61 @@ +/* +Copyright 2019 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 ( + "flag" + "os" + + "github.com/golang/glog" + + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/configuration" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/controller" +) + +//usage is responsible for setting up the default settings of all defined command-line flags for glog. +func usage() { + flag.PrintDefaults() + os.Exit(2) +} + +//init for getting command line arguments for glog +func init() { + flag.Usage = usage + // NOTE: This next line is key you have to call flag.Parse() for the command line + // options or "flags" that are defined in the glog module to be picked up. + flag.Parse() +} + +// main function +func main() { + BleConfig := configuration.BLEConfig{} + // load config + err := BleConfig.Load() + if err != nil { + glog.Errorf("Error in loading configuration: %s", err) + os.Exit(1) + } + bleController := controller.Config{ + Watcher: BleConfig.Watcher, + ActionManager: BleConfig.ActionManager, + Scheduler: BleConfig.Scheduler, + Converter: BleConfig.Converter, + Device: BleConfig.Device, + Mqtt: BleConfig.Mqtt, + } + bleController.Start() +} diff --git a/mappers/bluetooth_mapper/scheduler/scheduler.go b/mappers/bluetooth_mapper/scheduler/scheduler.go new file mode 100644 index 000000000..047aff4b8 --- /dev/null +++ b/mappers/bluetooth_mapper/scheduler/scheduler.go @@ -0,0 +1,117 @@ +/* +Copyright 2019 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 scheduler + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/golang/glog" + + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/action_manager" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/data_converter" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/helper" +) + +const ( + MapperTopicPrefix = "$ke/device/bluetooth-mapper/" + SchedulerResultSuffix = "/scheduler/result" + defaultEventFrequency = 5000 +) + +// Schedule is structure to define a schedule +type Schedule struct { + // Name is name of the schedule. It should be unique so that a stop-chan + // can be made corresponding to name to stop the schedule. + Name string `yaml:"name" json:"name"` + // Interval is the time in milliseconds after which this action are to be performed + Interval int `yaml:"interval" json:"interval"` + //OccurrenceLimit refers to the number of time the action can occur, if it is 0, then the event will execute infinitely + OccurrenceLimit int `yaml:"occurrence-limit" json:"occurrence-limit"` + // Actions is list of Actions to be performed in this schedule + Actions []string `yaml:"actions"` +} + +//Scheduler structure contains the list of schedules to be scheduled +type Scheduler struct { + Schedules []Schedule `yaml:"schedules" json:"schedules"` +} + +//ScheduleResult structure contains the format in which telemetry data will be published on the MQTT topic +type ScheduleResult struct { + EventName string + TimeStamp int64 + EventResult string +} + +// ExecuteSchedule is responsible for scheduling the operations +func (schedule *Schedule) ExecuteSchedule(actionManager []actionmanager.Action, dataConverter dataconverter.DataRead, deviceID string) { + glog.Infof("Executing schedule: %s", schedule.Name) + if schedule.OccurrenceLimit != 0 { + for iteration := 0; iteration < schedule.OccurrenceLimit; iteration++ { + schedule.performScheduleOperation(actionManager, dataConverter, deviceID) + } + } else { + for { + schedule.performScheduleOperation(actionManager, dataConverter, deviceID) + } + } + helper.ControllerWg.Done() +} + +// performScheduleOperation is responsible for performing the operations associated with the schedule +func (schedule *Schedule) performScheduleOperation(actionManager []actionmanager.Action, dataConverter dataconverter.DataRead, deviceID string) { + var scheduleResult ScheduleResult + actionExists := false + for _, actionName := range schedule.Actions { + for _, action := range actionManager { + if strings.ToUpper(action.Name) == strings.ToUpper(actionName) { + actionExists = true + glog.Infof("Performing scheduled operation: %s", action.Name) + action.PerformOperation(dataConverter) + scheduleResult.EventName = actionName + scheduleResult.TimeStamp = time.Now().UnixNano() / 1e6 + scheduleResult.EventResult = fmt.Sprintf("%s", action.Operation.Value) + publishScheduleResult(scheduleResult, deviceID) + } + } + if schedule.Interval == 0 { + schedule.Interval = defaultEventFrequency + } + if !actionExists { + glog.Errorf("Action %s does not exist. Exiting from schedule !!!", actionName) + break + } + time.Sleep(time.Duration(time.Duration(schedule.Interval) * time.Millisecond)) + } +} + +//publishScheduleResult publishes the telemetry data on the given MQTT topic +func publishScheduleResult(scheduleResult ScheduleResult, deviceID string) { + scheduleResultTopic := MapperTopicPrefix + deviceID + SchedulerResultSuffix + glog.Infof("Publishing schedule: %s result on topic: %s", scheduleResult.EventName, scheduleResultTopic) + scheduleResultBody, err := json.Marshal(scheduleResult) + if err != nil { + glog.Errorf("Error: %s", err) + } + helper.TokenClient = helper.Client.Publish(scheduleResultTopic, 0, false, scheduleResultBody) + if helper.TokenClient.Wait() && helper.TokenClient.Error() != nil { + glog.Errorf("client.publish() Error in device twin get is %s", helper.TokenClient.Error()) + } +} diff --git a/mappers/bluetooth_mapper/watcher/watcher.go b/mappers/bluetooth_mapper/watcher/watcher.go new file mode 100644 index 000000000..9218b8ac5 --- /dev/null +++ b/mappers/bluetooth_mapper/watcher/watcher.go @@ -0,0 +1,190 @@ +/* +Copyright 2019 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 watcher + +import ( + "errors" + "os" + "reflect" + "strings" + "time" + + "github.com/golang/glog" + "github.com/paypal/gatt" + + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/action_manager" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/data_converter" + "github.com/kubeedge/kubeedge/mappers/bluetooth_mapper/helper" +) + +var DeviceConnected = make(chan bool) +var done = make(chan struct{}) +var deviceName string +var deviceID string +var actionManager []actionmanager.Action +var dataConverter dataconverter.Converter + +//Watch structure contains the watcher specific configurations +type Watcher struct { + DeviceTwinAttributes []Attribute `yaml:"device-twin-attributes" json:"device-twin-attributes"` +} + +//Attribute structure contains the name of the attribute along with the actions to be performed for this attribute +type Attribute struct { + Name string `yaml:"device-property-name" json:"device-property-name"` + Actions []string `yaml:"actions" json:"actions"` +} + +//Initiate initiates the watcher module +func (w *Watcher) Initiate(device gatt.Device, nameOfDevice, idOfDevice string, actions []actionmanager.Action, converter dataconverter.Converter) { + deviceID = idOfDevice + deviceName = nameOfDevice + actionManager = actions + dataConverter = converter + // Register optional handlers. + device.Handle( + gatt.PeripheralConnected(w.onPeripheralConnected), + gatt.PeripheralDisconnected(onPeripheralDisconnected), + gatt.PeripheralDiscovered(onPeripheralDiscovered), + ) + device.Init(onStateChanged) + <-done + glog.Infof("Watcher Done") +} + +//onStateChanged contains the operations to be performed when the state of the peripheral device changes +func onStateChanged(device gatt.Device, s gatt.State) { + switch s { + case gatt.StatePoweredOn: + glog.Infof("Scanning for BLE device Broadcasts...") + device.Scan([]gatt.UUID{}, true) + return + default: + device.StopScanning() + } +} + +//onPeripheralDiscovered contains the operations to be performed as soon as the peripheral device is discovered +func onPeripheralDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) { + if strings.ToUpper(a.LocalName) == strings.ToUpper(strings.Replace(deviceName, "-", " ", -1)) { + glog.Infof("Device: %s found !!!! Stop Scanning for devices", deviceName) + // Stop scanning once we've got the peripheral we're looking for. + p.Device().StopScanning() + glog.Infof("Connecting to %s", deviceName) + p.Device().Connect(p) + } +} + +//onPeripheralDisconnected contains the operations to be performed as soon as the peripheral device is disconnected +func onPeripheralDisconnected(p gatt.Peripheral, err error) { + glog.Infof("Disconnecting from bluetooth device....") + DeviceConnected <- false + close(done) + p.Device().CancelConnection(p) +} + +//onPeripheralConnected contains the operations to be performed as soon as the peripheral device is connected +func (w *Watcher) onPeripheralConnected(p gatt.Peripheral, err error) { + actionmanager.GattPeripheral = p + ss, err := p.DiscoverServices(nil) + if err != nil { + glog.Errorf("Failed to discover services, err: %s\n", err) + os.Exit(1) + } + for _, s := range ss { + // Discovery characteristics + cs, err := p.DiscoverCharacteristics(nil, s) + if err != nil { + glog.Errorf("Failed to discover characteristics for service %s, err: %v\n", s.Name(), err) + continue + } + actionmanager.CharacteristicsList = append(actionmanager.CharacteristicsList, cs...) + } + DeviceConnected <- true + for { + newWatcher := &Watcher{} + if !reflect.DeepEqual(w, newWatcher) { + err := w.EquateTwinValue(deviceID) + if err != nil { + glog.Errorf("Error in watcher functionality: %s", err) + } + } + } +} + +//EquateTwinValue is responsible for equating the actual state of the device to the expected state that has been set and syncing back the result to the cloud +func (w *Watcher) EquateTwinValue(deviceID string) error { + var updateMessage helper.DeviceTwinUpdate + updatedActualValues := make(map[string]string) + helper.Wg.Add(1) + glog.Infof("Watching on the device twin values for device with deviceID: %s", deviceID) + go helper.TwinSubscribe(deviceID) + helper.GetTwin(updateMessage, deviceID) + helper.Wg.Wait() + twinUpdated := false + for _, twinAttribute := range w.DeviceTwinAttributes { + if helper.TwinResult.Twin[twinAttribute.Name] != nil { + if helper.TwinResult.Twin[twinAttribute.Name].Expected != nil && ((helper.TwinResult.Twin[twinAttribute.Name].Actual == nil) && helper.TwinResult.Twin[twinAttribute.Name].Expected != nil || (*helper.TwinResult.Twin[twinAttribute.Name].Expected.Value != *helper.TwinResult.Twin[twinAttribute.Name].Actual.Value)) { + glog.Infof("%s Expected Value : %s", twinAttribute.Name, *helper.TwinResult.Twin[twinAttribute.Name].Expected.Value) + if helper.TwinResult.Twin[twinAttribute.Name].Actual == nil { + glog.Infof("%s Actual Value: %v", twinAttribute.Name, helper.TwinResult.Twin[twinAttribute.Name].Actual) + } else { + glog.Infof("%s Actual Value: %s", twinAttribute.Name, *helper.TwinResult.Twin[twinAttribute.Name].Actual.Value) + } + glog.Infof("Equating the actual value to expected value for: %s", twinAttribute.Name) + for _, watcherAction := range twinAttribute.Actions { + actionExists := false + for _, action := range actionManager { + if strings.ToUpper(action.Name) == strings.ToUpper(watcherAction) { + actionExists = true + for _, converterAttribute := range dataConverter.DataWrite.Attributes { + if strings.ToUpper(converterAttribute.Name) == strings.ToUpper(twinAttribute.Name) { + for operationName, dataMap := range converterAttribute.Operations { + if action.Name == operationName { + expectedValue := helper.TwinResult.Twin[twinAttribute.Name].Expected.Value + if _, ok := dataMap.DataMapping[*expectedValue]; ok { + action.Operation.Value = dataMap.DataMapping[*expectedValue] + } + } + action.PerformOperation() + } + } + } + } + } + if !actionExists { + return errors.New("The action: " + watcherAction + " does not exist for this device") + } + } + updatedActualValues[twinAttribute.Name] = *helper.TwinResult.Twin[twinAttribute.Name].Expected.Value + twinUpdated = true + } + } else { + return errors.New("The attribute: " + twinAttribute.Name + " does not exist for this device") + } + } + if twinUpdated { + updateMessage = helper.CreateActualUpdateMessage(updatedActualValues) + helper.ChangeTwinValue(updateMessage, deviceID) + time.Sleep(2 * time.Second) + glog.Infof("Syncing to cloud.....") + helper.SyncToCloud(updateMessage, deviceID) + } else { + glog.Infof("Actual values are in sync with Expected value") + } + return nil +} diff --git a/mappers/modbus_mapper/Dockerfile b/mappers/modbus_mapper/Dockerfile new file mode 100644 index 000000000..d62599bc0 --- /dev/null +++ b/mappers/modbus_mapper/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine:latest + +COPY src/ /opt/src +COPY conf/ /opt/src/conf +COPY scripts/ /opt/scripts + +RUN chmod +x /opt/scripts/start_modbusmapper.sh +RUN apk add --update nodejs + +CMD sh /opt/scripts/start_modbusmapper.sh diff --git a/mappers/modbus_mapper/Makefile b/mappers/modbus_mapper/Makefile new file mode 100644 index 000000000..9be3cb8d8 --- /dev/null +++ b/mappers/modbus_mapper/Makefile @@ -0,0 +1,5 @@ +#make modbus_mapper +.PHONY: default modbus_mapper +modbus_mapper: + cd src && npm install --unsafe-perm=true + docker build -t modbus_mapper:v1.0 . diff --git a/mappers/modbus_mapper/README.md b/mappers/modbus_mapper/README.md new file mode 100644 index 000000000..e9af90c96 --- /dev/null +++ b/mappers/modbus_mapper/README.md @@ -0,0 +1,4 @@ + +# Modbus Mapper + +Click [here](https://github.com/kubeedge/kubeedge/blob/master/docs/mappers/modbus_mapper.md#modbus-mapper) for detailed documentation of the modbus mapper.
\ No newline at end of file diff --git a/mappers/modbus_mapper/conf/conf.json b/mappers/modbus_mapper/conf/conf.json new file mode 100644 index 000000000..5d80341b8 --- /dev/null +++ b/mappers/modbus_mapper/conf/conf.json @@ -0,0 +1,6 @@ +{ + "mqtt_port": {CONNECTOR_MQTT_PORT}, + "mqtt_ip": "{CONNECTOR_MQTT_IP}", + "dpl_name": "{CONNECTOR_DPL_NAME}", + "log_level": "info" +} diff --git a/mappers/modbus_mapper/deployment.yaml b/mappers/modbus_mapper/deployment.yaml new file mode 100644 index 000000000..30a536796 --- /dev/null +++ b/mappers/modbus_mapper/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: modbus-device-mapper-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: modbus-mapper + template: + metadata: + labels: + app: modbus-mapper + spec: + hostNetwork: true + containers: + - name: modbus-mapper-container + image: <your_dockerhub_username>/modbus_mapper:v1.0 + env: + - name: CONNECTOR_MQTT_PORT + value: "1883" + - name: CONNECTOR_MQTT_IP + value: 127.0.0.1 + - name: CONNECTOR_DPL_NAME + value: dpl/deviceProfile.json + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + volumeMounts: + - name: dpl-config-volume + mountPath: /opt/src/dpl + nodeSelector: + modbus: "true" + volumes: + - name: dpl-config-volume + configMap: + name: device-profile-config-<edge_node_name> + restartPolicy: Always +
\ No newline at end of file diff --git a/mappers/modbus_mapper/dpl/deviceProfile.json b/mappers/modbus_mapper/dpl/deviceProfile.json new file mode 100644 index 000000000..00082d319 --- /dev/null +++ b/mappers/modbus_mapper/dpl/deviceProfile.json @@ -0,0 +1,42 @@ +{ + "deviceInstances": [{ + "id": "f81e1dce-8152-4f76-b673-d6728b46e395", + "name": "modbus-mock-instance-01", + "model": "modbus-mock-model", + "protocol": "modbus-tcp-01" + }], + "deviceModels": [{ + "properties": [{ + "name": "temperature", + "minimum": 0, + "maximum": 100, + "datatype": "int", + "accessMode": "rw" + }], + "name": "modbus-mock-model", + "description": null + }], + "propertyVisitors": [{ + "visitorConfig": { + "index": 0, + "scale": 1.0, + "isRegisterSwap": true, + "register": "HoldingRegister", + "offset": 1, + "isSwap": false + }, + "propertyName": "temperature", + "modelName": "modbus-mock-model", + "protocol": "modbus-tcp", + "name": "temperature" + }], + "protocols": [{ + "protocolConfig": { + "ip": "127.0.0.1", + "port": 5028, + "slaveID": 1 + }, + "protocol": "modbus-tcp", + "name": "modbus-tcp-01" + }] +} diff --git a/mappers/modbus_mapper/sample/sample_server.js b/mappers/modbus_mapper/sample/sample_server.js new file mode 100644 index 000000000..79341ab76 --- /dev/null +++ b/mappers/modbus_mapper/sample/sample_server.js @@ -0,0 +1,33 @@ +// create an empty modbus client +const ModbusRTU = require("modbus-serial"); +var holdingValue = 5 +var coilValue = false +var vector = { + getHoldingRegister: function() { + return holdingValue; + }, + getCoil: function() { + return coilValue; + }, + setRegister: function(addr, value, unitID) { + // Asynchronous handling supported also here + console.log("set register", addr, value, unitID); + holdingValue = value; + return; + }, + setCoil: function(addr, value, unitID) { + // Asynchronous handling supported also here + console.log("set coil", addr, value, unitID); + coilValue = Boolean(value); + return; + }, +}; + +// set the server to answer for modbus requests +console.log("ModbusTCP listening on modbus://127.0.0.1:5028"); +var serverTCP = new ModbusRTU.ServerTCP(vector, { host: "0.0.0.0", port: 5028, debug: true, unitID: 1 }); + +serverTCP.on("socketError", function(err){ + // Handle socket error if needed, can be ignored + console.error(err); +});
\ No newline at end of file diff --git a/mappers/modbus_mapper/scripts/start_modbusmapper.sh b/mappers/modbus_mapper/scripts/start_modbusmapper.sh new file mode 100644 index 000000000..80c0dd1b1 --- /dev/null +++ b/mappers/modbus_mapper/scripts/start_modbusmapper.sh @@ -0,0 +1,14 @@ +#!/bin/sh +CONFIG_DIR=/opt/src/conf + +for VAR in $(env) +do + if [[ ! -z "$(echo $VAR | grep -E '^CONNECTOR_')" ]]; then + VAR_NAME=$(echo "$VAR" | sed -r "s/([^=]*)=.*/\1/g") + echo "$VAR_NAME=$(eval echo \$$VAR_NAME)" + sed -i "s#{$VAR_NAME}#$(eval echo \$$VAR_NAME)#g" $CONFIG_DIR/conf.json + fi +done + +cd /opt/src +node index.js diff --git a/mappers/modbus_mapper/src/common.js b/mappers/modbus_mapper/src/common.js new file mode 100644 index 000000000..400848d62 --- /dev/null +++ b/mappers/modbus_mapper/src/common.js @@ -0,0 +1,93 @@ +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const mkdirp = require('mkdirp'); + +// bitArrayToInt change bit array to Int +function bitArrayToInt(bitArr, callback) { + let bitStr = ''; + if (bitArr.length > 0 && bitArr.length < 64) { + for (let i = 0; i < bitArr.length; i++){ + bitStr = bitStr + bitArr[i].toString(); + } + num = parseInt(bitStr,2); + callback(num); + } +} + +// byteArrayToInt change one byte array to Int +function byteArrayToInt(byteArr, callback) { + let bitArr = ''; + if (byteArr.length > 0 && byteArr.length < 5){ + for (let i = 0; i < byteArr.length; i++){ + bitArr = bitArr + (byteArr[i]).toString(2).padStart(8, '0'); + } + callback(parseInt(bitArr, 2)); + } +} + +// twoByteArrayToInt change two byte array to Int +function twoByteArrayToInt(byteArr, callback) { + let bitArr = ''; + if (byteArr.length > 0 && byteArr.length < 5){ + for (let i = 0; i < byteArr.length; i++){ + bitArr = bitArr + (byteArr[i]).toString(2).padStart(16, '0'); + } + callback(parseInt(bitArr, 2)); + } +} + +// IntToByteArray change Int to byte array +function IntToByteArray(value, callback) { + if ((value).toString(2).length > 32){ + let cs1 = (value).toString(2).slice(0,(value).toString(2).length-32); + let cs2 = (value).toString(2).slice((value).toString(2).length-32); + Int32ToByte(parseInt(cs1, 2), (arr1)=>{ + Int32ToByte(parseInt(cs2, 2), (arr2)=>{ + arr1 = arr1.concat(arr2); + callback(arr1); + }); + }); + } else { + Int32ToByte(value, (arr)=>{ + callback(arr); + }); + } +} + +// Int32ToByte change Int32 num to byte array +function Int32ToByte(value, callback) { + let byteArr = []; + for (let i = 16; i >= 0; i = i - 16) { + if((value >> i & 0xffff) != 0) { + byteArr.push(value >> i & 0xffff); + } + } + callback(byteArr); +} + +// switchRegister reverse the order of array +function switchRegister(data, callback) { + let switchData = []; + for (let i = 0; i < data.length/2; i++) { + switchData[i] = data[data.length-i-1]; + switchData[data.length-i-1] = data[i]; + } + callback(switchData) +} + +// switchByte exchange lower and higher byte value of two byte data +function switchByte(data, callback){ + let switchData = []; + let InternalData = []; + for (let i = 0; i < data.length; i++){ + InternalData[0] = data[i] & 0xff; + InternalData[1] = data[i] >> 8 & 0xff; + byteArrayToInt(InternalData, (bitarr)=>{ + switchData[i] = bitarr; + }); + } + callback(switchData) +} + +module.exports = {bitArrayToInt, byteArrayToInt, IntToByteArray, switchRegister, switchByte, twoByteArrayToInt}; diff --git a/mappers/modbus_mapper/src/constant.js b/mappers/modbus_mapper/src/constant.js new file mode 100644 index 000000000..39193fe85 --- /dev/null +++ b/mappers/modbus_mapper/src/constant.js @@ -0,0 +1,17 @@ +const defaultTopicPrefix = '$hw/events/device/'; +const defaultDirectTopicPrefix = '$hw/devices/'; +const twinDeltaTopic = defaultTopicPrefix + '+/twin/update/delta'; +const twinUpdateTopic = '/twin/update'; +const twinGetResTopic = defaultTopicPrefix + '+/twin/get/result'; +const twinGetTopic = '/twin/get'; +const directGetTopic = '/events/properties/get'; + +module.exports = { + twinDeltaTopic, + twinUpdateTopic, + defaultTopicPrefix, + defaultDirectTopicPrefix, + directGetTopic, + twinGetTopic, + twinGetResTopic +}; diff --git a/mappers/modbus_mapper/src/devicetwin.js b/mappers/modbus_mapper/src/devicetwin.js new file mode 100644 index 000000000..6f15d5425 --- /dev/null +++ b/mappers/modbus_mapper/src/devicetwin.js @@ -0,0 +1,230 @@ +const constant = require('./constant'); +const common = require('./common'); +const Buffer = require('buffer').Buffer; +const uuidv4 = require('uuid/v4'); +const util = require('util'); +const async = require('async'); + +class DeviceTwin { + constructor(mqttClient) { + this.mqttClient = mqttClient; + } + + // transferType transfer data according to the dpl configuration + transferType(visitor, property, data, callback) { + let transData; + async.waterfall([ + function(callback) { + if (visitor.visitorConfig.isRegisterSwap) { + common.switchRegister(data, (switchedData)=>{ + callback(null, switchedData); + }); + } else { + callback(null, data); + } + }, + function(internalData, callback) { + if (visitor.visitorConfig.isSwap && (visitor.visitorConfig.register === 'HoldingRegister' || visitor.visitorConfig.register === 'InputRegister')) { + common.switchByte(internalData, (switchedData)=>{ + callback(null, switchedData); + }); + } else { + callback(null, internalData); + } + } + ], function(err, transedData) { + transData = transedData; + }); + this.transferDataType(visitor, property, transData, callback); + } + + // transferDataType transfer data according to the dpl configuration + transferDataType(visitor, property, data, callback) { + let transData; + switch(property.dataType) { + case 'int': + case 'float': + if (visitor.visitorConfig.register === 'DiscreteInputRegister' || visitor.visitorConfig.register === 'CoilRegister') { + common.bitArrayToInt(data, (num)=>{ + transData = num; + }); + } else if (visitor.visitorConfig.register === 'HoldingRegister' || visitor.visitorConfig.register === 'InputRegister') { + common.twoByteArrayToInt(data, (num)=>{ + transData = num; + }) + } + + if (visitor.visitorConfig.scale !=0 && transData != null) { + transData = transData * visitor.visitorConfig.scale; + } + + if (property.dataType === 'int') { + transData = parseInt(transData); + } + + if (property.maximum !== null && transData > parseFloat(property.maximum)) { + logger.info("read data is larger than max value, use max value") + transData = parseInt(property.maximum); + } else if (property.minimum !== null && transData < parseFloat(property.minimum)) { + logger.info("read data is smaller than min value, use min value") + transData = parseInt(property.minimum); + } + + callback(transData); + break; + case 'string': + let buf = new Buffer.from(data); + transData = buf.toString('utf8') + callback(transData); + break; + case 'boolean': + if (data[0] == 0 || data[0] == 1){ + transData = Boolean(data[0]); + } else { + transData = null; + } + callback(transData); + break; + default: + logger.error('unknown dataType: ', property.dataType); + callback(null); + break; + } + } + + // updateActual update actual value to edge mqtt + updateActual(deviceID, property, value) { + let reply_msg = { + event_id: "", + timestamp: new Date().getTime() + }; + let twin = {}; + twin[property.name] = { + actual: { + value: String(value), + metadata: { + timestamp: new Date().getTime() + } + }, + metadata: { + tyep: property.dataType + } + }; + reply_msg.twin = twin; + this.mqttClient.publish(constant.defaultTopicPrefix + deviceID + constant.twinUpdateTopic, JSON.stringify(reply_msg)); + } + + // dealUpdate set latest actual value of devicetwin into actualVal map + dealUpdate(transData, property, deviceID, actualVals) { + if (!actualVals.has(util.format("%s-%s", deviceID, property.name))) { + this.updateActual(deviceID, property, transData); + actualVals.set(util.format("%s-%s", deviceID, property.name), String(transData)); + logger.info("update devicetwin[%s] of device[%s] successfully", property.name, deviceID); + } else { + this.compareActuals(transData, actualVals.get(util.format("%s-%s", deviceID, property.name)), (changed)=>{ + if (changed) { + this.updateActual(deviceID, property, transData); + actualVals.set(util.format("%s-%s", deviceID, property.name), String(transData)); + logger.info("update devicetwin[%s] of device[%s] successfully", property.name, deviceID); + } + }); + } + } + + // getActuals publish get devicetwin msg to edge mqtt + getActuals(deviceID) { + let payload_msg = { + event_id: "", + timestamp: new Date().getTime() + }; + this.mqttClient.publish(constant.defaultTopicPrefix + deviceID + constant.twinGetTopic, JSON.stringify(payload_msg)); + } + + // setActuals set device property and actual value map + setActuals(getMsg, callback) { + let deviceTwin = getMsg.twin; + let PropActuals = new Map(); + Object.keys(deviceTwin).forEach(function(key){ + if (deviceTwin[key].hasOwnProperty('actual')) { + PropActuals.set(key, deviceTwin[key].actual.value); + } + }) + callback(PropActuals); + } + + // setExpecteds set device property and expected value map + setExpecteds(getMsg, callback) { + let deviceTwin = getMsg.twin; + let ProExpect = new Map(); + Object.keys(deviceTwin).forEach(function(key){ + if (deviceTwin[key].hasOwnProperty('expected') && !deviceTwin[key].hasOwnProperty('actual') || JSON.stringify(deviceTwin[key].actual) == '{}') { + ProExpect.set(key, deviceTwin[key].expected.value); + } + }) + callback(ProExpect); + } + + // compareActuals compare if data is changed + compareActuals(data, cachedActuals, callback) { + let changed = false; + if (data != cachedActuals) { + changed = true; + } + callback(changed); + } + + // UpdateDirectActuals update all devicetwin property to edge mqtt + UpdateDirectActuals(devIns, deviceID, actualVals) { + if (devIns.has(deviceID)) { + let deviceName = devIns.get(deviceID).name; + this.generateDirectGetMsg(deviceName, deviceID, actualVals, (directGetMsg)=>{ + this.mqttClient.publish(constant.defaultDirectTopicPrefix + deviceID + constant.directGetTopic, JSON.stringify(directGetMsg)); + }); + } + } + + // generateDirectGetMsg generate Direct Get Msg in message format + generateDirectGetMsg(deviceName, deviceID, actualVals, callback) { + let header = { + msg_id: uuidv4(), + parent_msg_id: "", + timestamp: new Date().getTime(), + sync: false + }; + let route = { + source: "eventbus", + group: "", + operation: "upload", + resource: util.format("%s%s%s", constant.defaultDirectTopicPrefix, deviceID, constant.directGetTopic) + }; + let content = { + data: actualVals, + device_name: deviceName, + device_id: deviceID, + timestamp: new Date().getTime() + }; + let directGetMsg = { + header: header, + route: route, + content: content + }; + callback(directGetMsg); + } + + // syncExpected check whether expected value should be update to device + static syncExpected(delta, key, callback) { + let deviceTwin = delta.twin[key]; + if (!delta.twin.hasOwnProperty(key)) { + logger.error("Invalid device twin ", key); + return; + } + if (!deviceTwin.hasOwnProperty('actual') || + (deviceTwin.hasOwnProperty('expected') && deviceTwin.expected.hasOwnProperty('metadata') && deviceTwin.actual.hasOwnProperty('metadata') && + deviceTwin.expected.metadata.timestamp > deviceTwin.actual.metadata.timestamp && + deviceTwin.expected.value !== deviceTwin.actual.value)) { + callback(deviceTwin.expected.value); + } + } +} + +module.exports = DeviceTwin; diff --git a/mappers/modbus_mapper/src/index.js b/mappers/modbus_mapper/src/index.js new file mode 100644 index 000000000..93945032f --- /dev/null +++ b/mappers/modbus_mapper/src/index.js @@ -0,0 +1,328 @@ +const async = require('async'); +const log4js = require('log4js'); +const mqtt = require('mqtt'); +const path = require('path'); +const util = require('util'); + +const common = require('./common'); +const constant = require('./constant'); +const DeviceTwin = require('./devicetwin'); +const Modbus = require('./modbus'); +const WatchFiles = require('./watchfile'); + +//default logger options +log4js.configure({ + appenders: { + out: { type: 'stdout' }, + }, + categories: { + default: { appenders: ['out'], level: 'info' } + } +}); +logger = log4js.getLogger('appenders'); + +let options = { + port: 1883, + host: '127.0.0.1', + dpl_name: 'dpl/deviceProfile.json' +}; + +let mqtt_client, mqtt_client2, msg, mqtt_options, dt, devIns, devMod, devPro, modVistr; +let ActualVal = new Map(); + +async.series([ + //load conf.json + function(callback) { + WatchFiles.loadConfig('conf/conf.json', (err, configs)=>{ + if (err) { + logger.error('failed to load config, err: ', err); + } else { + options = { + port: configs.mqtt_port, + host: configs.mqtt_ip, + dpl_name: configs.dpl_name + }; + callback(err); + } + }); + }, + + //load dpl first time + function(callback) { + WatchFiles.loadDpl(options.dpl_name, (devInsMap, devModMap, devProMap, modVistrMap)=>{ + devIns = devInsMap; + devMod = devModMap; + devPro = devProMap; + modVistr = modVistrMap; + callback(); + }); + }, + + //first get twinget build map + function(callback) { + mqtt_options = { + port: options.port, + host: options.host, + }; + mqtt_client = mqtt.connect(mqtt_options); + dt = new DeviceTwin(mqtt_client); + mqtt_client.on('connect', ()=>{ + logger.info('connetced to edge mqtt with topic twinGet'); + mqtt_client.subscribe(constant.twinGetResTopic); + for (let instance of devIns) { + dt.getActuals(instance[0]); + } + }); + callback(); + }, + + //deal with twin get msg and set expected value into device + function(callback) { + mqtt_client.on('message', (topic, message)=>{ + try { + var msgGet = JSON.parse(message.toString()); + } catch (err) { + logger.error('unmarshal error'); + return; + } + let resources = topic.toString().split('/'); + let deviceID = resources[3]; + let dt = new DeviceTwin(mqtt_client); + let devProtocol, devInstance; + if (devPro.has(deviceID) && devIns.has(deviceID)) { + devProtocol = devPro.get(deviceID); + devInstance = devIns.get(deviceID); + } else { + logger.error('match visitor failed'); + } + logger.info('recieve twinGet msg, set properties actual value map'); + if (resources.length === 7 && resources[5] === 'get' && msgGet != null && msgGet.code != 404 && typeof(devProtocol) != 'undefined' && typeof(devInstance) != 'undefined') { + dt.setActuals(msgGet, (PropActuals)=>{ + for (let actual of PropActuals) { + ActualVal.set(util.format('%s-%s', deviceID, actual[0]), actual[1]); + } + }); + dt.setExpecteds(msgGet, (PropExpecteds)=>{ + for (let expected of PropExpecteds) { + modbusProtocolTransfer(devProtocol.protocol, (transferedProtocol)=>{ + if (modVistr.has(util.format('%s-%s-%s', devInstance.model, expected[0], transferedProtocol))) { + let visitor = modVistr.get(util.format('%s-%s-%s', devInstance.model, expected[0], transferedProtocol)); + dealDeltaMsg(msgGet, expected[0], visitor, devProtocol, expected[1]); + } + }); + } + }); + } + }); + callback(); + }, + + // start mqtt sub delta topic + function(callback) { + mqtt_options = { + port: options.port, + host: options.host, + }; + mqtt_client2 = mqtt.connect(mqtt_options); + mqtt_client2.on('connect', ()=>{ + logger.info('connetced to edge mqtt with topic twinDelta'); + mqtt_client2.subscribe(constant.twinDeltaTopic); + }); + callback(); + }, + + // on receive msg of delta topic + function(callback) { + logger.info('start to wait for devicetwin update'); + mqtt_client2.on('message', (topic, message)=>{ + try { + msg = JSON.parse(message.toString()); + } catch (err) { + logger.error('unmarshal error'); + callback(err); + return; + } + + //match visitors + let resources = topic.toString().split('/'); + let deviceID = resources[3]; + let devProtocol, devInstance; + if (devPro.has(deviceID) && devIns.has(deviceID)) { + devProtocol = devPro.get(deviceID); + devInstance = devIns.get(deviceID); + } else { + logger.error('match visitor failed'); + } + + try { + if (resources.length === 7 && resources[6] === 'delta' && typeof(devProtocol) != 'undefined' && typeof(devInstance) != 'undefined') { + logger.info('recieved twinDelta msg'); + Object.keys(msg.delta).forEach(function(key){ + modbusProtocolTransfer(devProtocol.protocol, (transferedProtocol)=>{ + if (modVistr.has(util.format('%s-%s-%s', devInstance.model, key, transferedProtocol))) { + let visitor = modVistr.get(util.format('%s-%s-%s', devInstance.model, key, transferedProtocol)); + DeviceTwin.syncExpected(msg, key, (value)=>{ + dealDeltaMsg(msg, key, visitor, devProtocol, value); + }); + } + }); + }); + } + } catch (err) { + logger.error('failed to change devicetwin of device[%s], err: ', deviceID, err); + } + }); + } +],function(err) { + if (err) { + logger.error(err); + } else { + logger.info('changed devicetwin successfully'); + } +}); + +let mqtt_client3; +logger.info('start to watch dpl config'); +WatchFiles.watchChange(path.join(__dirname, 'dpl'), ()=>{ + async.series([ + function(callback) { + WatchFiles.loadDpl(options.dpl_name, (devInsMap, devModMap, devProMap, modVistrMap)=>{ + devIns = devInsMap; + devMod = devModMap; + devPro = devProMap; + modVistr = modVistrMap; + callback(); + }); + }, + + function(callback) { + mqtt_options = { + port: options.port, + host: options.host, + }; + mqtt_client3 = mqtt.connect(mqtt_options); + let dt = new DeviceTwin(mqtt_client3); + mqtt_client3.on('connect', ()=>{ + logger.info('connetced to edge mqtt with topic twinGet'); + mqtt_client.subscribe(constant.twinGetResTopic); + for (let instance of devIns) { // let change from var + dt.getActuals(instance[0]); + } + }); + callback(); + }, + + function(callback) { + mqtt_client3.on('message', (topic, message)=>{ + try { + var msgGet = JSON.parse(message.toString()); + } catch (err) { + logger.error('unmarshal error'); + return; + } + let resources = topic.toString().split('/'); + let deviceID = resources[3]; + let devProtocol, devInstance; + if (devPro.has(deviceID) && devIns.has(deviceID)) { + devProtocol = devPro.get(deviceID); + devInstance = devIns.get(deviceID); + } else { + logger.error('match visitor failed'); + } + + let dt = new DeviceTwin(mqtt_client3); + if (resources.length === 7 && resources[5] === 'get' && msgGet != null && msgGet.code != 404 && typeof(devProtocol) != 'undefined' && typeof(devInstance) != 'undefined') { + logger.info('received twinGet message'); + dt.setExpecteds(msgGet, (PropExpecteds)=>{ + for (let expected of PropExpecteds) { + dt.compareActuals(expected[1], ActualVal.get(util.format('%s-%s', deviceID, expected[0])),(changed)=>{ + modbusProtocolTransfer(devProtocol.protocol, (transferedProtocol)=>{ + if (changed && modVistr.has(util.format('%s-%s-%s', devInstance.model, expected[0], transferedProtocol))) { + let visitor = modVistr.get(util.format('%s-%s-%s', devInstance.model, expected[0], transferedProtocol)); + dealDeltaMsg(msgGet, expected[0], visitor, devProtocol, expected[1]); + } + }); + }); + } + }); + } + }); + callback(); + } + ],function(err) { + if (err) { + logger.error('failed to load changed dpl config, err: ', err); + } else { + logger.info('load changed dpl config successfully'); + } + }); +}); + +logger.info('start to check devicetwin state'); +setInterval(()=>{ + let dt = new DeviceTwin(mqtt_client); + logger.info('chechking devicetwin state'); + for (let instance of devIns) { + if (devPro.has(instance[0])) { + let protocol = devPro.get(instance[0]); + let actuals = new Map(); + syncDeviceTwin(dt, instance[0], protocol, actuals); + } + } +}, 2000); + +// syncDeviceTwin check each property of each device accroding to the dpl configuration +function syncDeviceTwin(dt, key, protocol, actuals) { + async.eachSeries(devMod.get(key).properties, (property, callback)=>{ + let visitor; + if (typeof(protocol) != 'undefined') { + modbusProtocolTransfer(protocol.protocol, (transferedProtocol)=>{ + if (devIns.has(key) && modVistr.has(util.format('%s-%s-%s', devIns.get(key).model, property.name, transferedProtocol))) { + visitor = modVistr.get(util.format('%s-%s-%s', devIns.get(key).model, property.name, transferedProtocol)); + } else { + logger.error('failed to match visitor'); + } + }); + } + if (typeof(protocol) != 'undefined' && typeof(visitor) != 'undefined') { + let modbus = new Modbus(protocol, visitor); + modbus.ModbusUpdate((err, data)=>{ + if (err) { + logger.error('failed to update devicetwin[%s] of device[%s], err: ', property.name, key, err); + } else { + dt.transferType(visitor, property, data, (transData)=>{ + if (transData != null) { + actuals.set(property.name, String(transData)); + dt.dealUpdate(transData, property, key, ActualVal); + } + }); + callback(); + } + }); + } + },()=>{ + dt.UpdateDirectActuals(devIns, key, actuals); + }); +} + +// dealDeltaMsg deal with the devicetwin delta msg +function dealDeltaMsg(msg, key, visitor, protocol, value) { + let modbus = new Modbus(protocol, visitor); + modbus.ModbusDelta(msg.twin[key].metadata.type, value, (err, data)=>{ + if (err) { + logger.error('failed to modify register, err: ', err) + } else { + logger.info('modify register %s successfully', JSON.stringify(data)); + } + }) +} + +function modbusProtocolTransfer(protocol, callback) { + let transferedProtocol; + if (protocol === 'modbus-rtu' || protocol === 'modbus-tcp') { + transferedProtocol = 'modbus'; + } else { + transferedProtocol = protocol; + } + callback(transferedProtocol) +} diff --git a/mappers/modbus_mapper/src/modbus.js b/mappers/modbus_mapper/src/modbus.js new file mode 100644 index 000000000..3167a0cb8 --- /dev/null +++ b/mappers/modbus_mapper/src/modbus.js @@ -0,0 +1,204 @@ +const ModbusRTU = require('modbus-serial'); +const Buffer = require('buffer').Buffer; +const async = require('async'); +const common = require('./common'); + +class Modbus { + constructor(protocol, visitor){ + this.protocol = protocol; + this.visitor = visitor; + this.client = new ModbusRTU(); + } + + // connect to device with modbus protocol + connect(callback) { + let protocol = this.protocol; + let client = this.client; + switch(protocol.protocol) { + case 'modbus-tcp': + client.connectTCP(protocol.protocol_config.ip, { port: parseInt(protocol.protocol_config.port) }, ()=>{ + client.setTimeout(500); + client.setID(parseInt(protocol.protocol_config.slaveID)); + callback(client); + }); + break; + case 'modbus-rtu': + async.series([ + function(callback) { + setTimeout(function () { + callback(null, null); + }, 100); + }, + function (callback) { + client.connectRTUBuffered(protocol.protocol_config.serialPort, { baudRate: parseInt(protocol.protocol_config.baudRate) }, ()=>{ + client.setTimeout(500); + client.setID(parseInt(protocol.protocol_config.slaveID)); + callback(null, client); + }); + }], (err, res) => {callback(res[1]); + }); + break; + default: + logger.info('unknwon modbus_type ', protocol.protocol); + break; + } + } + + // WriteAction write the value into device registers + WriteAction(value, callback) { + let visitor = this.visitor; + let client = this.client; + switch(visitor.visitorConfig.register){ + case 'CoilRegister': + client.writeCoils(parseInt(visitor.visitorConfig.index), value, (err, data)=>{ + client.close(); + callback(err, data); + }); + break; + case 'HoldingRegister': + client.writeRegisters(parseInt(visitor.visitorConfig.index), value, (err, data)=>{ + client.close(); + callback(err, data); + }); + break; + default: + client.close(); + logger.info('write action is not allowed on register type ', visitor.visitorConfig.register) + callback('unkown action', null); + break; + } + } + + // PreWriteAction transfer data before writing data to register + PreWriteAction(type, value, callback) { + let visitor = this.visitor; + let transData; + async.waterfall([ + function(callback) { + switch(type) { + case 'int': + case 'float': + value = parseInt(value); + if (visitor.visitorConfig.register === 'CoilRegister') { + transData = (value).toString(2).split('').map(function(s) { return parseInt(s); }); + } else if (visitor.visitorConfig.register === 'HoldingRegister') { + common.IntToByteArray(value, (byteArr)=>{ + if (byteArr.length < visitor.visitorConfig.offset) { + let zeroArr = new Array(visitor.visitorConfig.offset -byteArr.length).fill(0); + byteArr = zeroArr.concat(byteArr); + transData = byteArr; + } else { + transData = byteArr; + } + }); + } else { + transData = null; + } + callback(null, transData); + break; + case 'string': { + let buf = new Buffer.from(value); + transData = buf.toJSON().data; + callback(null, transData); + break; + } + case 'boolean': + if (value === 'true') { + transData = [1]; + } else if (value === 'false') { + transData = [0]; + } else { + transData = null; + } + callback(null, transData); + break; + default: + transData = null; + callback(null, transData); + break; + } + }, + function(transData, callback) { + if (visitor.visitorConfig.isRegisterSwap && transData != null) { + common.switchRegister(transData, (switchedData)=>{ + callback(null, switchedData); + }); + } else { + callback(null, transData); + } + }, + function(internalData, callback) { + if (visitor.visitorConfig.isSwap && internalData != null && (visitor.visitorConfig.register === 'HoldingRegisters' || visitor.visitorConfig.register === 'CoilsRegisters')) { + common.switchByte(internalData, (switchedData)=>{ + callback(null, switchedData); + }); + } else { + callback(null, internalData); + } + }], function(err, transData) { + callback(transData); + } + ); + } + + // ReadAction read register data from device + ReadAction(callback) { + let visitor = this.visitor; + let client = this.client; + switch (visitor.visitorConfig.register) { + case 'CoilRegister': + client.readCoils(parseInt(visitor.visitorConfig.index), parseInt(visitor.visitorConfig.offset), (err, data)=>{ + client.close(); + callback(err, err?data:[data.data[0]]); + }); + break; + case 'DiscreteInputRegister': + client.readDiscreteInputs(parseInt(visitor.visitorConfig.index), parseInt(visitor.visitorConfig.offset), (err, data)=>{ + client.close(); + callback(err, err?data:[data.data[0]]); + }); + break; + case 'HoldingRegister': + client.readHoldingRegisters(parseInt(visitor.visitorConfig.index), parseInt(visitor.visitorConfig.offset), (err, data)=>{ + client.close(); + callback(err, err?data:data.data); + }); + break; + case 'InputRegister': + client.readInputRegisters(parseInt(visitor.visitorConfig.index), parseInt(visitor.visitorConfig.offset), (err, data)=>{ + client.close(); + callback(err, err?data:data.data); + }); + break; + default: + client.close(); + logger.info('read action is not allowed on register type ', visitor.visitorConfig.register) + callback('unknown Registers type', null); + break; + } + } + + // ModbusDelta deal with the delta message to modify the register + ModbusDelta(type, value, callback) { + this.connect(()=>{ + this.PreWriteAction(type, value, (transData)=>{ + if (transData != null) { + this.WriteAction(transData, (err, data)=>{ + callback(err, data); + }); + } + }); + }); + } + + // ModbusUpdate deal with the update message to read the register + ModbusUpdate(callback) { + this.connect(()=>{ + this.ReadAction((err, data)=>{ + callback(err, data); + }); + }); + } +} + +module.exports = Modbus; diff --git a/mappers/modbus_mapper/src/package-lock.json b/mappers/modbus_mapper/src/package-lock.json new file mode 100644 index 000000000..1c2f1de22 --- /dev/null +++ b/mappers/modbus_mapper/src/package-lock.json @@ -0,0 +1,1281 @@ +{ + "name": "modbus_mapper", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@serialport/binding-abstract": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-2.0.5.tgz", + "integrity": "sha512-oRg0QRsXJFKHQbQjmo0regKLZ9JhjLmTqc47ocJgYM5UtU9Q1VFrVPh0B2lr2pfm/tr3aNvTLX1eiVAvXyZ/bg==", + "requires": { + "debug": "^4.1.1" + } + }, + "@serialport/binding-mock": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-2.0.5.tgz", + "integrity": "sha512-1kD1qI686pIIolGZ6TPjAtvy8c3XIUlE4OXRZf7ZHaZgGaOUHAUMLKZt4tNTxsfedRTFyiYyHoe5QAbx82R9pQ==", + "requires": { + "@serialport/binding-abstract": "^2.0.5", + "debug": "^4.1.1" + } + }, + "@serialport/bindings": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-2.0.8.tgz", + "integrity": "sha512-paKLa9JkoH5FAy2sATTdXLCiKpuKn0pN15/etcCqzX8vi25fnQgJ8Yx9Z6zdbcKe1No7s/9PuH9yfjDR61fbOQ==", + "requires": { + "@serialport/binding-abstract": "^2.0.5", + "@serialport/parser-readline": "^2.0.2", + "bindings": "^1.3.0", + "debug": "^4.1.1", + "nan": "^2.13.2", + "prebuild-install": "^5.2.1" + } + }, + "@serialport/parser-byte-length": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-2.0.2.tgz", + "integrity": "sha512-cUOprk1uRLucCJy6m+wAM4pwdBaB5D4ySi6juwRScP9DTjKUvGWYj5jzuqvftFBvYFmFza89aLj5K23xiiqj7Q==" + }, + "@serialport/parser-cctalk": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-2.0.2.tgz", + "integrity": "sha512-5LMysRv7De+TeeoKzi4+sgouD4tqZEAn1agAVevw+7ILM0m30i1zgZLtchgxtCH7OoQRAkENEVEPc0OwhghKgw==" + }, + "@serialport/parser-delimiter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-2.0.2.tgz", + "integrity": "sha512-zB02LahFfyZmJqak9l37vP/F1K+KCUxd1KQj35OhD1+0q/unMjVTZmsfkxFSM4gkaxP9j7+8USk+LQJ3V8U26Q==" + }, + "@serialport/parser-readline": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-2.0.2.tgz", + "integrity": "sha512-thL26dGEHB+eINNydJmzcLLhiqcBQkF+wNTbRaYblTP/6dm7JsfjYSud7bTkN63AgE0xpe9tKXBFqc8zgJ1VKg==", + "requires": { + "@serialport/parser-delimiter": "^2.0.2" + } + }, + "@serialport/parser-ready": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-2.0.2.tgz", + "integrity": "sha512-6ynQ+HIIkFQcEO2Hrq4Qmdz+hlJ7kjTHGQ1E7SRN7f70nnys1v3HSke8mjK3RzVw+SwL0rBYjftUdCTrU+7c+Q==" + }, + "@serialport/parser-regex": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-2.0.2.tgz", + "integrity": "sha512-7qjYd7AdHUK8fJOmHpXlMRipqRCVMMyDFyf/5TQQiOt6q+BiFjLOtSpVXhakHwgnXanzDYKeRSB8zM0pZZg+LA==" + }, + "@serialport/stream": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-2.0.5.tgz", + "integrity": "sha512-9gc3zPoAqs/04mvq8TdZ7GxtnacCDuw3/u0u18UXXHgC/5tNDYkY+hXFIJB1fQFnP5yyNB1L2XLfX974ySJg9Q==", + "requires": { + "@serialport/binding-mock": "^2.0.5", + "debug": "^4.1.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "anymatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz", + "integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "async": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz", + "integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ==" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "callback-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", + "integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "> 1.0.0 < 3.0.0" + } + }, + "chokidar": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz", + "integrity": "sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==", + "requires": { + "anymatch": "^3.0.1", + "braces": "^3.0.2", + "fsevents": "^2.0.6", + "glob-parent": "^5.0.0", + "is-binary-path": "^2.1.0", + "is-glob": "^4.0.1", + "normalize-path": "^3.0.0", + "readdirp": "^3.1.1" + } + }, + "chownr": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "requires": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "date-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz", + "integrity": "sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "es5-ext": { + "version": "0.10.50", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", + "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "^1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==" + }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz", + "integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==", + "optional": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "help-me": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", + "integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=", + "requires": { + "callback-stream": "^1.0.2", + "glob-stream": "^6.1.0", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" + }, + "log4js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", + "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", + "requires": { + "date-format": "^2.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.0", + "rfdc": "^1.1.4", + "streamroller": "^1.0.6" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "modbus-serial": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/modbus-serial/-/modbus-serial-7.6.0.tgz", + "integrity": "sha512-6gEEdl2p2oVpFvfbjdxmCexPgo/0G8wAlobqwQo+8Zz/T5nqmcP1RhX0xqQuNx2r8SjTFipYNVkIiiFXRmi0Ew==", + "requires": { + "debug": "^4.1.1", + "serialport": "^7.1.3" + } + }, + "mqtt": { + "version": "2.18.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.8.tgz", + "integrity": "sha512-3h6oHlPY/yWwtC2J3geraYRtVVoRM6wdI+uchF4nvSSafXPZnaKqF8xnX+S22SU/FcgEAgockVIlOaAX3fkMpA==", + "requires": { + "commist": "^1.0.0", + "concat-stream": "^1.6.2", + "end-of-stream": "^1.4.1", + "es6-map": "^0.1.5", + "help-me": "^1.0.1", + "inherits": "^2.0.3", + "minimist": "^1.2.0", + "mqtt-packet": "^5.6.0", + "pump": "^3.0.0", + "readable-stream": "^2.3.6", + "reinterval": "^1.1.0", + "split2": "^2.1.1", + "websocket-stream": "^5.1.2", + "xtend": "^4.0.1" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "mqtt-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-5.6.1.tgz", + "integrity": "sha512-eaF9rO2uFrIYEHomJxziuKTDkbWW5psLBaIGCazQSKqYsTaB3n4SpvJ1PexKaDBiPnMLPIFWBIiTYT3IfEJfww==", + "requires": { + "bl": "^1.2.1", + "inherits": "^2.0.3", + "process-nextick-args": "^2.0.0", + "safe-buffer": "^5.1.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "napi-build-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", + "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "node-abi": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.9.0.tgz", + "integrity": "sha512-jmEOvv0eanWjhX8dX1pmjb7oJl1U1oR4FOh0b2GnvALwSYoOdU7sj+kLDSAyjo4pfC9aj/IxkloxdLJQhSSQBA==", + "requires": { + "semver": "^5.4.1" + } + }, + "nodejs-base64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/nodejs-base64/-/nodejs-base64-1.0.3.tgz", + "integrity": "sha512-6wLDv7N/MfV4wDsrBC2s8xVfx82iywopGLdAVYYTLmwOHTm6N463Dwr9tP4+GHhfTMPN4h9i2Z0LzMdFB2G5cw==" + }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==" + }, + "prebuild-install": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz", + "integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "os-homedir": "^1.0.1", + "pump": "^2.0.1", + "rc": "^1.2.7", + "simple-get": "^2.7.0", + "tar-fs": "^1.13.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.1.tgz", + "integrity": "sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ==", + "requires": { + "picomatch": "^2.0.4" + } + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "serialport": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-7.1.5.tgz", + "integrity": "sha512-NplGdqaY+ZL8t3t5egbT+3oqLW4d7WvDT/x1ACxAyWa1fSnx+KTAmlDHeCls39lXwu8voaOr3bPOW4bwM7PdAA==", + "requires": { + "@serialport/binding-mock": "^2.0.5", + "@serialport/bindings": "^2.0.8", + "@serialport/parser-byte-length": "^2.0.2", + "@serialport/parser-cctalk": "^2.0.2", + "@serialport/parser-delimiter": "^2.0.2", + "@serialport/parser-readline": "^2.0.2", + "@serialport/parser-ready": "^2.0.2", + "@serialport/parser-regex": "^2.0.2", + "@serialport/stream": "^2.0.5", + "debug": "^4.1.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", + "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", + "requires": { + "decompress-response": "^3.3.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "^2.0.2" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "streamroller": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", + "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", + "requires": { + "async": "^2.6.2", + "date-format": "^2.0.0", + "debug": "^3.2.6", + "fs-extra": "^7.0.1", + "lodash": "^4.17.14" + }, + "dependencies": { + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "requires": { + "lodash": "^4.17.11" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "requires": { + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" + }, + "dependencies": { + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", + "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "websocket-stream": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.0.tgz", + "integrity": "sha512-EXy/zXb9kNHI07TIMz1oIUIrPZxQRA8aeJ5XYg5ihV8K4kD1DuA+FY6R96HfdIHzlSzS8HiISAfrm+vVQkZBug==", + "requires": { + "duplexify": "^3.5.1", + "inherits": "^2.0.1", + "readable-stream": "^2.3.3", + "safe-buffer": "^5.1.2", + "ws": "^3.2.0", + "xtend": "^4.0.0" + } + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + } + } +} diff --git a/mappers/modbus_mapper/src/package.json b/mappers/modbus_mapper/src/package.json new file mode 100644 index 000000000..3737905cd --- /dev/null +++ b/mappers/modbus_mapper/src/package.json @@ -0,0 +1,23 @@ +{ + "name": "modbus_mapper", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "async": "3.1.0", + "buffer": "^5.2.1", + "chokidar": "3.0.2", + "fs": "^0.0.1-security", + "log4js": "4.5.1", + "modbus-serial": "^7.4.2", + "mqtt": "^2.18.8", + "nodejs-base64": "^1.0.3", + "util": "^0.11.1", + "uuid": "^3.3.2" + } +} diff --git a/mappers/modbus_mapper/src/watchfile.js b/mappers/modbus_mapper/src/watchfile.js new file mode 100644 index 000000000..1cf13c675 --- /dev/null +++ b/mappers/modbus_mapper/src/watchfile.js @@ -0,0 +1,106 @@ +const path = require('path'); +const fs = require('fs'); +const chokidar = require('chokidar'); +const filename = 'dpl/deviceProfile.json'; +const util = require('util'); + +var devIns = new Map(); +var devMod = new Map(); +var devPro = new Map(); +var modVisitr = new Map(); + +// watchChange monitor dpl configuration file, reload if changed +function watchChange(paths, onChange) { + if (typeof onChange !== 'function') throw Error(`onChange (${onChange}) is not a function`); + + if (!Array.isArray(paths)) paths = [paths]; + + paths.forEach(path => { + if (!(path && fs.existsSync(path))) throw Error(`can't find path ${path}`); + }); + + let watcher = chokidar.watch(paths); + watcher.on('ready', ()=>{ + watcher.on('add', ()=>{ + logger.info('watched file added, load dpl config'); + onChange(filename); + }); + }); +} + +// loadDpl load dpl configuration file +function loadDpl(filename, callback) { + fs.readFile(filename, 'utf-8', (err, data)=>{ + if (err) { + logger.error('load dpl config error: ', err); + } else { + let dplConfigs = JSON.parse(data); + processData(dplConfigs, callback); + } + }); +} + +// loadConfig load mqtt configuration file +function loadConfig(filename, callback) { + fs.readFile(filename, 'utf-8', (err, data)=>{ + if (err) { + logger.error('load config error: ', err); + } else { + let configs = JSON.parse(data); + callback(null, configs); + } + }); +} + +// processData parse dpl config for each deviceInstance +function processData(dplConfigs, callback) { + for (let i = 0; i < dplConfigs.deviceInstances.length; i++) { + buildMaps(dplConfigs, i, (err)=>{ + if (err) { + logger.error('build devIns maps error: ', err) + } + }); + } + for (let i = 0; i < dplConfigs.deviceModels.length; i++) { + for (let j = 0; j < dplConfigs.deviceModels[i].properties.length; j++){ + buildVisitorMaps(dplConfigs, i, j); + } + } + callback(devIns, devMod, devPro, modVisitr); +} + +// buildMaps build three maps 1.map[deviceID]deviceInstance, 2.map[deviceID]deviceModel, 3.map[deviceID]protocol +function buildMaps(dplConfigs, i) { + devIns.set(dplConfigs.deviceInstances[i].id, dplConfigs.deviceInstances[i]); + let foundMod = dplConfigs.deviceModels.findIndex((element)=>{ + return element.name === dplConfigs.deviceInstances[i].model; + }); + if (foundMod != -1) { + devMod.set(dplConfigs.deviceInstances[i].id, dplConfigs.deviceModels[foundMod]); + } else { + logger.error('failed to find model[%s] for deviceid', dplConfigs.deviceModels[i].model); + } + + let foundPro = dplConfigs.protocols.findIndex((element)=>{ + return element.name === dplConfigs.deviceInstances[i].protocol; + }); + if (foundPro != -1) { + devPro.set(dplConfigs.deviceInstances[i].id, dplConfigs.protocols[foundMod]); + } else { + logger.error('failed to find protocol[%s] for deviceid', dplConfigs.deviceModels[i].protocol); + } +} + +// buildVisitorMaps build map[model-property-protocol]propertyVisitor +function buildVisitorMaps(dplConfigs, i, j) { + let foundVisitor = dplConfigs.propertyVisitors.findIndex((element)=>{ + return element.modelName === dplConfigs.deviceModels[i].name && element.propertyName === dplConfigs.deviceModels[i].properties[j].name; + }); + if (foundVisitor != -1) { + modVisitr.set(util.format('%s-%s-%s', dplConfigs.propertyVisitors[foundVisitor].modelName, dplConfigs.propertyVisitors[foundVisitor].propertyName, dplConfigs.propertyVisitors[foundVisitor].protocol), dplConfigs.propertyVisitors[foundVisitor]); + } else { + logger.error('failed to find visitor for model[%s], property[%s]', dplConfigs.deviceModels[i].name, dplConfigs.deviceModels[i].properties[j].name); + } +} + +module.exports = {watchChange, loadDpl, loadConfig}; |
