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