微信蓝牙外设开发记录 - 3 (Android与微信蓝牙BEL设备通讯)

22 篇文章 0 订阅
2 篇文章 0 订阅

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

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值