1. 前言
上一篇文章了关于微信蓝牙外设的调试过程中,微信蓝牙外设与微信小程序之间进行通讯。这篇文章将记录的是Android与微信蓝牙外设,通过微信蓝牙外设协议中的数据透传通道,如何与单片机端自定义通讯。
2. 微信蓝牙外设
关于微信蓝牙外设的一下相关的,可以请移步到我前两篇文章。
由于protobuf 是谷歌开发的开源项目,而Android 也是Google 亲儿子。因此,在Android 上,使用Google 的protobuf 使用非常简单和方便。protobuf在android还推荐一种使用方式为protobuf-lite,使用protobuf gradle plugin在构建时生成代码的方式来使用protobuf。
3. Android 使用protobuf
3.1 开发坏境
Android Studio
3.2 微信蓝牙外设 proto 文件
syntax = "proto2";
enum EmCmdId
{
ECI_none = 0;
// req: 蓝牙设备 -> 微信/厂商服务器
ECI_req_auth = 10001; // 登录
ECI_req_sendData = 10002; // 蓝牙设备发送数据给微信或厂商
ECI_req_init = 10003; // 初始化
// resp:微信/厂商服务器 -> 蓝牙设备
ECI_resp_auth = 20001;
ECI_resp_sendData = 20002;
ECI_resp_init = 20003;
// push:微信/厂商服务器 -> 蓝牙设备
ECI_push_recvData = 30001; // 微信或厂商发送数据给蓝牙设备
ECI_push_switchView = 30002; // 进入/退出界面
ECI_push_switchBackgroud = 30003; // 切换后台
ECI_err_decode = 29999; // 解密失败的错误码。注意:这不是 cmdid。为节省固定包头大小,这种特殊的错误码放在包头的 cmdid 字段。
}
enum EmErrorCode
{
EEC_system = -1; // 通用的错误
EEC_needAuth = -2; // 设备未登录
EEC_sessionTimeout = -3; // session 超时,需要重新登录
EEC_decode = -4; // proto 解码失败
EEC_deviceIsBlock = -5; // 设备出现异常,导致被微信临时性禁止登录
EEC_serviceUnAvalibleInBackground = -6; // ios 处于后台模式,无法正常服务
EEC_deviceProtoVersionNeedUpdate = -7; // 设备的 proto 版本过老,需要更新
EEC_phoneProtoVersionNeedUpdate = -8; // 微信客户端的 proto 版本过老,需要更新
EEC_maxReqInQueue = -9; // 设备发送了多个请求,并且没有收到回包。微信客户端请求队列拥塞。
EEC_userExitWxAccount = -10; // 用户退出微信帐号。
}
message BaseRequest
{
}
message BaseResponse
{
required int32 ErrCode = 1;
optional string ErrMsg = 2;
}
message BasePush
{
}
// req, resp ========================================
enum EmAuthMethod
{
EAM_md5 = 1; // 设备通过 Md5DeviceTypeAndDeviceId,来通过微信 app 的认证。1. 如果是用 aes 加密,注意设置 AesSign 有值。 2. 如果是没有加密,注意设置 AesSign 为空或者长度为零。
EAM_macNoEncrypt = 2; // 设备通过 mac 地址字段,且没有加密,来通过微信 app 的认证。
}
// 登录 ---------------------------------------------
message AuthRequest
{
required BaseRequest BaseRequest = 1;
optional bytes Md5DeviceTypeAndDeviceId = 2; // deviceType 加 deviceId 的 md5,16 字节的二进制数据
required int32 ProtoVersion = 3; // 设备支持的本 proto 文件的版本号,第一个字节表示最小版本,第二个字节表示小版本,第三字节表示大版本。版本号为 1.0.0 的话,应该填:0x010000;1.2.3 的话,填成 0x010203。
required int32 AuthProto = 4; // 填 1
required EmAuthMethod AuthMethod = 5; // 验证和加密的方法,见 EmAuthMethod
optional bytes AesSign = 6; // 具体生成方法见文档
optional bytes MacAddress = 7; // mac 地址,6 位。当设备没有烧 deviceId 的时候,可使用该 mac 地址字段来通过微信 app 的认证
optional string TimeZone = 10; // 废弃
optional string Language = 11; // 废弃
optional string DeviceName = 12; // 废弃
}
message AuthResponse
{
required BaseResponse BaseResponse = 1;
required bytes AesSessionKey = 2;
}
// 初始化 --------------------------------------------
enum EmInitRespFieldFilter
{
EIRFF_userNickName = 0x1;
EIRFF_platformType = 0x2;
EIRFF_model = 0x4;
EIRFF_os = 0x8;
EIRFF_time = 0x10;
EIRFF_timeZone = 0x20;
EIRFF_timeString = 0x40;
}
// 微信连接上设备时,处于什么情景
enum EmInitScence
{
EIS_deviceChat = 1; // 聊天
EIS_autoSync = 2; // 自动同步
}
message InitRequest
{
required BaseRequest BaseRequest = 1;
optional bytes RespFieldFilter = 2; // 当一个 bit 被设置就表示要 resp 的某个字段:见EmInitRespFieldFilter。
optional bytes Challenge = 3; // 设备用来验证手机是否安全。为设备随机生成的四个字节。
}
enum EmPlatformType
{
EPT_ios = 1;
EPT_andriod = 2;
EPT_wp = 3;
EPT_s60v3 = 4;
EPT_s60v5 = 5;
EPT_s40 = 6;
EPT_bb = 7;
}
message InitResponse
{
required BaseResponse BaseResponse = 1;
required uint32 UserIdHigh = 2; // 微信用户 Id 高 32 位
required uint32 UserIdLow = 3; // 微信用户 Id 低 32 位
optional uint32 ChalleangeAnswer = 4; // 手机回复设备的挑战。为设备生成的字节的 crc32。
optional EmInitScence InitScence = 5; // 微信连接上设备时,处于什么情景。如果该字段为空,表示处于 EIS_deviceChat 下。
optional uint32 AutoSyncMaxDurationSecond = 6; // 自动同步最多持续多长,微信就会关闭连接。0xffffffff 表示无限长。
optional string UserNickName = 11; // 微信用户昵称
optional EmPlatformType PlatformType = 12; // 手机平台
optional string Model = 13; // 手机硬件型号
optional string Os = 14; // 手机 os 版本
optional int32 Time = 15; // 手机当前时间
optional int32 TimeZone = 16; // 手机当前时区
optional string TimeString = 17; // 手机当前时间,格式如 201402281005285,具体字段意义为 2014(年)02(2 月)28(28 号)10(点)05(分钟)28(秒)5(星期五)。星期一为 1,星期天为 7。
}
// 设备发送数据给微信或厂商 ----------------------------
// 设备数据类型
enum EmDeviceDataType
{
EDDT_manufatureSvr = 0; // 厂商自定义数据
EDDT_wxWristBand = 1; // 微信公众平台手环数据
EDDT_wxDeviceHtmlChatView = 10001; // 微信客户端设备 html5 会话界面数据
}
message SendDataRequest
{
required BaseRequest BaseRequest = 1;
required bytes Data = 2;
optional EmDeviceDataType Type = 3; // 数据类型(如厂商自定义数据,或公众平台规定的手环数据,或微信客户端设备 html5 会话界面数据等)。不填,或者等于 0 的时候,表示设备发送厂商自定义数据到厂商服务器。
}
message SendDataResponse
{
required BaseResponse BaseResponse = 1;
optional bytes Data = 2;
}
// push ===================================================
// 微信或厂商发送数据给蓝牙设备 ---------------------------
message RecvDataPush
{
required BasePush BasePush = 1;
required bytes Data = 2;
optional EmDeviceDataType Type = 3; // 数据类型(如厂商自定义数据,或公众平台规定的手环数据,或微信客户端设备 html5 会话界面数据等)。不填,或者等于 0 的时候,表示设备收到厂商自定义数据。
}
3.4 在Android studio 项目上集成protobuf
关于protobuf-gradle-plugin的更多用法可参考官方文档 https://github.com/google/protobuf-gradle-plugin
3.4.1 添加protobuf-gradle-plugin
在项目的根build.gradle文件中增加如下代码,
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3'
}
}
3.4.2 在app 项目中引用protobuf-gradle-plugin
在app 项目中的build.gradle 文件中增加如下代码
apply plugin: 'com.google.protobuf'//声明插件
...
//编写编译任务,调用plugin编译生成java文件
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.0.0' //编译器版本
}
plugins {
javalite {
artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' //指定当前工程使用的protobuf版本为javalite版,以生成javalite版的java类
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
javalite {}
}
}
}
}
//指定原始.proto文件的位置
android {
sourceSets {
main {
java {
srcDirs 'src/main/java'
}
proto {
srcDirs 'src/main/proto'
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 定义protobuf依赖,使用精简版
implementation "com.google.protobuf:protobuf-lite:3.0.0"
implementation ('com.squareup.retrofit2:converter-protobuf:2.2.0') {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
}
3.4.3 执行Android Stdio 菜单中的build -> Rebuild Project
执行完毕后,会出现如下错误
ERROR: SSL peer shut down incorrectly
出现这个原因可能 sync gradle无法同步
修改问题,
修改gradle/wrapper/gradle-wrapper.properties下内容,
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
改为:
distributionUrl=http://services.gradle.org/distributions/gradle-5.1.1-all.zip
再次点击菜单Buidl ->Rebuild Project
则会出现如下问题,
查看错误原因,可以发现是在根build.gradle 中配置的Protobuf Gradle 插件版本太低了
根据提示,修改插件的对应支持的版本,再次Rebuild
出现这个,则编译成功。
synced successfully 5 s 260 ms
Run build 4 s 159 ms
Load build 122 ms
Configure build 261 ms
Build parameterized model 'com.android.builder.model.AndroidProject' for project ':app' 19 ms
Build parameterized model 'com.android.builder.model.NativeAndroidProject' for project ':app' 3 ms
Build parameterized model 'com.android.builder.model.Variant' for project ':app' 1 s 71 ms
null
E:/Android/wxProtobuf
3.4.4 添加proto文件目录和编写.proto 文件
根据上面的配置,需要在app 工程的build.gradle 配置,需要在app工程的main/src/下新建proto 目录,然后在目录下新建和编写.proto 文件
再次,Rebuild Project ,又出现错误,提示某个目录缺失,o(╥﹏╥) oo(╥﹏╥)o
Directory 'D:\working porject\Android\wxProtobuf\app\build\extracted-include-protos\main' specified for property '$3' does not exist.
翻遍网络,发现别人配置Android Studio 的 protobuf plugin 很简单,到我这里就很奇怪,这么多问题,而且这个问题,还没有人遇过,o(╥﹏╥)o。
后面去protobuf 的gitub 官网上,再次看了一下,发现自己遗漏了一些信息,
o(╥﹏╥)o,在前面,我根据提示,配置成 0.8.6 ,并没有去官网自己看
The latest version is 0.8.10. It requires at least Gradle 3.0 and Java 8.
看了一下自己Project 中,app 的build.gradle 中的配置,
配的是3.5.0 ,于是修改一下protobuf 插件版本,根据官网提示,修改修改0.8.10
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
再次尝试Rebuild Project,终于成功了,O(∩_∩)O哈哈~
3.4.5 编译成功后,可以发现生成.proto 文件对应的.java文件
编译完成,可以开始操作protobuf 了。
4. 微信蓝牙外设协议操作
相关代码如下:
package com.chen.wxprotobuf.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.icu.util.LocaleData;
import android.os.Bundle;
import android.text.LoginFilter;
import android.util.Log;
import android.view.View;
import com.chen.wxprotobuf.R;
import com.chen.wxprotobuf.protobuf.wxPorotcolBuffers;
import com.chen.wxprotobuf.util.Utils;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_text).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v)
{
recvDataPushTest();
}
});
}
private void authResponseTest()
{
try
{
wxPorotcolBuffers.AuthResponse.Builder authResponseBuildrer = wxPorotcolBuffers.AuthResponse.newBuilder();
wxPorotcolBuffers.BaseResponse.Builder baseResponseBuilder = wxPorotcolBuffers.BaseResponse.newBuilder();
baseResponseBuilder.setErrCode(0);
baseResponseBuilder.setErrMsg("OK");
authResponseBuildrer.setAesSessionKey(ByteString.EMPTY);
authResponseBuildrer.setBaseResponse(baseResponseBuilder);
wxPorotcolBuffers.AuthResponse authResponse = authResponseBuildrer.build();
byte[] serialized = authResponse.toByteArray();
Log.d(TAG, Utils.byteArrayToHexString(serialized, 0, serialized.length));
Log.d(TAG, "serialized length = " + serialized.length);
Log.d(TAG, "serialized size = " + authResponse.getSerializedSize());
wxPorotcolBuffers.AuthResponse authResponse1 = wxPorotcolBuffers.AuthResponse.parseFrom(serialized);
Log.d(TAG, "base error code = " + authResponse1.getBaseResponse().getErrCode());
Log.d(TAG, "base error msg = " + authResponse1.getBaseResponse().getErrMsg());
Log.d(TAG, "aes session key = " + authResponse1.getAesSessionKey());
String data = "0A 02 08 00 12 00".replace(" ", "");
Log.d(TAG, wxPorotcolBuffers.AuthResponse.parseFrom(Utils.hexStringToBytes(data)).toString());
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void authRequset()
{
String data = "0A 00 18 84 80 04 20 01 28 02 3A 06 F3 3F 31 F3 FF 3F".replace(" ", "");
//wxPorotcolBuffers.AuthRequest.Builder authRequsetBuilder = wxPorotcolBuffers.AuthRequest.newBuilder();
try
{
wxPorotcolBuffers.AuthRequest authRequest = wxPorotcolBuffers.AuthRequest.parseFrom(Utils.hexStringToBytes(data));
Log.d(TAG, "auth requset = " + authRequest.toString());
ByteString macAddress = authRequest.getMacAddress();
Log.d(TAG, "mac address = " + Utils.byteArrayToHexString(macAddress.toByteArray(), 0, macAddress.toByteArray().length));
} catch (InvalidProtocolBufferException e)
{
e.printStackTrace();
}
}
private void initResqusetTest()
{
try
{
String data = "0A 00 1A 10 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F".replace(" ", "");
wxPorotcolBuffers.InitRequest initRequest = wxPorotcolBuffers.InitRequest.parseFrom(Utils.hexStringToBytes(data));
Log.d(TAG, "init request = " + initRequest.toString());
Log.d(TAG, "challenge " + Utils.byteArrayToHexString(initRequest.getChallenge().toByteArray(), 0, initRequest.getChallenge().toByteArray().length));
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void initResponse()
{
try
{
wxPorotcolBuffers.InitResponse.Builder builder = wxPorotcolBuffers.InitResponse.newBuilder();
wxPorotcolBuffers.BaseResponse.Builder baseResponseBuilder = wxPorotcolBuffers.BaseResponse.newBuilder();
baseResponseBuilder.setErrCode(0);
baseResponseBuilder.setErrMsg("OK");
builder.setUserIdHigh(0);
builder.setUserIdLow(0);
builder.setBaseResponse(baseResponseBuilder.build());
byte[] serialize = builder.build().toByteArray();
Log.d(TAG, "init response = " + Utils.byteArrayToHexString(serialize, 0, serialize.length));
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void sendDataTest()
{
try
{
String data = "0A0012143300810FB9001804004F4190FD632800000C787718914E";
wxPorotcolBuffers.SendDataRequest sendDataRequest = wxPorotcolBuffers.SendDataRequest.parseFrom(Utils.hexStringToBytes(data));
Log.d(TAG, "send data requset" + sendDataRequest.toString());
Log.d(TAG, "data is = " + Utils.byteArrayToHexString(sendDataRequest.getData().toByteArray(), 0, sendDataRequest.getData().toByteArray().length));
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void recvDataPushTest()
{
try
{
wxPorotcolBuffers.RecvDataPush.Builder recvDataPushBuilder = wxPorotcolBuffers.RecvDataPush.newBuilder();
wxPorotcolBuffers.BasePush.Builder basePushBuilder = wxPorotcolBuffers.BasePush.newBuilder();
recvDataPushBuilder.setBasePush(basePushBuilder.build());
byte[] data = Utils.hexStringToBytes("11223344556677889900aabbccddeeff");
recvDataPushBuilder.setData(ByteString.copyFrom(data));
recvDataPushBuilder.setType(wxPorotcolBuffers.EmDeviceDataType.EDDT_manufatureSvr);
Log.d(TAG, "recv data push = " + recvDataPushBuilder.build().toString());
Log.d(TAG, "recv data push stram = " + Utils.byteArrayToHexString(recvDataPushBuilder.build().toByteArray(), 0, recvDataPushBuilder.build().toByteArray().length));
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
总结
1.使用protobuf-gradle-plugin时,必须确定我们开发时,使用的jdk 版本、app 工程中build.gradle 版本和Android Studio 版本,
不然就会出现上面的找不到目录的问题。因此,使用和配置Android protobuf-gradle-plugin 最好上github 官网上(https://github.com/google/protobuf-gradle-plugin)查看一下版本信息。
/**
* ┏┓ ┏┓+ +
* ┏┛┻━━━┛┻┓ + +
* ┃ ┃
* ┃ ━ ┃ ++ + + +
* ████━████ ┃+
* ┃ ┃ +
* ┃ ┻ ┃
* ┃ ┃ + +
* ┗━┓ ┏━┛
* ┃ ┃
* ┃ ┃ + + + +
* ┃ ┃ Code is far away from bug with the animal protecting
* ┃ ┃ + 神兽保佑,代码无bug
* ┃ ┃
* ┃ ┃ +
* ┃ ┗━━━┓ + +
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛ + + + +
* ┃┫┫ ┃┫┫
* ┗┻┛ ┗┻┛+ + + +
*
* @author chenxi
* @date 2019-10-29 21:59:45
*/
参考文章
https://www.jianshu.com/p/e8712962f0e9?tdsourcetag=s_pcqq_aiomsg
https://www.jianshu.com/p/2a376e657ae0