zoukankan      html  css  js  c++  java
  • 微信硬件H5面板开发(二) ---- 实现一个灯的控制

    第一节中讲解了openApi的调用,这一篇讲一下如何实现一个灯的控制。就用微信提供的lamp例子来做,将代码扒下来(实在是没办法,没有示例),整合到自己的项目中。lamp源码:http://files.cnblogs.com/files/stoneniqiu/lamp.zip

    你可以自己扒,带参数的页面在浏览器中打开会马上跳转,不带参数的会提示参数不全,需要用mobile模式观看。

    呈现的界面如下:

    目录结构 

    解压开lamp.js ,目录如下,这个demo是基于sea.js+zepto实现,sea.js用来加载模块,zepto提供ajax请求和tab事件等。

    common中包含了一个keyConfig.js(地址参数),一个reqData.js(请求封装)还有一个zepto,ui里是一个上面图片的中的slider一样的组件。util中是一组方法集合。最重要的就是lamp.js 。

    define(function (require) {
        var $ = require("common/zepto");
        var keyConfig = require("common/keyConfig");
        var reqData = require("common/reqData");
        var util = require("util/util");
        var ProcessBar = require("ui/process-bar");
    
        var pageParam = {
            device_id: util.getQuery("device_id"),
            device_type: util.getQuery("device_type"),
            appid: util.getQuery("appid")
        };
        var lastModTime = 0;
    
        var powerBtn = $("#powerBtn"), // 开关按钮
            lightBar;
        var device_status= {
            services: {
                lightbulb: {alpha:0},
                operation_status:{status:0}
            }
        }; // 数据对象
    
        (function () {
            if(!pageParam.device_id || !pageParam.device_type){
                alert("页面缺少参数");
                return;
            }
            log("appid:" + pageParam.appid);
            log("device_id:" + pageParam.device_id);
            log("device_type:" + pageParam.device_type);
            powerBtn.on("tap", togglePower); // 开关按钮事件
            initBar();
            initInterval();
    
            // todo : for test, delete before submit
    //        renderPage({});
        })();
    
        /**
         * 初始化进度条
         */
        function initBar() {
            log("初始化lightBar");
            lightBar = new ProcessBar({
                $id: "lightBar",
                min: 0,
                stepCount: 100,
                step: 1,
                touchEnd: function (val) {
                    device_status.services.lightbulb.alpha = val;
                    log("亮度值为:"+val);
                    setData();
                }
            });
        }
        /**
         * 请求数据
         */
        function getData() {
            reqData.ajaxReq({
                //url: keyConfig.GET_LAMP_STATUS,
                url:'https://api.weixin.qq.com/device/getlampstatus',
                data: pageParam,
                onSuccess: renderPage,
                onError:function(msg) {
                    log("获取数据失败:" + JSON.stringify(msg));
                }
            });
        }
        /**
         * 设置数据
         */
        function setData() {
            console.log("setUrl", keyConfig.SET_LAMP_STATUS);
            lastModTime = new Date().getTime(); // 更新最后一次操作时间
            reqData.ajaxReq({
               // url: keyConfig.SET_LAMP_STATUS,
                url: 'https://api.weixin.qq.com/device/setlampstatus',
                type: "POST",
                data: JSON.stringify(device_status)
            });
            log("setData:" + JSON.stringify(device_status));
    
        }
    
        /**
         * 开关按钮事件
         */
        function togglePower() {
            $("#switchBtn").toggleClass("on").toggleClass("off");
            log("灯的状态status:"+device_status.services.operation_status.status);
            if(device_status.services.operation_status.status==0){
                device_status.services.operation_status.status = 1;
                log("灯的状态:1");
    
            } else {
                device_status.services.operation_status.status = 0;
                log("灯的状态:0");
            }
            setData();
        }
    
        /**
         * 轮询
         */
        function initInterval() {
            getData();
            setInterval(function () {
                if((new Date().getTime() - lastModTime) > 2000){ // 当有设置操作时,停止1s轮询,2秒后继续轮询
                    getData();
                }
            }, 1000);
        }
    
        /**
         * 渲染页面
         */
        function renderPage(json) {
            // todo : for test, delete before submit
    //        json = {
    //            device_status: {
    //                services: {
    //                    operation_status: {
    //                        status: 0
    //                    },
    //                    lightbulb: {
    //                        alpha: 0
    //                    }
    //                }
    //            }
    //        };
            log("renderPage:"+json);
            if(!json.device_status){
                return;
            }
            console.log("json", json);
            device_status = json.device_status;
            log(device_status);
            if(device_status.services.operation_status.status==0){
                $("#switchBtn").addClass("on").removeClass("off");
            } else {
                $("#switchBtn").addClass("off").removeClass("on");
            }
            lightBar.setVal(device_status.services.lightbulb.alpha);
        }
    });/*  |xGv00|4199711a9ade00e2807e7ea576d92f55 */

     首先我们看到pageParam对象是获取页面上参数的,device_id,device_type以及appid三个参数。其实有用的只有前面两个,因为appid的话,后台服务器已经配置了,而且在微信中的通过“进入面板”的时候只附带了id和type两个参数。然后device_status是一个设备状态对象对象是灯,根据微信services的定义,灯有一个亮度值。这个在上一篇提到过。然后是一个立即执行的匿名函数,这个函数函数里面会先检查一下参数,然后初始化开关和亮度条。最好进入循环。initInterval中就是不断的通过getdata获取数据。注意到这儿有一个lastModTime的比较,然后延时2秒再触发,这个地方主要是因为每次设置之后再从服务器捞到数据有一个延时。原本是10,你设置了20,bar也到了20的位置,但是呢,服务器还有一个10在路上发过来,你设置的20并没有马上失效,这会有一个卡顿的效果。但这个两秒也不是那么的有效,卡顿还是会有;另外一方面就是,不能设置太快,设置太快了会报50019的错误(设备正在被操作);getdata成功后,就是renderpage,这个不用解释了。注意到在绑定开关时间的地方,其实是先调用了一次setdata

     powerBtn.on("tap", togglePower);
    
     function togglePower() {
            $("#switchBtn").toggleClass("on").toggleClass("off");
            log("灯的状态status:"+device_status.services.operation_status.status);
            if(device_status.services.operation_status.status==0){
                device_status.services.operation_status.status = 1;
                log("灯的状态:1");
    
            } else {
                device_status.services.operation_status.status = 0;
                log("灯的状态:0");
            }
            setData();
        }

     这个作用有两个,一个是获取设备目前的状态,因为设备可能没有开启,或者没有联网,二个是将参数传递给后台,不然getdata无效。最后理清一下思路就是

    获取参数-->初始化-->setdata一次-->循环-->渲染页面  界面操作-->setdata-->延时读取。 加上后端的部分,全部的流程图如下。

    所以拿到前端代码只是一半,后端还需要自己实现。

    实现

    纯静态文件是无法请求微信服务器的,所以我们需要自己实现后台的部分,这也是第一节中要讲的目的。

    html:

    @{
        Layout = null;
    }
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta id="viewport" name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
        <title>我的灯泡</title>
        <link href="/css/common.css" rel="stylesheet" />
        <link href="/css/light_switch.css" rel="stylesheet" />
    </head>
    
    <body>
        <div>
            <div class="body">
                <div class="inner">
                    <div id="switchBtn" class="status_button off">
                        <div class="button_wrp">
                            <div class="button_mask">
                                <div class="alerter_button" id="powerBtn">
                                    <i class="status_pot"></i>
                                    <span class="on">ON</span>
                                    <span class="off">OFF</span>
                                </div>
                            </div>
                        </div>
                        <div class="on">
                            <h2>灯已开</h2>
                        </div>
                    </div>
                    <div id="reData"></div>
                </div>
            </div>
            <div class="foot">
                <div class="slider_box J_slider_box">
                    <i class="slider_box_icon icon dark"></i>
                    <div id="lightBar" class="slider_box_bar">
                        <div class="slider_box_slider J_slider" style="left:0%">
                            <p class="slider_box_slider_label J_value"></p>
                            <i class="slider_box_slider_touch"></i>
                        </div>
                        <div class="slider_box_line">
                            <span class="slider_box_line_fill J_fill" style="0%"></span>
                        </div>
                    </div>
                    <i class="slider_box_icon icon light"></i>
                </div>
            </div>
        </div>
        <script src="/js/sea.js"></script>
        <script>
            seajs.config({
                base: '/js/',
                //map: [[/^(.*.(?:css|js))(.*)$/i, "$1"]],
                charset: 'utf-8'
            });
            seajs.use("baby");
        </script>
    </body>
    </html>
    View Code

    自己的实现就拿掉了遮罩和config部分,将sea.js的目录改到自己对应的目录即可:

       seajs.config({
                base: '/js/',
                //map: [[/^(.*.(?:css|js))(.*)$/i, "$1"]],
                charset: 'utf-8'
            });
            seajs.use("baby");

    这个baby(命名和产品有关~)就相当于是lamp。 另外就是,修改请求地址。也就是通过后台调用api来实现getdate和setdata。第一版我修改的js和lamp.js的差别不大 就增加了一个log为了调试,修改调用路径。

    define(function (require) {
        var $ = require("common/zepto");
        var util = require("util/util");
        var ProcessBar = require("ui/process-bar");
      
        var requestData = {
            services: {
                lightbulb: { alpha: 10 },
                air_conditioner: {},
                power_switch: {},
                operation_status: { status: 0 }
            },
            device_type: util.getQuery("device_type"),
            device_id: util.getQuery("device_id"),
            user: '',
        };
        var lastModTime = 0;
        var powerBtn = $("#powerBtn"), // 开关按钮
           lightBar;
    
        function log(msg, arg) {
            console.log(msg, arg);
            msg = JSON.stringify(msg);
            if (arg) {
                msg = msg + "," + JSON.stringify(arg);
            }
            $.post('/device/log', { msg: msg });
        }
        (function () {
            bindEvent();
            if (!requestData.device_id || !requestData.device_type) {
                alert("页面缺少参数");
                return;
            }
            powerBtn.on("tap", togglePower); // 开关按钮事件
            initBar();
            queryDevice();
        })();
    
        function bindEvent() {
            $(".footer .nav_side li").click(function () {
                activePage($(this).data("index"), $(this));
            });
        }
        function activePage(index, $self) {
            $self.parent('li').addClass("on");
            $body.find('.page:eq(' + index + ')').addClass("active").siblings().removeClass("active");
        }
        /**
         * 初始化进度条
         */
        function initBar() {
            log("初始化lightBar");
            lightBar = new ProcessBar({
                $id: "lightBar",
                min: 0,
                stepCount: 100,
                step: 1,
                touchEnd: function (val) {
                    requestData.services.lightbulb.alpha = val;
                    log("亮度值为:" + val);
                    setData();
                }
            });
        }
    
        /**
       * 开关按钮事件
       */
        function togglePower() {
            $("#switchBtn").toggleClass("on").toggleClass("off");
            if (requestData.services.operation_status.status == 0) {
                requestData.services.operation_status.status = 1;
                log("灯的状态:1");
    
            } else {
                requestData.services.operation_status.status = 0;
                log("灯的状态:0");
            }
            setData();
        }
        function queryDevice() {
            $.getJSON('/device/RequestDeviceStatus', { reqstr: JSON.stringify(requestData) },
                function (data) {
                    console.log(data);
                    if (data.error_code == 0) {
                        //请求成功;
                        initInterval();
                        console.log("查询成功");
                    } else {
                        alert(data.error_msg);
                    }
                });
        }
    
        /**
       * 轮询
       */
        function initInterval() {
            getData();
            setInterval(function () {
                if ((new Date().getTime() - lastModTime) > 2000) { // 当有设置操作时,停止1s轮询,2秒后继续轮询
                    getData();
                }
            }, 1000);
        }
    
        function setData() {
            $.getJSON('/device/RequestDeviceStatus', { reqstr: JSON.stringify(requestData) }, function (data) {
                console.log(data);
                lastModTime = new Date().getTime();
                if (data.error_code == 0) {
                    console.log("设置成功");
                }
            });
        }
    
        function getData() {
            $.post('/device/getData', function (data) {
                $("#reData").html(JSON.stringify(data));
                if (data && data.services) {
                    renderPage(data);
                }
            });
    
        };
        function renderPage(json) {
            if (!json.services) {
                return;
            }
            console.log("json", json);
            requestData = json;
            if (requestData.services.operation_status.status == 0) {
                $("#switchBtn").addClass("off").removeClass("on");
            } else {
                $("#switchBtn").addClass("on").removeClass("off");
            }
            lightBar.setVal(requestData.services.lightbulb.alpha);
        }
    })
    View Code

     我将pageParam和device_status做成了一个对象。requestData。

        var requestData = {
            services: {
                lightbulb: { alpha: 10 },
               // air_conditioner: {},
                power_switch: {},
                operation_status: { status: 0 }
            },
            device_type: util.getQuery("device_type"),
            device_id: util.getQuery("device_id"),
            user: '',
        };

    后台就是两个主要方法,一个设置(查询页就是设置),一个读取。这里又回到上一节的内容了。我先查询一次设备(lamp中在绑定)之后,再进入循环。

    setdata

    public ActionResult RequestDeviceStatus(string reqstr)
            {
                if (string.IsNullOrEmpty(reqstr))
                {
                    return Json("-1", JsonRequestBehavior.AllowGet);
                }
    
                var args = JsonConvert.DeserializeObject<RequestData>(reqstr);
                args.user = getOpenId(args.device_type, args.device_id);
                Session["warmwood"] = args.device_id;
                //args.services.air_conditioner = null;
                args.services.power_switch = null;
                args.services.lightbulb.value_range = null;
                try
                {
                    var res = wxDeviceService.RequestDeviceStatus(getToken(), args);
                    if (res.error_code != 0)
                    {
                        Logger.Debug("error_code:" + res.error_code);
                        Logger.Debug("error_msg:" + res.error_msg);
                    }
                    return Json(res, JsonRequestBehavior.AllowGet);
                }
                catch (ErrorJsonResultException e)
                {
                    if (e.JsonResult.errcode.ToString() == "access_token expired")
                    {
                        //重新获取token
                    }
                    Logger.Debug("请求失败:" + e.Message);
                }
                return Json("-1", JsonRequestBehavior.AllowGet);
            }

    这个方法先将字符串转成我们的RequestData对象,RequestData如下:

        public class RequestData
        {
            public string device_type { get; set; }
            public string device_id { get; set; }
            public string user { get; set; }
            public Service services { get; set; }
            public object data { get; set; }
        }

    services就是根据微信services定义的,可以参考上一节,然后用wxDeviceService请求。

     var res = wxDeviceService.RequestDeviceStatus(getToken(), args);
                    if (res.error_code != 0)
                    {
                        Logger.Debug("error_code:" + res.error_code);
                        Logger.Debug("error_msg:" + res.error_msg);
                    }
       return Json(res, JsonRequestBehavior.AllowGet);

    设置之后马上会受到是否设置成功的响应,error_code 可能为50019(设置频繁),50013(网络问题)等等。真正的设备状态是通过getdata获得的。

    getdata

            public JsonResult GetData()
            {
                var userdata = getUserWxData();
                return Json(userdata.ResponseData, JsonRequestBehavior.AllowGet);
            }

    getdata比较简单就是返回数据,但是这个数据是在ReceiveWXMsg方法中设置的。这个上一节也讲过,这是在公众号后台我们设置的一个地址。

       public string ReceiveWXMsg()
            {
                //somecode
                try
                {
                    var userdata = getUserWxData();
                    var data = wxDeviceService.GetDeviceStatus(Request);
                    userdata.ResponseData = data;
                    Logger.Debug("ResponseData.asy_error_code:" + userdata.ResponseData.asy_error_code);
                    Logger.Debug("ResponseData.asy_error_msg:" + userdata.ResponseData.asy_error_msg);
                    setUserWxData(userdata);
                }
                catch (Exception e)
                {
                    Logger.Debug(e.Message);
                }
    
    
                return echostr;
            }

    wxDeviceService如下:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Net.Http;
    using System.Web;
    using Newtonsoft.Json;
    using Niqiu.Core.Domain.Common;
    using Senparc.Weixin;
    using Senparc.Weixin.Exceptions;
    using SendHelp= Senparc.Weixin.CommonAPIs.CommonJsonSend;
    
    namespace Portal.MVC.WXDevice
    {
        public class WxDeviceService:IWxDeviceService
        {
            //private readonly ICacheManager _cacheManager;
            //public WxDeviceService(ICacheManager cacheManager)
            //{
            //    _cacheManager = cacheManager;
            //}
    
            public TokenResult GetAccessToken()
            {
                var url = string.Format(WxDeviceConfig.AccessTokenUrl, WxDeviceConfig.AppId, WxDeviceConfig.APPSECRET);
                var res = SendHelp.Send<TokenResult>(null, url, null, CommonJsonSendType.GET);
                return res;
            }
    
            public WxResponseData GetDeviceStatus(HttpRequestBase request)
            {
                Stream postData = request.InputStream;
                StreamReader sRead = new StreamReader(postData);
                string postContent = sRead.ReadToEnd();
                if (!string.IsNullOrEmpty(postContent))
                {
                    Logger.Debug("收到数据:" + postContent);
                }
                try
                {
                    var data = JsonConvert.DeserializeObject<WxResponseData>(postContent);
                    data.rawStr = postContent;
                    Logger.Debug("转换消息状态:" + data.asy_error_msg);
                    return data;
                }
                catch (Exception e)
                {
                    Logger.Debug(e.Message);
                    throw;
                }
            }
    
            public OpenApiResult RequestDeviceStatus(string accessToken, RequestData data)
            {
                var url = string.Format(WxDeviceConfig.GetDeviceStatusUrl, accessToken);
                return SendHelp.Send<OpenApiResult>(accessToken, url, data);
            }
    
            public OpenApiResult SetDevice(string accessToken, RequestData data)
            {
                var url = string.Format(WxDeviceConfig.GetDeviceStatusUrl, accessToken);
                return SendHelp.Send<OpenApiResult>(accessToken, url, data);
            }
    
            public string GetOpenId(string accessToken,string deviceType,string deviceId)
            {
                try
                {
                    var url = string.Format(WxDeviceConfig.GetOpenid, accessToken, deviceType, deviceId);
                    var res = SendHelp.Send<OpenIdResult>(accessToken, url, null, CommonJsonSendType.GET);
                    return res.GetOpenId();
                }
                catch (ErrorJsonResultException e)
                {
                    Logger.Debug(e.Message);
                    throw;
                }
            }
        }
    }
    View Code

    这方法读到数据后就交给了userdata 缓存起来。在getdata方法中返回。

       private UserWxData getUserWxData()
            {
                var target = _cacheManager.Get<UserWxData>(userKey) ?? new UserWxData();
                return target;
            }
    
            private string userKey
            {
                get
                {
                    var key = Session["warmwood"] ?? Session.SessionID;
                    Session.Timeout = 240;
                    return key.ToString();
                }
            }
    View Code

    UserWxData是我自定义的对象,包含了下面的几个熟悉。

        public class UserWxData
        {
            private WxResponseData _responseData;
    
            public UserWxData()
            {
                CreateTime = DateTime.Now;
            }
            public DateTime CreateTime { get; set; }
            public TokenResult AccessToken { get; set; }
    
            public WxResponseData ResponseData
            {
                get { return _responseData??(_responseData=new WxResponseData()); }
                set { _responseData = value; }
            }
    
            public string OpenId { get; set; }
        }

    比较重要的是token和responseData。WxResponseData 也就是最终要发给页面上的对象。包含你需要的功能的参数。

     public class WxResponseData
        {
            public int asy_error_code { get; set; }
    
            public string asy_error_msg { get; set; }
    
            public string create_time { get; set; }
    
            public string msg_id { get; set; }
    
            /// <summary>
            /// notify 说明是设备变更
            /// set_resp 说明是设置设备
            /// get_resp 说明获取设备信息
            /// </summary>
            public string msg_type { get; set; }
    
            public string device_type { get; set; }
            public string device_id { get; set; }
            public object data { get; set; }
    
            public Service services { get; set; }
    
            public string user { get; set; }
    
            public string rawStr { get; set; }
        }

    severices看自己的设备定义,比如我现在包含了空调,开关,温度湿度。

        public class Service
        {
            public lightbulb lightbulb { get; set; }
    
            public air_conditioner air_conditioner { get; set; }
    
            public power_switch power_switch { get; set; }
    
            public operation_status operation_status { get; set; }
    
            public tempe_humidity tempe_humidity { get; set; }
        }

    到这儿,整个过程就讲完了,获取token和openid上一节讲过,就不赘述了。如果后端是node的话,就不需要这么多的类型转换了。

    最后可以看下效果:

     小结:以上就这一篇的全部内容,流程图画可能画的不够好,但这都是摸索出来的结果,微信硬件虽然提供了自定义的链接设置,但是没有提供demo。文中有不对或者不合适的地方欢迎拍砖,欢迎加Q交流。

  • 相关阅读:
    ASP.NET Core中的Action的返回值类型
    ASP.NET Core中的Controller
    ASP.NET Core Authentication and Authorization
    ASP.NET Core
    ASP.NET Core ActionFilter引发的一个EF异常
    使用Github Packages功能上传nuget包到Github
    「每日五分钟,玩转JVM」:线程共享区
    JVM(二):画骨
    Spring Boot 2.x (十八):邮件服务一文打尽
    一道面试题
  • 原文地址:https://www.cnblogs.com/stoneniqiu/p/5840820.html
Copyright © 2011-2022 走看看