微信公众号硬件开发杂谈

最近帮朋友研究一个单片机的项目,简单接触了一下微信公众号的硬件平台,遇到很多问题,简单记录一下

该怎么连接

准备工作

首先不管用什么接口,做硬件和软件的交互一般还是先想着怎么去做一个基础的连接操作,最开始查到一些教程并参考微信官方的说法就是先申请设备接入的权限,以测试号来说,申请非常简单,如图所示

微信公众号硬件开发杂谈_第1张图片

申请完成后,点击【设置】进入设备管理页面,按照提示一步步操作就可以添加设备,我这边是使用的是第三方的硬件设备,通过wifi进行连接,根据相关文档进行如下设置,具体选项内容应该要根据硬件设备类型来定。

微信公众号硬件开发杂谈_第2张图片

经过简单的设置就可以生成一个设备,可以在设备列表看到,另外提一下,似乎一直都存在一个意义不明的undefined设备,应该是平台开发人员保留的,不影响使用

微信公众号硬件开发杂谈_第3张图片

常规方法

通过这种方式新建的设备默认拥有100个配额,代表能授权100个可以控制的设备,我这里的截图中是已经授权了一个设备,授权需要通过接口发送请求,具体接口内容在后面详述。之后开始说第一次连接这个重要的问题,但是我这边并不方便验证结果,在这里说下最后的实践结果。一些教程会提到通过扫描产品详情里的二维码就可以执行连接操作,但对于普通测试情况并不符合。以下是通常情况的操作流程:

  • 在产品配置中连接方式选择wifi
  • 进行设备授权(不确定是否必要)
  • 扫描产品详情中的二维码会调起Airkiss页面(现在先简单理解成一个输入你当前连接wifi的密码的页面)
  • 打开硬件设备的AirLink模式,并在手机上输入wifi密码进行连接
  • 连接成功后,会跳转至一个搜索设备的页面

微信公众号硬件开发杂谈_第4张图片

微信公众号硬件开发杂谈_第5张图片

微信公众号硬件开发杂谈_第6张图片

问题就出在最后一步,我在这里进行了反复的测试,前置的配置操作重复了很多遍,还换过开发板,在这里都是搜索不到设备,经过大量的查找资料发现如果想通过这种方式扫描到设备必须在设备芯片的固件中写入公众号的ID,并且我后来也在设备详情中发现了相关依据

虽然找到了问题,但是固件的编写是个很大的问题,简单的烧录可以做到,但修改编译固件的代码却是不懂,所幸后面发现了另一种方法,但是由于最终使用的固件版本是第三方平台机智云的,所以无法验证其他的情况是否可以,这里主要是记录一下开发的过程。

JS-SDK

上面介绍的方法在扫码后就会自动跳出一个页面,这个是微信官方提供的,如果不想使用这个页面的话就可以通过JS-SDK的方式。

在进行下一步操作之前我们还要了解一下这个跳出的页面是什么,官方的描述如下:

Airkiss是微信硬件平台为Wi-Fi设备提供的微信配网、局域网发现和局域网通讯的技术。开发者若要实现通过微信客户端对Wi-Fi设备配网、通过微信客户端在局域网发现Wi-Fi设备,或者把微信客户端内的音乐、图片、文件等消息通过局域网发送至Wi-Fi设备,需要在硬件设备中集成相应的AirKiss静态库。

通过这句话我们可以知道要想在局域网内发现设备就可以通过这一方法,更详细的说明可以看这个页面
AirKiss概述及应用场景

根据Airkiss2.0的文档我们可以知道通过JSAPI可以进行配网,以达到我们的目的。首先在Web层引用微信JSAPI,JSAPI技术是微信JS-SDK的一部分,,主要是通过 wx.readywx.config 这两个函数调用官方的接口,具体说明参考
JSAPI介绍

使用Airkiss文档里的configWXDeviceWiFi这个方法就可以进行自定义的AirKiss连接,这里我使用的框架是ASP.NET MVC,所以可以看到AppID、timestamp、nonceStr、signature这几个参数是从后台传入的,具体的后台生成代码放在下面参考,有些参数需要自行传入,注意修改。



C#后台代码参考

public class AirkissHelper
{
    public static string appID = BaseConfig.appID;
    public static string appsecret = BaseConfig.appsecret;

    class TokenResultMessage // 封装调用access_token接口返回的数据
    {
        public string access_token;
        public string expires_in;
    }

    class JsapiResultMessage // 封装调用jsapi_ticket接口返回的数据
    {
        public string errcode;
        public string errmsg;
        public string ticket;
        public string expires_in;
    }


