微信硬體平台是微信推出連結物與人,物與物的IOT解決方案。也就是說可以透過微信控制各種智慧型設備。例如一些藍牙設備、空調、電視等等。
我本身不懂硬體(雖然是電子資訊專業),硬體是北航的兩個研究生在弄,小團隊裡我負責開發H5自訂面板,剛開始看官方文件各種迷糊,對於jssdk、jsapi、Airkiss、openApi、直連SDK都不知道該用哪個做,官方論壇問問題基本上沒結果,加了幾個微信硬體群問問題,發現好些開發者和我一樣,同一個問題,發到幾個群裡問,畫面好心酸。寄email給wxthings問,能回覆就不錯了,往往還是只言片語。吐槽了這麼多,還是得去解決問題,畢竟設備能搭上微信是一大賣點,最近摸索出來一些東西,於是有了此文。
也就是說,首先你得有一個公眾號,然後開通設備功能、新增產品(也就是你的智慧型設備)。這些過程官方文件比較清楚,我就不講了。接取方案我們選擇的是微信硬體雲端標準接取方案。
而我要說的H5面板開發,指的就是在微信中打開的一個H5控制頁面,它如何和微信硬體雲通信,如何讀取和設定設備的狀態。在新增產品的過程中有一欄設定面板
如果選擇標準面板,微信官方給出了三類標準面板:
分別是空調、開關和燈,如果是自訂,輸入位址即可。如果是標準面板,你是不需要伺服器,但如果是自訂的面板,你就需要有自己的伺服器,不然你無法處理微信雲發過來的訊息。
在設定伺服器位址的時候要注意,你必須按照它要求方式處理回應了,你才能啟用成功。
你點擊啟用的時候,微信雲會發過來一個簽章、一個時間戳、一個隨機數字和一個隨機字串,驗證之後,回傳那個隨機字串,微信雲收到你回傳的隨機字串了,就能啟用成功。例如,如果你定義的位址是http://www.xxx.com/device/ReceiveWXMsg,那麼先把程式碼上傳伺服器,然後再點擊啟用,微信雲會向這個位址post資料。 每一次微信傳送資料給伺服器時,都會先發這驗證(也就是說如果不校驗就回傳會有安全問題)。
public string ReceiveWXMsg() { var signature = Request.QueryString["signature"]; var timestamp = Request.QueryString["timestamp"]; var echostr = Request.QueryString["echostr"]; var nonce = Request.QueryString["nonce"]; Logger.Debug("signature:" + signature); Logger.Debug("timestamp:" + timestamp); Logger.Debug("nonce:" + nonce); Logger.Debug("echostr:" + echostr); //验证 return echostr; }
#View Code
直接傳回就行,不要加個json什麼的。這個地址是幹嘛的呢,往下看。
Wifi设备和蓝牙设备是不同的,蓝牙使用Airsync协议和微信通信,而wifi设备的话,我们在自己的服务器上调用微信提供的openApi获取或设置设备状态,微信服务器就会把数据以json格式post到我们上面设置的那个地址。硬件方面wifi设备可以使用微信提供的直连SDK来控制设备。
添加完设备,设置好服务器,你还需要硬件的同学打开设备,帮定你的微信。从微信的设置-->设备-->选择设备-->打开面板。你就可以看到设备并进行控制了。
说了这么多前提工作,终于进入调用api环节。可以先看一下openApi的官方文档:iot.weixin.qq.com/wiki/doc/hardwarecloud/openapi.pdf
文档里面主要讲了三个方法,一个是查询设备状态,一个是设置设备状态,一个是接受设备状态变化的消息,然后是一些错误信息等。但观察api就会发现我们还需要两个重要的参数,一个是access_token,一个是用户的openid。还说明一点,网页是Asp.net mvc。
官方有一个接口调试页面:mp.weixin.qq.com/debug/ ,获取access_token需要appid和secret。
而这两个值,是在公众号后台的基本配置中查看,secret是个很重要的参数,所以请保密好。
查看密钥还需要扫二维码得到管理员的确认... 拿到这两个参数了,我们就可以生成token了。注意返回的json是一个token字符串再加一个超时时间。定义一个TokenResult:
public class TokenResult { public string access_token { get; set; } public int expires_in { get; set; } }
要注意的一点是,token两小时后会过期。所以在我们的代码里面需要检查是否超时,然后自动重新获取。
public const string AccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
getAccessToken方法定义在一个服务类中,比如WxDeviceService
using SendHelp= Senparc.Weixin.CommonAPIs.CommonJsonSend;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; }
使用Senparc.Weixin框架封装好的api来处理请求。
这个参数在查询和设置设备状态的时候会用到,对应user参数名。获取openid需要三个参数,access_token已经有了,然后需要device_type和device_id
public const string GetOpenid ="https://api.weixin.qq.com/device/get_openid?access_token={0}&device_type={1}&device_id={2}";
而type和id是前端页面传过来的,当用户微信中打开控制面板的时候,微信会自动将这两个参数追加到url后面。而这个url的返回值结构是这样:
{"open_id":["oxa1otw5sk-Azgd8mx1bmBqoM2_E","oxa1ot8-j9j5bYUJJyAexe9d41_Y","oxa1ot5QTdxn0xNQ0DmYzoN0tUp1"],"resp_msg":{"ret_code":0,"error_info":"ok"}}
openid部分是一个数组,实际使用的是第三个(我目前也不知道前面两个id是干啥的),定义一个openidResult:
List<> List<> { _openId??(_openId= List<> { _openId = resp_msg resp_msg { ;
service中:
public string GetOpenId(string accessToken,string deviceType,string deviceId) { var url = string.Format(WxDeviceConfig.GetOpenid, accessToken, deviceType, deviceId); var res = SendHelp.Send<OpenIdResult>(accessToken, url, null, CommonJsonSendType.GET); return res.GetOpenId(); }
如果你的devicetype和deviceId错误,微信会返回一个不太恰当的错误json:
对比错误代码列表,我开始以为微信服务器出错了。其实是我参数填错了。
遇到的错误,将不止官方文档公布的这几个。
查询设备和上面的这两个方法略有不同,因为流程是这的,我们的服务器先像向微信服务器请求,微信接受到请求后马上返回一个确认json,然后微信服务器会马上把数据post我们前面设置的那个地址上。
请求url:
GetDeviceStatusUrl=
从官方文档可以看到,查询设备还需要另外一个重要的参数,它包含device_type和device_id,services,user,data。
需要说明一下的就是services,意思是指设备的能力项,也就是你要查询设备的哪些属性,这个在设置设备的时候一样用到。完成的产品能力定义请看:http://iot.weixin.qq.com/wiki/doc/intro/%E4%BA%A7%E5%93%81%E8%83%BD%E5%8A%9B%E5%AE%9A%E4%B9%89%E6%8C%87%E5%BC%95%20V1.2.pdf
因此定义一个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; } }
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; } }
service包括两个部分,一个是能力部分,好比上面这个service,就包含了三种能力,灯、空调以及开关(这只是测试,不是真正产品的能力)。和一个操作状态。操作状态就是指这个设备是否开着或者关闭了。而每一个能力,又包括两部分,拿灯来说:
public class lightbulb { public int alpha { get; set; } public lightbulb_value_range value_range { get; set; } } public class lightbulb_value_range { public string alpha { get; set; } }
灯有一个亮度值,和一个范围属性。范围值中包含了最大和最小值以及单位值。
"lightbulb":{"alpha":10,"value_range":{"alpha":"0|100|1"}},"
这表示灯的亮度是10,返回是0到100,每次可以调节1个单位。
发送查询请求后,微信返回一个json,定义对象为下:
public class OpenApiResult { public int error_code { get; set; } public string error_msg { get; set; } public string msg_id { get; set; } }
WxDeviceService中:
public OpenApiResult RequestDeviceStatus(string accessToken, RequestData data) { var url = string.Format(WxDeviceConfig.GetDeviceStatusUrl, accessToken); return SendHelp.Send<OpenApiResult>(accessToken, url, data); }
那么问题来了,如何接受post过来的数据,以及如何存储呢。微信post的数据格式如下:
定义了一个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; } }
View Code
msg_type代表着不同类型的消息, notify 说明是设备变更,set_resp 说明是设置设备 get_resp 说明获取设备信息。在WxDeviceService中增加GetDeviceStatus方法:
public T GetWxResponse<T>(HttpRequestBase request) { Stream postData = request.InputStream; StreamReader sRead = new StreamReader(postData); string postContent = sRead.ReadToEnd(); if (!string.IsNullOrEmpty(postContent)) { Logger.Debug("收到数据:"+postContent); } try { return JsonConvert.DeserializeObject<T>(postContent); } catch (Exception e) { Logger.Debug(e.Message); throw; } } public WxResponseData GetDeviceStatus(HttpRequestBase request) { return GetWxResponse<WxResponseData>(request); }
需要先读取请求中的json字符串然后转换成C#对象。然后在最初启用的ReceiveWXMsg方法中随时准备接受消息:
public string ReceiveWXMsg() { var signature = Request.QueryString["signature"]; var timestamp = Request.QueryString["timestamp"]; var echostr = Request.QueryString["echostr"]; var nonce = Request.QueryString["nonce"]; Logger.Debug("signature:" + signature); Logger.Debug("timestamp:" + timestamp); Logger.Debug("nonce:" + nonce); Logger.Debug("echostr:" + echostr); try { var userdata = getUserWxData(); var data = wxDeviceService.GetDeviceStatus(Request); userdata.ResponseData = data; setUserWxData(userdata); } catch (Exception e) { Logger.Debug(e.Message); } return echostr; }
因为读取到的数据需要及时呈现给页面,所以这里选用了缓存来存储设备信息以及用户相关信息。
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; } }
View Code
private UserWxData getUserWxData() { var target = _cacheManager.Get<UserWxData>(userKey) ?? new UserWxData(); return target; } private string userKey { get { return Session.SessionID; } } private void setUserWxData(UserWxData data) { _cacheManager.Set(userKey, data, 7200); }
View Code
缓存是Nop中的MemoryCacheManager:
using System;using System.Collections.Generic;using System.Runtime.Caching;using System.Text.RegularExpressions;namespace Niqiu.Core.Domain.Common { /// <summary> /// Represents a manager for caching between HTTP requests (long term caching) /// </summary> public partial class MemoryCacheManager : ICacheManager { protected ObjectCache Cache { get { return MemoryCache.Default; } } /// <summary> /// Gets or sets the value associated with the specified key. /// </summary> /// <typeparam name="T">Type</typeparam> /// <param name="key">The key of the value to get.</param> /// <returns>The value associated with the specified key.</returns> public virtual T Get<T>(string key) { return (T)Cache[key]; } /// <summary> /// Adds the specified key and object to the cache. /// </summary> /// <param name="key">key</param> /// <param name="data">Data</param> /// <param name="cacheTime">Cache time</param> public virtual void Set(string key, object data, int cacheTime) { if (data == null) return; var policy = new CacheItemPolicy {AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime)}; Cache.Add(new CacheItem(key, data), policy); } /// <summary> /// Gets a value indicating whether the value associated with the specified key is cached /// </summary> /// <param name="key">key</param> /// <returns>Result</returns> public virtual bool IsSet(string key) { return (Cache.Contains(key)); } /// <summary> /// Removes the value with the specified key from the cache /// </summary> /// <param name="key">/key</param> public virtual void Remove(string key) { Cache.Remove(key); } /// <summary> /// Removes items by pattern /// </summary> /// <param name="pattern">pattern</param> public virtual void RemoveByPattern(string pattern) { var regex = new Regex(pattern, RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase); var keysToRemove = new List<String>(); foreach (var item in Cache) if (regex.IsMatch(item.Key)) keysToRemove.Add(item.Key); foreach (string key in keysToRemove) { Remove(key); } } /// <summary> /// Clear all cache data /// </summary> public virtual void Clear() { foreach (var item in Cache) Remove(item.Key); } } }
View Code
而为什么不是PerRequestCacheManager呢,想一想~
using System;using System.Collections;using System.Collections.Generic;using System.Text.RegularExpressions;using System.Web;namespace Niqiu.Core.Domain.Common { /// <summary> /// Represents a manager for caching during an HTTP request (short term caching) /// </summary> public partial class PerRequestCacheManager : ICacheManager { /// <summary> /// Ctor /// </summary> /// <param name="context">Context</param> //public PerRequestCacheManager(HttpContextBase context) //{ // this._context = context; //} /// <summary> /// Creates a new instance of the NopRequestCache class /// </summary> protected virtual IDictionary GetItems() { if (_context != null) return _context.Items; return null; } //不用注入 private HttpContextBase _context { get { return new HttpContextWrapper(HttpContext.Current); } } /// <summary> /// Gets or sets the value associated with the specified key. /// </summary> /// <typeparam name="T">Type</typeparam> /// <param name="key">The key of the value to get.</param> /// <returns>The value associated with the specified key.</returns> public virtual T Get<T>(string key) { var items = GetItems(); if (items == null) return default(T); return (T)items[key]; } /// <summary> /// Adds the specified key and object to the cache. /// </summary> /// <param name="key">key</param> /// <param name="data">Data</param> /// <param name="cacheTime">Cache time</param> public virtual void Set(string key, object data, int cacheTime) { var items = GetItems(); if (items == null) return; if (data != null) { if (items.Contains(key)) items[key] = data; else items.Add(key, data); } } /// <summary> /// Gets a value indicating whether the value associated with the specified key is cached /// </summary> /// <param name="key">key</param> /// <returns>Result</returns> public virtual bool IsSet(string key) { var items = GetItems(); if (items == null) return false; return (items[key] != null); } /// <summary> /// Removes the value with the specified key from the cache /// </summary> /// <param name="key">/key</param> public virtual void Remove(string key) { var items = GetItems(); if (items == null) return; items.Remove(key); } /// <summary> /// Removes items by pattern /// </summary> /// <param name="pattern">pattern</param> public virtual void RemoveByPattern(string pattern) { var items = GetItems(); if (items == null) return; var enumerator = items.GetEnumerator(); var regex = new Regex(pattern, RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase); var keysToRemove = new List<String>(); while (enumerator.MoveNext()) { if (regex.IsMatch(enumerator.Key.ToString())) { keysToRemove.Add(enumerator.Key.ToString()); } } foreach (string key in keysToRemove) { items.Remove(key); } } /// <summary> /// Clear all cache data /// </summary> public virtual void Clear() { var items = GetItems(); if (items == null) return; var enumerator = items.GetEnumerator(); var keysToRemove = new List<String>(); while (enumerator.MoveNext()) { keysToRemove.Add(enumerator.Key.ToString()); } foreach (string key in keysToRemove) { items.Remove(key); } } } }
View Code
有了前面几步,这里也好说了。url:
SetDeviceUrl =
设置设备的参数和请求是一样的,
public OpenApiResult SetDevice(string accessToken, RequestData data) { var url = string.Format(WxDeviceConfig.SetDeviceUrl, accessToken); return SendHelp.Send<OpenApiResult>(accessToken, url, data); }
View Code
调用openApi基本上就这样了,如有不完善的地方还请指正。 这个方法有权限的问题,可以用查询方法代替,同样可以改变设备状态。不知道这api是个什么鬼。
如果硬件通信没有开启这个能力,去查询的会报这个错误。
刚开始看到"device not login"实在没明白什么意思,文档里也没说明这个错误。设备还需要什么登录?原来是硬件同学没有连接设备... ORZ
如果你的requestData结构不对,特别是附加的那个Data参数只能是字符串,不要写成空对象{},就会出现这个错误。
token超时
同一个设备同时被操作。
【相关推荐】
1. 微信公众号平台源码下载
2. 微信投票源码下载
以上是微信H5開發 調用openApi的詳細內容。更多資訊請關注PHP中文網其他相關文章!