本地倒计时功能开发实践
更新时间:2019-04-08 14:48:02
简介
本文提供了一个插座本地倒计时功能的开发案例,开发者可以参考本文,实现任意设备的本地倒计时功能。
“本地倒计时”是指,由App页面向设备端下发“开始倒计时”的任务后,设备端开始根据本地时钟,执行倒计时任务。
要实现本地倒计时的效果,设备端和APP端需要按照本文推荐的方式进行实现。
设备端:按照平台标准数据格式,实现倒计时任务的增删改。
APP端:在公版App中,或平台给自定义App提供的设备界面中,如“灯”“开关”,可以直接使用倒计时功能,如下图位置:
多路倒计时
1.概述
与单路倒计时不同的是,多路倒计时可以同时对多个设备上的布尔值设定本地倒计时任务,如多路插座。
多路倒计时:
2.准备工作
控制台-产品-人机交互-自动化和定时:
倒计时一列,勾选需要支持本地倒计时的功能,如主灯开关:
完成这个步骤后,点击保存,平台将在 功能定义 中,自动插入一个标准功能“倒计时”(CountDownList):
参数名称 | 参数标识 | 数据类型 | 值 |
---|---|---|---|
操作对象 | Target | string | |
倒计时命令 | Contents | string |
数据结构说明
功能名称:倒计时列表
标识符:CountDownList
类型:JSON
CountDownList: {
Target: "PowerSwitch"(string,当次设置操作指定的布尔值的identifier),
Contents: "PowerSwitch-1-1-123-1535644800000,LightSwitch-0-1-456-1535644800000"(string, 该设备的所有倒计时任务字符串,具体格式说明见下方)
XXX1:0(bool,该产品已有的布尔类型标准属性),
XXX2:0(bool,该产品已有的布尔类型标准属性)
}
CountDownList本身是可选的标准属性
Target和Contents为CountDownList的必选属性
二者都是string类型(最大长度为2048字节)
Target和Contents期望后台数据加上,不用开发者自己添加
XXX1和XXX2为当前产品已有的标准布尔属性,可以由开发者随意添加或删除
注意:如果功能定义里有CountDownList,但CountDownList里除了Target和Contents外没有别的布尔属性,那么属于异常情况,应该报错提示
设备端同学注意:前端设置完以后,XX1 XX2 这边的值默认是0,设备端不需要Care这个。仅关注Contents里内容即可
字段说明
Target
类型:string
属性名:操作对象
是否必选:是
长度:2048
当次操作的布尔值类型的标准属性
为什么要有这个字段?假设第一次设置一个倒计时任务:PowerSwitch-1-1-1000-1535644800000,那么1分钟后,要设置第二个倒计时任务:PowerSwitch-1-1-1000-1535644800000,Enable-0-1-2000-1535644800000,如果没有这个Target字段指定的属性,那么第一次设置的倒计时任务的时间将又从1000s开始计时
Contents
类型:string
属性名:倒计时命令
是否必选:是
长度:2048
因为本次的需求是同时设置多个布尔值属性的倒计时任务,且当前没有数组类型的数据,故采用一个string来存放设备的所有倒计时任务
例子:"PowerSwitch-1-1-123-1535644800000,LightSwitch-0-1-456-1535644800000"
不同布尔值的倒计时任务用逗号来分隔
用-来分隔一个属性倒计时任务的不同字段
每一条倒计时任务的格式说明:
以PowerSwitch-0-1-1535644800000为例
第一位:PowerSwitch 设备上某一个布尔值的identifier
第二位:0 倒计时到了之后要执行的布尔值动作,可选值仅有0和1
第三位:1 当前这一条倒计时任务的执行状态,可选值仅有0和1,0代表倒计时时间已到,1代表倒计时时间还未到
第四位:123 代表设置的倒计时时间段,单位为s,最大可以设置为23时59分59秒,最小为1秒,不能设置为0。当设置为0时,插件侧应进行报错提示。
第五位:1535644800000 代表设置该倒计时任务时,App侧的当前时间戳。这个字段是用来重新展示倒计时列表时(例如退出设备面板后过一会儿又再进入面板),展示最新的剩余倒计时时间。
点击倒计时按钮时
1.如果当前产品的功能定义里有CountDownList属性,那么此时走新的倒计时列表逻辑,在当前组件展示倒计时列表相关界面
2.如果当前产品的功能定义里没有CountDownList属性,但是有CountDown属性,那么则走以前的倒计时插件逻辑。
点击某个倒计时任务的撤销
1.App删除Contents字段里对应identifier的任务,同时设置Target为这个identifier,上报云端,下发给设备端。
2.设备端收到新的CountDownList属性,发现Target指定了一个identifier,但是Contents里却没有这个identifier,那么删除正在执行的identifier对应的倒计时任务,不影响其余的倒计时任务。
3.场景举例说明
1.点击倒计时按钮
2.检查tsl,发现CountDownList里还有额外的XXX1,XXX2属性
3.倒计时列表弹层展示XXX1、XXX2的中文名称列表
4.选择其中一个属性,例如PowerSwitch,展示设置时间和动作界面,设定好时间(1000s)和动作(打开)
5.设置Target: "PowerSwitch", 设置Contents: "PowerSwitch-1-1-1000-1535644800000",下发给云端
6.设备端收到CountDown后,解析Contents和Target内容,设置实际的定时任务
7.此时又设置了第二个属性XXX2,Contents为:"PowerSwitch-1-1-1000-1535644800000,XXX2-0-1-2000-1535644800000",Target:"XXX2"
8.1000s到了之后,第一个PowerSwitch倒计时任务结束,设备端删除Contents里PowerSwitch这一条任务,同时设置Target为PowerSwitch,将整个CountDownList属性上报到云端,插件侧查询CountDown属性,发现Contents里没有Target指定的布尔属性,代表PowerSwitch倒计时任务已执行,进而提示PowerSwitch倒计时任务执行完毕
注意:整个插件和设备端上报云端的过程中,CountDownList里的XXX1和XXX2等布尔属性可以设置为任意符合布尔属性的值
单路倒计时
1. 概述
如果该设备中,只有一个功能需要支持倒计时,可以使用该方案,如单路插座。
单路倒计时:
2. 准备工作
控制台中注册您的产品,详细参考 产品注册,本文以插座为例。在 功能定义 中,需要选择标准功能“倒计时”(CountDown),倒计时属性的数据结构为JSON对象。
例如:
CountDown: {
IsRunning: 0(bool,0:没有倒计时,1:正在倒计时,),
TimeLeft: 200(int,秒数,最大值为23h59m59s对应的秒数),
PowerSwitch: 0(bool,代表倒计时时间到了后,开关要执行的动作,0为关闭,1为打开)
Timestamp: "1535644800000"(string类型的UTC时间戳,调用设置接口时的当前时间戳,用于同步时间)
}
功能名称:本地倒计时
标识符:CountDown
类型:JSON
参数名称 | 参数标识 | 数据类型 | 值 |
---|---|---|---|
执行状态 | IsRunning | bool | 0 - 已停止1 - 执行中 |
剩余时间 | TimeLeft | int | min: 1max: 86399step: 1unit: 秒/s |
开关动作 | PowerSwitch | bool | 0 - 关闭1 - 打开 |
当前时间戳 | Timestamp | date | string类型的UTC时间戳 |
3. 设计原理
如何实现本地倒计时:
APP上设定时间,点击开始倒计时,会将上面所述的CountDown通过云端下发到设备端;
设备端收到CountDown后,根据TimeLeft和PowerSwitch的内容,开始执行倒计时,并且在倒计时时间到了之后,执行PowerSwitch中的开关动作;
倒计时的动作执行后,将下发时的CountDown中的IsRunning设置为0,再将CountDown上报到云端(不用修改CountDown中除IsRunning外的其他字段);
若设备中途断电,在上电后,需要将CountDown中的IsRunning设置为0,然后上报CountDown到云端。
如何取消本地倒计时功能:
将之前收到的CountDown中的IsRunning设置为0下行到设备端
设备端会回复CountDown(IsRunning=0)至云端
一些说明:
- APP设置了一个200s后关闭电源的动作,set时候传的数据应为:
CountDown: {
IsRunning: 1(bool,0:没有倒计时,1:正在倒计时,),
TimeLeft: 200(int,秒数,最大值为23h59m59s对应的秒数),
PowerSwitch: 0(bool,代表倒计时时间到了后,开关要执行的动作,0为关闭,1为打开)
Timestamp: "1535644800000"(string类型的UTC时间戳,调用设置接口时的当前时间戳,用于同步时间)
}
APP收到set成功的返回响应后,跳转到开始倒计时的页面,开始正常倒计时
此时若退出APP,50s后再打开APP进入设备控制界面或本地倒计时插件,那么此时显示的剩余时间应为:
200 - (当前本地时间戳 - set时候传递的本地时间戳)= 200 - 50 = 150s
4. 设备端开发
以下时间案例中,设备每次收到云端下行的指令会立即回复CountDown做出响应,同时创建一个绑定至定时器服务函数app_timer_expired_handle()的定时器,等到本地倒计时时间耗尽时,该定时器服务函数会自动调用,CountDown属性中的IsRunning修改成0上行至云端,同时上报设备PowerSwitch属性,即宣告本地倒计时完成。
/*
* the handler of property changed
* alink method: thing.service.property.set
*/
static int thing_prop_changed(const void *thing_id, const char *property, void *ctx)
{
int ret = -1;
app_context_t *app_ctx = (app_context_t *)ctx;
APP_TRACE("property \"%s\" changed", property);
if (strstr(property, app_ctx->prop_countdown) != 0) {
/* post property
* result is response_id; if response_id = -1, it is fail, else it is success.
* response_id by be compare in post_property_cb.
*/
int powerSwitch[1] = {0};
int timeLeft[1] = {0};
int isRunning[1] = {0};
/* Get property value of "CountDown" */
linkkit_get_value(linkkit_method_get_property_value, thing_id, app_ctx->prop_countdown_pwrsw, powerSwitch, NULL);
APP_TRACE("app get property value, PowerSwitch = %d", powerSwitch[0]);
linkkit_get_value(linkkit_method_get_property_value, thing_id, app_ctx->prop_countdown_timelf, timeLeft, NULL);
APP_TRACE("app get property value, TimeLeft = %d", timeLeft[0]);
linkkit_get_value(linkkit_method_get_property_value, thing_id, app_ctx->prop_countdown_isrun, isRunning, NULL);
APP_TRACE("app get property value, IsRunning = %d", isRunning[0]);
/* Start or stop timer according to "IsRunning" */
if (isRunning[0] == 1) {
app_timer_start(app_ctx->timerHandle, timeLeft[0]);
/* temp powerswitch value to app context */
app_ctx->powerSwitch_Target = powerSwitch[0];
} else if (isRunning[0] == 0) {
app_timer_stop(app_ctx->timerHandle);
}
/* Post property "CountDown" to response request from cloud*/
ret = linkkit_post_property(thing_id, property, post_property_cb);
if (ret != SUCCESS_RETURN) {
APP_TRACE("app post property \"%s\" failed", property);
return ret;
}
APP_TRACE("app post property \"%s\" succeed", property);
/* Set property "PowerSwitch" then post */
ret = linkkit_set_value(linkkit_method_set_property_value, app_ctx->thing, app_ctx->prop_powerswitch,
&app_ctx->powerSwitch_Actual, NULL);
if (ret != SUCCESS_RETURN) {
APP_TRACE("app set property \"%s\" failed", app_ctx->prop_powerswitch);
return ret;
}
ret = linkkit_post_property(app_ctx->thing, app_ctx->prop_powerswitch, post_property_cb);
if (ret != SUCCESS_RETURN) {
APP_TRACE("app post property \"%s\" failed", app_ctx->prop_powerswitch);
return ret;
}
APP_TRACE("app post property \"%s\" succeed", app_ctx->prop_powerswitch);
} else if (strstr(property, app_ctx->prop_powerswitch) != 0) {
int powerSwitch[1] = {0};
/* Get property value of "PowerSwitch" */
linkkit_get_value(linkkit_method_get_property_value, thing_id, app_ctx->prop_powerswitch, powerSwitch, NULL);
APP_TRACE("app get property value, PowerSwitch = %d", powerSwitch[0]);
/* Trigger powerswitch action, just set value here */
app_ctx->powerSwitch_Actual = powerSwitch[0];
/* Set property "PowerSwitch" then post */
ret = linkkit_set_value(linkkit_method_set_property_value, app_ctx->thing, app_ctx->prop_powerswitch,
&app_ctx->powerSwitch_Actual, NULL);
if (ret != SUCCESS_RETURN) {
APP_TRACE("app set property \"%s\" failed", app_ctx->prop_powerswitch);
return ret;
}
ret = linkkit_post_property(app_ctx->thing, app_ctx->prop_powerswitch, post_property_cb);
if (ret != SUCCESS_RETURN) {
APP_TRACE("app post property \"%s\" failed", app_ctx->prop_powerswitch);
return ret;
}
APP_TRACE("app post property \"%s\" succeed", app_ctx->prop_powerswitch);
}
return 0;
}