    /// 
    /// Get请求封装
    /// 
    /// 
    /// 
    private static string GetWebUrl(string url)
    {
        WebClient client = new WebClient(); // 创建浏览器
        Stream stream = client.OpenRead(url); // 传入url地址
        return new StreamReader(stream).ReadToEnd(); // 得到响应字符串
    }

    /// 
    /// sha1签名算法
    /// 
    /// 
    /// 
    private static string Sha1(string str)
    {
        var sha1 = System.Security.Cryptography.SHA1.Create();
        byte[] bytes = Encoding.UTF8.GetBytes(str);
        byte[] bytesArr = sha1.ComputeHash(bytes);
        StringBuilder sb = new StringBuilder();
        foreach (var item in bytesArr)
        {
            sb.AppendFormat("{0:x2}", item);
        }
        return sb.ToString();
    }

    /// 
    /// 生成时间戳
    /// 
    public static string GetTimeStamp()
    {
        TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1);
        return Convert.ToInt64(ts.TotalSeconds).ToString();
    }

    /// 
    /// 生成32位随机字符串
    /// 
    public static string GetRandomStr()
    {
        string strArr = "0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm";
        Random rand = new Random();

        string randStr = "";
        for (int i = 0; i < 32; i++)
        {
            int index = rand.Next(strArr.Length);
            randStr += strArr.Substring(index, 1);
        }
        return randStr;
    }

    /// 
    /// 生成签名
    /// 
    public static string GetSignatur(string timestamp,string nonceStr)
    {
        // 通过API获取access_token
        string tokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appID + "&secret=" + appsecret;
        var tokenInfo = new JavaScriptSerializer().Deserialize(GetWebUrl(tokenUrl));

        // 通过API获取jsapi_ticket
        string jsapiUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + tokenInfo.access_token + "&type=jsapi";
        var jsapiInfo = new JavaScriptSerializer().Deserialize(GetWebUrl(jsapiUrl));

        // 当前网页的URL,不包含#及其后面部分
        string nowUrl = BaseConfig.nowUrl;
        //string nowUrl = Request.Url.AbsoluteUri; // 部署服务器后应动态获取页面url

        // 用Dictionary集合存储各变量
        Dictionary dic = new Dictionary
        {
            ["timestamp"] = timestamp,
            ["noncestr"] = nonceStr,
            ["url"] = nowUrl,
            ["jsapi_ticket"] = jsapiInfo.ticket
        };

        // 对变量名字典排序
        string[] arrKey = new string[] { "timestamp", "noncestr", "url", "jsapi_ticket" };
        arrKey = arrKey.OrderBy(n => n).ToArray();

        // 拼接字符串
        string signatureStr = "";
        foreach (var item in arrKey)
        {
            signatureStr += item + "=" + dic[item] + "&";
        }
        signatureStr = signatureStr.Substring(0, signatureStr.Length - 1);

        // 对拼接串sh1签名,得到最终签名
        return Sha1(signatureStr);
    }
}

做好配置部分的代码后,在页面上使用 wx.invoke(‘configWXDeviceWiFi’); 就可以手动调起Airkiss页面,输入wifi密码后手动连接,根据wx.config里面的debug配置会返回提示消息。

设备授权与状态查询

在上一篇中我们说到要使用设备首先要进行授权操作,设备完成授权之后就可以通过接口来做一些操作,授权接口的具体参数官方说明看这个地址
设备授权

入参中的AccessToken如果不明白的要看下微信开发的基础部分,参数body中id为deviceid,调试的时候mac地址和id都要向硬件厂商索要,authkey这些参数先可以自行填写,等有了再填入正确的。“connect_protocol”这个参数填4,因为咱们进行的是wifi设备开发,close_strategy这个是断开策略,请注意。下面auth_ver选0,不加密,与上面对应,op_type这个参数注意,为0时设备授权,为1时设备更新,完成后检查问题,返回正确参数deviceid和device_type,除了返回正确的入参,在公众号的产品列表中也可以看到授权配额-1,这个过程是不可逆的,已经授权的配额就不能再取消,但是可以通过“设备状态查询”接口查询到三种状态(未授权、已授权、已绑定)。

如果没有设备想模拟在线,可以使用设备授权新接口,还会返回了qrticket,把这个字符串放到二维码生成器(随便搜索一下就有)中,会返回一个二维码,扫码即可绑定设备,然后再通过 第三方主动发送设备状态消息给微信终端 模拟状态

{
    "device_type": "公众号原始ID",
    "device_id": "XXX",
    "open_id": "XXX",
    "msg_type": " 2",
    "device_status": " 1"
}

测试公众号可以在页面右上角看到原始ID,device_id和open_id都是基础参数,状态这里填“1”就是在线状态,接口请求成功就会在公众号的名称下面看到一个“已连接”的状态。

