微信硬件平台是微信推出连接物与人,物与物的iot解决方案。也就是说可以通过微信控制各种智能设备。比如一些蓝牙设备、空调、电视等等。
我本身不懂硬件(虽然是电子信息专业),硬件是北航的两个研究生在弄,小团队里我负责开发h5自定义面板,刚开始看官方文档各种迷糊,对于jssdk、jsapi、airkiss、openapi、直连sdk都不知道该用哪个做,官方论坛问问题基本上没结果,加了几个微信硬件群问问题,发现好些开发者和我一样,同一个问题,发到几个群里问,画面好心酸。给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来控制设备。
添加完设备,设置好服务器,你还需要硬件的同学打开设备,帮定你的微信。从微信的设置-->设备-->选择设备-->打开面板。你就可以看到设备并进行控制了。
三、调用openapi说了这么多前提工作,终于进入调用api环节。可以先看一下openapi的官方文档:iot.weixin.qq.com/wiki/doc/hardwarecloud/openapi.pdf
文档里面主要讲了三个方法,一个是查询设备状态,一个是设置设备状态,一个是接受设备状态变化的消息,然后是一些错误信息等。但观察api就会发现我们还需要两个重要的参数,一个是access_token,一个是用户的openid。还说明一点,网页是asp.net mvc。
1.获取access_token官方有一个接口调试页面: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来处理请求。
2.获取openid这个参数在查询和设置设备状态的时候会用到,对应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:
对比错误代码列表,我开始以为微信服务器出错了。其实是我参数填错了。
遇到的错误,将不止官方文档公布的这几个。
3.查询设备状态查询设备和上面的这两个方法略有不同,因为流程是这的,我们的服务器先像向微信服务器请求,微信接受到请求后马上返回一个确认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);
}
4.接受消息那么问题来了,如何接受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
5.设置设备状态有了前面几步,这里也好说了。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的详细内容。