微信藍牙外設開發記錄 - 3 (Android與微信藍牙BEL設備通訊)

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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章