HTTP请求帮助类

在调试过程中我们可以使用微信的接口调试工具或者Postman进行接口测试,但在产品实际应用中还是要实现HTTP请求的代码,这里贴一下帮助类的示例

/// 
/// 获取html
/// 
/// url
/// encoding
/// HttpResult
HttpResult _Get(string url, Encoding encoding)
{
    HttpResult result = new HttpResult();
    try
    {
        var response = HttpClient.GetAsync(url);
        result.result = true;
        result.html = response.Result.Content.ReadAsStringAsync().Result; // .Content.ReadAsStringAsync();
    }
    catch (Exception ex)
    {
        result.result = false;
        result.html = "转发接口失败," + ex.Message;
    }

    return result;
}

/// 
/// 获取html
/// 
/// 
/// 
/// 
/// 
/// 
private HttpResult _Post(string url, Dictionary paramss, Encoding encoding, string jsonstring = "")
{
    HttpResult r = new HttpResult();
    HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
    req.Proxy = null;
    try
    {
        string param = string.Empty;
        if (string.IsNullOrWhiteSpace(jsonstring))
        {
            foreach (string p in paramss.Keys)
            {
                param += p + "=" + paramss[p] + "&";
            }

            req.ContentType = "application/x-www-form-urlencoded";
        }
        else
        {
            param = jsonstring;
            req.ContentType = "application/json";
        }

        foreach (var headerParam in paramss)
        {
            req.Headers.Add(headerParam.Key, headerParam.Value);
        }

        byte[] bs = Encoding.UTF8.GetBytes(param);
        string responseData = string.Empty;
        req.Method = "POST";
        req.ContentLength = bs.Length;
        using (Stream reqStream = req.GetRequestStream())
        {
            reqStream.Write(bs, 0, bs.Length);
            reqStream.Close();
        }

        using (HttpWebResponse response = (HttpWebResponse)req.GetResponse())
        {
            using (StreamReader reader = new StreamReader(response.GetResponseStream(), encoding))
            {
                responseData = reader.ReadToEnd().ToString();
            }

            r.result = true;
            r.html = responseData;
        }
    }
    catch (Exception e)
    {
        r.html = e.ToString();
        if (req != null)
        {
            req.Abort();
        }

        return r;
    }

    return r;
}

public class HttpResult
{
    public bool result;
    public Task htmlasync;
    public string html;
    public int stateCode;
}

Get请求结构较为简单,需要注意的是Post请求中,根据接口的不同,参数会出现不同的位置,在url、header和body中均有可能需要传参。

WebSocket

以前知道有这么个东西,但是具体有什么作用,特别在哪是不太清楚,或者为什么要用这个东西,但通过这次接触微信硬件的开发大概理解了一点,首先要明确WebSocket是一种与HTTP不同的协议,引用维基百科的说明如下:

服务器可以通过标准化的方式来实现,而无需客户端首先请求内容,并允许消息在保持连接打开的同时来回传递。通过这种方式,可以在客户端和服务器之间进行双向持续对话。

简单来说他可以让客户端和服务器之间保持一个低耗的长连接,那么这个和硬件开发有什么关系呢?

最初我只是知道智能家居通过wifi远程控制,所以想单片机应该也可以通过同样的原理实现,利用一个业务服务器接受用户的操作,然后服务器发送请求给家里的路由器,再由路由器转发给家里的设备,既然是这样,就转化成了我说属性的HTTP请求,中间的传输无非就是POST请求发送json格式的数据。

想来是挺简单的,但是虽然开发的深入,发现硬件设备是一个发信器,它只向外请求并接收返回的结果,但是当路由器想发送接收自客户端的设备状态数据时却不知道发送给谁,这样引起的一个情况是如果要想实现实时的设备控制,需要设备不停地向服务器发送GET请求来获取当前的状态,这样就形成的极大的浪费,这样的轮询过程中大部分的数据可能都是无效的,比如请求头中的一些必要条件,但实际要传输的状态数据往往就只是一个字符串。

在WebSocket出现之前,也确实都是这样做的,但自从有了WebSocket,硬件设备就可以和服务器之间建立一个长连接,这也就是我们在前面所实现“上线”操作,实际上就是打开这一长连接。

后记

这次做硬件的研究断断续续持续了很久,其中真的是遇到了相当多的问题,尤其是在不了解的领域中,但是随着问题一个个被克服,有让我感受到了最初的那种解决问题的快乐,因为问题比较零碎,而且自己的理解还不到位,所以其中很多问题不能形成段落记录下来比较可惜,但是整个开发的过程和结果都是相当令人难忘的。

你可能感兴趣的:(微信公众号硬件开发杂谈)