yeedriver 驱动的基础类
在autoPoll模式下,writeWQ是同步的,如果返回成功,就不再重发,否则就会不断重发
在事件模式下,需要用另外一种方式处理:
1.每个workerBase的继承类,会有两个对象:
做一个wqs_latch,该值记录各个devId下的wq所对应的值,该值应该来自于设备,至于如何从设备获取,由实现者定义
驱动在写入的时候,会做一个wqs_target,该值会记录实际需要写入的值,该值由系统维护
workerBase的继承类,通过setAsyncWQTimer来设置需要重发的定时器间隔
系统驱动根据定时器,对比wqs_target和wqs_latch里的值,如果一致,则跳过,如果不一致,就会启动相应的WriteWQ进行重发
需要注意的是writeWQ是并发进行的,同时又被定时器触发(需要重发的时候),因此,有可能在某个wq正在被写入的时候,wq又触发了。为了避免这种情况,在wqs_target里,设置两项,一个是值(value),一个是状态(state),状态有两种,busy和idle。驱动定时器在发现状态是busy的时候,就跳过这一次的重写,只有当检查的时候是idle的时候,才可能进行重写。
####写状态定义
this.WRITE_WTATE= {
'IDLE': 0, //当前空闲
'BUSY': 1, //数据正在发送中
'PENDING': 2, //数据发送完毕,但是回应的状态不一致,需要重发,等待workerBase下一次重发
'FAILED': 3, //数据发送失败,这一次要重发
'CONFIRM':4
}
#####过程
IDLE -- 要求写入 --> BUSY --写入完毕--> 确认写入成功
如果目前和写入的一致--> IDLE
如果目标和写入的变化了--> PENDING
--> 写入失败 FAILED
######CONFIRM的意义: 有一些设备,操作回应需要一段时间,而设备本身在收到命令的时候,就会返回一个信息,告诉命令收到,而后等操作成功后,会再发送一个设备的当前状态 这种情况下,驱动就可以在收到第一次命令的时候,进入confirm状态,然后等到实际的状态和目标的状态一致时,再改成IDLE
因此,驱动在实现写动作的时候,需要做以下事情:
实现writeWQ,来实现实际的数据写入(驱动已经把wqs_target的内容填充好了,不用再考虑的)
在合适的时间点上修改wqs_target里的state值为idle
在合适的时候,更新wqs的状态
workerBase提供一个工具函数实现:
更新wq的状态: updateWqState(deviceId,ep,newState)
###实际的重发实现
workerBase有一个定时器,每隔2秒查询一次所有的wq的状态
如果是FAILED,就进行一次重发
如果是PENDING,就修改成FAILED
另外,驱动在实现读动作的时候,按以下的方式进行
1.驱动实现ReadWQ
2.驱动在确认数据变化的时候,产生一个RegRead的事件。 其数据内容格式如下:
const memories= {devId:devId,memories:{wq_map:[{start:wq,end:wq,len:1}]}};
this.evtMaster.emit('RegRead',memories);
另外,驱动 在初始化的时候,别忘记配置
this.setRunningState(this.RUNNING_STATE.CONNECTED); //设置成运行中,允许数据收发
this.setupEvent();//设置成事件模式
this.setupAsyncWQTimer(2000); //打开重发
如果本身不需要重发的,或者发送有回应本身就不一致的,如发送访客机查询一个访客记录等这种数据,通过setupAsyncWQTimer(0)来关闭重发,或者不调用setupAsyncWQTimer,因为默认是不重发的。
在autoPoll模式下,为了避免大量的无效数据召测,驱动可以设置自动读取功能,实现针对某些数据的自动定时读取,同时可以对不同的数据点进行不同间隔的读取 在autopoll类型的设备驱动中,如果没有设置,系统自动会不断查询memories中的所有的bi/bq/wi/wq的数据,但是在实际应用中,有可能需要按照某个间隔或是时间点进行一些读取的动作,想要达到这些效果,按如下的方式进行:
在调用this.SetAutoReadConfig 设置内容,其参数是一个对象,格式如下: {
"devId":{
"1":{
"interval":xxx ,读取的间隔时间
"readTime":{"month":xx,"day":xx,"hour":xx,"minute":xx,"second":xx}
},
"2":{
"interval":xxx ,读取的间隔时间
"readTime":{"month":xx,"day":xx,"hour":xx,"minute":xx,"second":xx}
}
}
}
interval是表示间隔多久读一次
readTime表示在哪个时间点读一次,两者只需要一个,interval优先
readTime:如果是每天读,就不需要month,如果每小时读,就不需要month/day字段,以此类推.
举例:
{"day":1,"hour":0,"minute":1,"second":0} 表示每月1号的0:1:0秒读取一次
而
{"hour":0,"minute":1,"second":0} 表示每天00:01:00秒读取一次
更低级别的如果没有定义,默认为0
{"hour":0,"minute":1} 仍旧示每天00:01:00秒读取一次
如果对应的属性值不是一个数字,而是一个字符串,那么系统会把这个字符串解析成一个函数,如果函数(以对应的属性值为参数)运行返回true,该点上就会执行数据读取动作
如:
"minute":"function(data) { return (data % 15) === 0},那么就会每隔15分钟读取一次
ReadWQ/WI的mapItem是一个复杂的结构,原来的目的在于提高效率,可以实现多个wq同时读取,但是在更方便的情况下,我们希望一个wq一个wq地读写,因此提供了一个辅助函数
/**
* 针对每一个mapItem的项,调用一次regReader,具有一个参数( reg),代表所要读取的寄存器, 返回读取的值即可
* @param mapItem 数据项
* @param regReader ( reg )
* @returns {Promise.<TResult>}
* @constructor
*/
WorkerBase.prototype.CreateWQReader = function(mapItem,regReader){
然后,实际的代码可以这样写:
CustomDeviceClass.prototype.ReadWQ = function(mapItem,devId){
return this.CreateWQReader(mapItem,function(reg){
return (this.devices[devId] && this.devices[devId].eps && this.devices[devId].eps[reg]);
});
};
this.devices[devId]是存在该动下的子设备信息
function(reg){函数直接使用this即可,原函数里已经bind(this)了
同样,针对写入函数:
/**
* 这是一个辅助函数
* 针对每一个mapItem的项,调用一次regReader,具有两个参数( reg,regValue), reg是要写入的寄存器号,regValue是要写入的值
* @param mapItem 数据项
* @param regReader ( reg,results)
* @returns {Promise.<TResult>}
* @constructor
*/
WorkerBase.prototype.CreateWQWriter = function(mapItem,values,regWriter){
客户代码的写法示例如下:
CustomDeviceClass.prototype.WriteWQ = function (mapItem, value, devId) {
let results = this.CreateWQWriter(mapItem,value,function(reg,regValue){
this.devices[devId] = this.devices[devId] || {};
this.devices[devId].eps = this.devices[devId].eps || {};
this.devices[devId].eps[reg] = regValue;
const memories= {devId:devId,memories:{wq_map:[{start:reg,end:reg,len:1}]}};
this.emit('RegRead',memories);
}.bind(this));
};
这里简单把数据存在devices里,然后数据变化了,触发RegRead来通知上层检测数据变化的内容
这里不需要针对WQ做一个数据变化的检测,检测到变化再触发消息,因为上层收到消息后,会自动检查。
##数据变化通知 在autoPoll模式下,系统不断的轮询数据,并且与上一次读取的数据作比较,比较发现有数据变化后,会自动触发通知相应的上层系统。 但是在event模式下,系统会主动收到数据,需要一个机制来实现向上层通知,WorkerBase提供一个事件'RegRead‘给客户端代码。 当数据变化的时候,客户代码通过this.emit('RegRead',memories)来向上层汇报有数据变化了 注意:memories是一个
const memories= {devId:devId,memories:{wq_map:[{start:reg,end:reg,len:1}]}};
的结构,驱动响应此消息,然后调用ReadWQ/WI系列命令来读取相应的WQ的值,并且会自动与上一次的相比较是否变化了,如果变化,就会产生一系列的动作通知上层
- 这种处理方式,对客户代码有麻烦,因此WorkBase提供了两个辅助函数 WorkBase.setOneMemChanged(devId,memTag,memId) 客户端代码通过此函数通知驱动某个设备的寄存器信息已经更新了
##添加删除设备 添加或删除设备通过inOrEx来实现
/*
* 启动加入设备状态
*
*/
WorkerBase.prototype.setInOrEx = function (option) {
};
WorkerBase.prototype.inOrEx = function (option) {
SendMessage(process, {cmd: 'inOrEx', option});
};
setInOrEx是收到云端发送的添加命令后的响应函数 inOrEx是用于向云端汇报设备有变化了。
继承类重写setInOrEx函数,从而实现对用户端设备刷新等命令的响应 在setInOrEx函数中,重新检查一遍设备,然后与options中的sids来对比,确定设备的变化以及设备的类型,通过调用inOrEx来向云端通知。 下面是一个示例
//向网关查询一遍
var addDevices = {};
var delDevices = {};
this.gatewayMaster.EnumDevices();
setTimeout(function () {
//3秒后对比数据
var self = this;
var rawOptIds = (self.rawOptions && self.rawOptions.sids) || {};
let newDevices = this.gatewayMaster.getDevicesList();
_.each(newDevices, function (devInfo, devId) {
if (rawOptIds[devId] === undefined) {
addDevices[devId] = devInfo;
}
});
_.each(rawOptIds, function (devInfo, devId) {
if (newDevices[devId] === undefined) {
delDevices[devId] = devInfo;
}
});
if (!_.isEmpty(addDevices))
this.inOrEx({type: "in", devices: addDevices});//uniqueKey:nodeid,uniqueId:nodeinfo.manufacturerid+nodeinfo.productid})
//console.log('new Devices:',addDevices);
if (!_.isEmpty(delDevices)) {
this.inOrEx({type: "ex", devices: delDevices});
}
//console.log('removed Devices:',delDevices);
}.bind(this), 3000);
通过this.inOrEx向主机汇报设备的变化
inOrEx(options)中的参数格式:
.type "in"/"ex" 表示设备是添加了还是删除了
.devices 需要对应的设备,其格式如下:
{
deviceId:{
uniqueId:xxxx,
nameInGroup:xxx,
groupId:xxxx
config:xxxx
}
}
deviceId是对应的设备id,这个是在一个家庭中唯一的,
uniqueId是在该驱动下的唯一类型id
nameInGroup 设备名称,可以没有,如果没有,云端会自动根据设备类型赋一个
groupId:所在的group,默认总是在.里
config,需要提供给云端的配置