kubuedge1.15版本Mapper实现步骤(详解):
前提:有真实的或者模拟的终端设备,我采用的是PLC作为智能终端设备,PLC利用modbus TCP协议与边缘节点node树莓派进行连接,中心节点采ubuntu系统主机
1、通过Mapper Framework框架生成Mapper文件:
1)从官网下载kubeedge-master.zip ,取其中的staging文件夹下的mapper-framewokr,传输到虚拟机master节点。(备注:Mapper是跑在边缘节点上,但先在云端进行生成和部署,再在边缘端拉取镜像到本身节点上;mapper以pod形式部署仍然是在云端执行kubectl创建的,与边缘节点上的其他pod管理方式没有区别)
2)进入解压好的kubeedge-framework工程目录下,执行指令:
make generate
但此时会弹出error,没有权限执行,所以给提示出的sh执行文件赋予权限 chmod 777:
chmod 777 拷贝弹出来的文件名称
3)接下来会弹出你需要输入的Mapper的工程名字,我输入的是modbus,我要用!这个根据自己的需要进行输入(The command below will generate a framework for the customized mapper. Run the command and input your mapper’s name:):
4)新生成的文件会出现在上一层目录文件中,找到他进入modbus目录(这是我刚刚输入的,我自己工程目录),接下来对官网给的生成的Mapper框架进行梳理,官网链接(kubeedge/staging/src/github.com/kubeedge/mapper-framework at master · kubeedge/kubeedge):
这个工程目录说明了只需要关心三个文件,分别是config.yaml、driver.go、devicetype.go。目前data工程目录中有三个数据库方法:
- Mapper通过配置目标地址将数据推送到云/边缘应用程序。
- Mapper通过配置目标地址将数据推送到云/边缘数据库。
- 用户应用程序通过 API 主动拉取设备数据。
- Mapper通过云边通道将设备数据推送到云端。
2、确定自己设备的Device model 和Device instance文件:
对本文采用的是PLC作为终端设备,采用modbus TCP协议进行连接边缘节点(树莓派),参考官方链接modbus RTU协议案例:Add v1beta1 Modbus mapper by wbc6080 · Pull Request #113 · kubeedge/mappers-go (github.com)
PLC的device model:
apiVersion: devices.kubeedge.io/v1beta1 #版本
kind: DeviceModel
metadata:
name: modbustcp-device
namespace: default
spec:
protocol: modbus
properties:
- name: temperature #温度
description: temperature in degree celsius
type: INT
accessMode: ReadWrite
maximum: "100" #要加""表示数值范围的属性,后面数字都需要加双引号
minimum: "1"
unit: "degree celsius"
- name: humidity #湿度
description: humidity in Relative Humidity
type: INT #源码都是大写
accessMode: ReadWrite #源码字母开头大写
maximum: "100"
minimum: "1"
unit: "Relative Humidity" #要加""
- name: temperature-enable
description: enable data collection of sensor #控制传感器数据的产生
type: BOOLEAN
accessMode: ReadWrite
- name: light-enable
description: enable of light #控制灯的亮灭
type: BOOLEAN
accessMode: ReadWrite
PLC的device instance文件:
apiVersion: devices.kubeedge.io/v1beta1
kind: Device
metadata:
name: modbusplc
labels:
model: modbustcp-device
spec:
deviceModelRef:
name: modbustcp-device
protocol:
protocolName: modbus
configData:
ip: 10.10.18.2 #modbus从站地址(PLC)
port: 502 #端口号
nodeName: nodepi
properties:
- name: temperature
visitors:
protocolName: modbus
configData:
register: "HoldingRegister" #PLC采取modbus tcp通信,保持寄存器
offset: 30001 #寄存器的起始地址
limit: 1 #读取的寄存器数量
scale: 0.1 #对采集到的温度进行缩放0.1倍
isSwap: false #读取的结果是否交换高低字节顺序
isRegisterSwap: false #寄存器字顺序是否交换
collectCycle: 1000000 #收集传感器数据的时间为1s
reportCycle: 1000000 #消息上报频率时间为1s
reportToCloud: true #是否汇报给云端
desired: #温度信息并不需要desired参数
metadata:
value: "40"
pushMethod:
mqtt:
address: tcp://127.0.0.1:1883
topic: tempearture
qos: 0
retained: false
dbMethod:
influxdb2:
influxdb2ClientConfig:
url: http://127.0.0.1:8086
org: test-org
bucket: test-bucket
influxdb2DataConfig:
measurement: stat
tag:
unit: temperature
fieldKey: beta1test
- name: humidity
visitors:
protocolName: modbus
configData:
register: "HoldingRegister" #PLC采取modbus tcp通信,保持寄存器
offset: 30002 #寄存器的起始地址
limit: 1 #读取的寄存器数量
scale: 0.1 #对采集到的湿度进行缩放0.1倍
isSwap: false #读取的结果是否交换高低字节顺序
isRegisterSwap: false #寄存器字顺序是否交换
collectCycle: 1000000
reportCycle: 1000000
reportToCloud: true
desired: #温度信息并不需要desired参数
metadata:
value: "20"
pushMethod:
mqtt:
address: tcp://127.0.0.1:1883
topic: humidity
qos: 0
retained: false
dbMethod:
influxdb2:
influxdb2ClientConfig:
url: http://127.0.0.1:8086
org: test-org
bucket: test-bucket
influxdb2DataConfig:
measurement: stat
tag:
unit: humidity
fieldKey: beta1test
- name: light-enable
collectCycle: 1000000
reportCycle: 1000000
reportToCloud: true
desired: #不开灯
metadata:
value: 'OFF'
visitors:
protocolName: modbus
configData:
register: "CoilRegister" #PLC采取modbus tcp通信,保持寄存器
offset: 00001 #寄存器的起始地址
limit: 1 #读取的寄存器数量
scale: 1 #对采集到的湿度进行缩放0.1倍
isSwap: false #读取的结果是否交换高低字节顺序
isRegisterSwap: false #寄存器字顺序是否交换
status:
twins:
- propertyName: temperature
reported:
metadata:
timestamp: '1550049403598'
type: int
value: "40"
observedDesired:
metadata:
timestamp: '1550049403598'
type: int
value: "40"
- propertyName: humidity
reported:
metadata:
timestamp: '1550049403598'
type: int
value: "20"
observedDesired:
metadata:
timestamp: '1550049403598'
type: int
value: "20"
- propertyName: light-enable
reported:
metadata:
timestamp: '1550049403598'
type: bool
value: 'OFF'
observedDesired:
metadata:
timestamp: '1550049403598'
type: bool
value: 'OFF'
3、配置生成的Mapper文件:modbus
按照config.yaml、driver.go、devicetype.go这个顺序一个一个的配置,官网链接可以参考(mapper-generator/_template at main · wbc6080/mapper-generator (github.com)):
1)在config.yaml中,需要定义mapper的协议名称。修改config.yaml文件,根据自己的需求,修改protocol和version(Kubeedge的版本):
2)在devicetype.go文件中,需要按照设备实例instance.yaml文件中定义的方式填写ProtocolConfig和VisitorConfig结构体信息,以便mapper能够正确解析配置信息,本文采用的是modbus TCP协议,以下是自己配置的devicetype.go文件代码:
package driver
import (
"sync"
"time"
"github.com/kubeedge/modbus/pkg/common"
"github.com/sailorvii/modbus"
)
// CustomizedDev is the customized device configuration and client information.
type CustomizedDev struct {
Instance common.DeviceInstance
CustomizedClient *CustomizedClient
}
type CustomizedClient struct {
// TODO add some variables to help you better implement device drivers
//intMaxValue int
deviceMutex sync.Mutex
ProtocolConfig
ModbusTCPProtocolConfig
ModbusClient modbus.Client
}
type ProtocolConfig struct {
ProtocolName string `json:"protocolName"`
ConfigData `json:"configData"`
}
type ConfigData struct {
// TODO: add your protocol config data
//DeviceID int `json:"deviceID,omitempty"`
//SerialPort string `json:"serialPort"`
//DataBits int `json:"dataBits"`
//BaudRate int `json:"baudRate"`
//Parity string `json:"parity"`
//StopBits int `json:"stopBits"`
//ProtocolID int `json:"protocolID"`
IP string
Port int
SlaveID byte
Timeout int
}
type ModbusTCPProtocolConfig struct {
IP string
Port int
SlaveID byte
Timeout time.Duration
}
type VisitorConfig struct {
ProtocolName string `json:"protocolName"`
VisitorConfigData `json:"configData"`
}
type VisitorConfigData struct {
// TODO: add your visitor config data
DataType string `json:"dataType"`
Register string `json:"register"`
Offset uint16 `json:"offset"`
Limit int `json:"limit"`
Scale float64 `json:"scale,omitempty"`
IsSwap bool `json:"isSwap,omitempty"`
IsRegisterSwap bool `json:"isRegisterSwap,omitempty"`
}
3)在driver.go文件中,需要自定义初始化设备和获取设备数据的方法,并对mapper收集的数据进行标准化。
以下是采用modbus TCP协议的driver.go文件:
package driver
import (
"errors"
"fmt"
"strconv"
"sync"
"time"
"github.com/sailorvii/modbus"
"k8s.io/klog/v2"
)
var clients *sync.Map
var clientInit sync.Once
func initMap() {
clientInit.Do(func() {
if clients == nil {
clients = new(sync.Map)
}
})
}
func NewClient(protocol ProtocolConfig) (*CustomizedClient, error) {
modbusTCPProtocolConfig := ModbusTCPProtocolConfig{
IP: protocol.IP,
Port: protocol.Port,
SlaveID: protocol.SlaveID,
Timeout: time.Duration(protocol.Timeout),
}
client := &CustomizedClient{
ProtocolConfig: protocol,
deviceMutex: sync.Mutex{},
ModbusTCPProtocolConfig: modbusTCPProtocolConfig,
// TODO initialize the variables you added
}
return client, nil
}
func (c *CustomizedClient) InitDevice() error {
// TODO: add init operation
// you can use c.ProtocolConfig
initMap()
klog.Infof("IP: %s, Port: %d", c.ModbusTCPProtocolConfig.IP, c.ModbusTCPProtocolConfig.Port)
key := fmt.Sprintf("%s:%d", c.ModbusTCPProtocolConfig.IP, c.ModbusTCPProtocolConfig.Port)
v, ok := clients.Load(key)
if ok {
c.ModbusClient = v.(modbus.Client)
return nil
}
handler := modbus.NewTCPClientHandler(c.ModbusTCPProtocolConfig.IP + ":" + strconv.Itoa(c.ModbusTCPProtocolConfig.Port))
handler.SlaveId = c.ModbusTCPProtocolConfig.SlaveID
handler.Timeout = c.ModbusTCPProtocolConfig.Timeout
client := modbus.NewClient(handler)
clients.Store(key, client)
c.ModbusClient = client
return nil
}
func (c *CustomizedClient) GetDeviceData(visitor *VisitorConfig) (interface{}, error) {
// TODO: add the code to get device's data
// you can use c.ProtocolConfig and visitor
c.deviceMutex.Lock()
defer c.deviceMutex.Unlock()
var results []byte
var err error
switch visitor.Register {
case "CoilRegister":
results, err = c.ModbusClient.ReadCoils(visitor.Offset, uint16(visitor.Limit))
case "DiscreteInputRegister":
results, err = c.ModbusClient.ReadDiscreteInputs(visitor.Offset, uint16(visitor.Limit))
case "HoldingRegister":
results, err = c.ModbusClient.ReadHoldingRegisters(visitor.Offset, uint16(visitor.Limit))
case "InputRegister":
results, err = c.ModbusClient.ReadInputRegisters(visitor.Offset, uint16(visitor.Limit))
default:
return nil, errors.New("Bad register type")
}
klog.V(2).Info("Get result: ", results)
return results, err
}
func (c *CustomizedClient) SetDeviceData(data interface{}, visitor *VisitorConfig) (interface{}, error) {
// TODO: set device's data
// you can use c.ProtocolConfig and visitor
var results []byte
var err error
c.deviceMutex.Lock()
defer c.deviceMutex.Unlock()
klog.V(1).Info("Set: %s %d %d", visitor.Register, visitor.Offset, uint16(visitor.Limit))
switch visitor.Register {
case "CoilRegister":
var valueSet uint16
switch uint16(visitor.Limit) {
case 0:
valueSet = 0x0000 //0x0000 表示设置线圈为OFF状态,关闭
case 1:
valueSet = 0xFF00 //0xFF00 表示设置线圈为ON状态,打开
default:
return nil, errors.New("Wrong value")
}
results, err = c.ModbusClient.WriteSingleCoil(visitor.Offset, valueSet)
case "HoldingRegister":
results, err = c.ModbusClient.WriteSingleRegister(visitor.Offset, uint16(visitor.Limit))
default:
return nil, errors.New("Bad register type")
}
klog.V(1).Info("Set result: %v %v", err, results)
return results, err
}
func (c *CustomizedClient) StopDevice() error {
// TODO: stop device
// you can use c.ProtocolConfig
//klog.Infof("Stop device%d successful", c.DeviceID)
err := c.ModbusClient.Close()
if err != nil {
return err
}
return nil
}
func parity(ori string) string {
var p string
switch ori {
case "even":
p = "E"
case "odd":
p = "O"
default:
p = "N"
}
return p
}
4、生成配置Device model 和Device instance文件:
注意事项
:在高版本golang中会报错,需要修改device.go 263行
err = dev.CustomizedClient.SetDeviceData(value, visitorConfig)---->>
_err = dev.CustomizedClient.SetDeviceData(value, visitorConfig)
2) 从官网找到devices_v1beta1_device.yaml和devices_v1beta1_devicemodel.yaml文件,在master节点apply他们,然后部署自己的model与instance。对修改完成的两个文件在master节点进行apply:
kubectl apply -f <path to device model yaml>
kubectl apply -f <path to device instance yaml>
5)部署Mapper
1)生成mapper项目并填充driver文件夹后,用户可以根据Dockerfile文件制作自己的mapper镜像,随后通过deployment等方式将mapper部署在集群中
docker build -t [YOUR MAPPER IMAGE NAME] .
备注:在高版本golang中会报错,需要修改device.go 263行err = dev.CustomizedClient.SetDeviceData(value, visitorConfig)------------------_err = dev.CustomizedClient.SetDeviceData(value, visitorConfig)
2)对修改完成的两个文件在master节点进行apply,部署自己的model和instance:
kubectl apply -f <path to device model yaml>
kubectl apply -f <path to device instance yaml>
5)部署Mapper
1)生成mapper项目并填充driver文件夹后,用户可以根据Dockerfile文件制作自己的mapper镜像,随后通过deployment等方式将mapper部署在集群中
docker build -t [YOUR MAPPER IMAGE NAME] .
注:如果边缘节点跑的是ARM架构的设备,比如树莓派这些,自己记得修改Dockfiles文件,不然默认AMD架构
备注:Mapper设计者:mapper设计的机制就是无论遵从什么协议都只需要修改这三个地方。一般创建mapper的流程是先制作一个device-instance.yaml,里面需要填写用户自己protocol相关的配置字段,之后需要在devicetype文件中对应yaml文件把用户自己填写的字段copy一遍,这样mapper就能正确解析到yaml中字段的信息,然后这些信息会被发送到driver文件中,通过协议字段来完成设备连接、设备数据获取工作