笨笨智能积木
虽说这上面写的是羊很大和商汤科技,不过经过拆解 apk 包发现它的所有控制内容全部位于一个不属于 "引力之外" APP 的包下。而是属于一个叫做 com.yundongjia
的包,经过搜索,我发现在 Google Play 上,有一款这个包名的应用叫做 CaDASMART
,那么我有理由推断,这整个智能积木完全是由 广东双鹰玩具实业有限公司
的其他拼装模型改成的。
它说8小时拼搭... 我穿着 Kig 直播拼了5次直播一共12个小时...
这个拼装引导真是一言难尽
BLE 协议
它的 BLE 设备名称为 YX_000000(BLE)
。
只用原本的 "引力之外" APP 多无聊,而且那个摇杆真是一言难尽,基本上没有设置死区,也非常容易误触。那么我们为什么不能用 Xbox 手柄来操作它呢?
反编译
直接打开 jadx,整个包没有加壳,只进行了混淆。
根据 Android 框架,和 Android 相关的调用一定是不会被混淆的,直接起手搜索 Bluetooth
。很快就定位到了 ConnectActivity
ControllerActivity
两个名字就非常令人可疑的 Activity。
事实上这就是连接和操纵摇杆这两个 Activity。
具体分析过程就不多介绍了。直接贴上最重要的部分。
com.yundongjia.tongui.ControllerActivity
public void mo16251a(EnumC0789e enumC0789e, double d) {
ImageView imageView;
int i;
int ordinal = enumC0789e.ordinal();
if (ordinal == 8) {
ControllerActivity.m16252a(ControllerActivity.this);
return;
}
if (ordinal == 0) {
ControllerActivity controllerActivity = ControllerActivity.this;
int i2 = ControllerActivity.f9405j;
controllerActivity.getClass();
int abs = (int) (Math.abs(d) * 127.0d);
controllerActivity.f9406a.enginRun(EngineEnum.A, true, abs);
controllerActivity.f9406a.enginRun(EngineEnum.B, false, abs);
controllerActivity.f9406a.enginRun(EngineEnum.C, false, abs);
controllerActivity.f9406a.enginRun(EngineEnum.D, true, abs);
imageView = (ImageView) controllerActivity.findViewById(C2526R.C2528id.direction_bg);
i = C2526R.C2527drawable.direction_upper;
} else if (ordinal == 1) {
ControllerActivity controllerActivity2 = ControllerActivity.this;
int i3 = ControllerActivity.f9405j;
controllerActivity2.getClass();
int abs2 = (int) (Math.abs(d) * 127.0d);
controllerActivity2.f9406a.enginRun(EngineEnum.A, false, abs2);
controllerActivity2.f9406a.enginRun(EngineEnum.B, true, abs2);
controllerActivity2.f9406a.enginRun(EngineEnum.C, true, abs2);
controllerActivity2.f9406a.enginRun(EngineEnum.D, false, abs2);
imageView = (ImageView) controllerActivity2.findViewById(C2526R.C2528id.direction_bg);
i = C2526R.C2527drawable.direction_bottom;
} else if (ordinal == 2) {
ControllerActivity controllerActivity3 = ControllerActivity.this;
int i4 = ControllerActivity.f9405j;
controllerActivity3.getClass();
int abs3 = (int) (Math.abs(d) * 127.0d);
controllerActivity3.f9406a.enginRun(EngineEnum.A, true, abs3);
controllerActivity3.f9406a.enginRun(EngineEnum.B, true, abs3);
controllerActivity3.f9406a.enginRun(EngineEnum.C, false, abs3);
controllerActivity3.f9406a.enginRun(EngineEnum.D, false, abs3);
imageView = (ImageView) controllerActivity3.findViewById(C2526R.C2528id.direction_bg);
i = C2526R.C2527drawable.direction_left;
} else if (ordinal != 3) {
return;
} else {
ControllerActivity controllerActivity4 = ControllerActivity.this;
int i5 = ControllerActivity.f9405j;
controllerActivity4.getClass();
int abs4 = (int) (Math.abs(d) * 127.0d);
controllerActivity4.f9406a.enginRun(EngineEnum.A, false, abs4);
controllerActivity4.f9406a.enginRun(EngineEnum.B, false, abs4);
controllerActivity4.f9406a.enginRun(EngineEnum.C, true, abs4);
controllerActivity4.f9406a.enginRun(EngineEnum.D, true, abs4);
imageView = (ImageView) controllerActivity4.findViewById(C2526R.C2528id.direction_bg);
i = C2526R.C2527drawable.direction_right;
}
imageView.setImageResource(i);
imageView.setVisibility(0);
}
这里就是处理摇杆的地方了,我们可以看到,它调用了 enginRun
(怎么还拼错了 Engine)。从这里看来传入值就是 enginRun(轮子, 正反转, 速度值)
。
com.yundongjia.tongble.BleContext
public void enginRun(EngineEnum engineEnum, boolean z, int i) {
int i2 = 0;
if (i > 127) {
i = 127;
} else if (i < 0) {
i = 0;
}
byte b = (byte) i;
byte b2 = (byte) ((engineEnum != EngineEnum.A ? !z : z) ? (b + 128) & 255 : (128 - b) & 255);
int i3 = C25141.f9364a[engineEnum.ordinal()];
if (i3 == 1) {
i2 = 4;
} else if (i3 == 2) {
i2 = 5;
} else if (i3 == 3) {
i2 = 6;
} else if (i3 == 4) {
i2 = 7;
}
if (i2 > 0) {
BluetoothContext.getDevice().setSendData(i2, b2);
}
}
这里的 EngineEnum 代表四个轮子,可用值为 A
B
C
D
。我看到这里还在想为什么要判断是否是 "引擎 A" ,如果是就反转结果,后来我试了发现这大概是他们硬件或者固件做错了,于是软件修补了一下...
这里的代码就很容易理解了,先将 i 限制到 0-127,然后将其转换成 Java 中并不存在的 UInt,然后根据 i3 的取值将发送的数据位转换为 4 5 6 7,这是为什么呢?
我们直接追踪到 setSendData
。
com.yundongjia.tongble.BluetoothDeviceBase
public abstract void setSendData(int i, byte b);
啊 不小心追踪到了抽象类,去搜索一下 BluetoothDeviceBase
com.yundongjia.tongble.BluetoothDevice
public void setSendData(int i, byte b) {
this.f9383c[i] = b;
}
What? 就做了一次数组设置?那这不看看数组写了什么。
public static final byte[] f9382d = {-52, 0, 0, 2, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE, 51};
public byte[] f9383c = f9382d;
好,这就是协议本体了。根据上面变换为 4 5 6 7 的结果,正好对应着这里的 f9383c[4]
f9383c[5]
f9383c[6]
f9383c[7]
,这里也就是组装协议的地方了。
而相对于 setSendData
还有 getSendData
public byte[] getSendData() {
BluetoothDeviceBase.f9385b = false;
byte[] bArr = this.f9383c;
int i = bArr[1];
for (int i2 = 2; i2 < bArr.length - 2; i2++) {
i += bArr[i2];
}
bArr[bArr.length - 2] = (byte) (i & 255);
return this.f9383c;
}
这里很明显是计算校验和,并将其修改到 bArr.length - 2
即倒数第二位这个位置上。
自此,已经完成了对协议的组装。
那么接下来,就是要知道 BLE 的 UUID 和发送频率之类的了。
继续搜索 getSendData
看看谁在用它。这里定位到 getIfChanged
com.yundongjia.tongble.BluetoothDeviceBase
public byte[] getIfChanged() {
if (f9385b) {
return getSendData();
}
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - this.f9386a > 150) {
this.f9386a = currentTimeMillis;
return getSendData();
}
return null;
}
这里很明显是,如果 f9385b
为 true
则返回发送内容,否则如果上次发送和本次超过 150 毫秒则发送。这里也不用其他搜索了,很明显这里是如果有变化则发送比如摇杆方向变了之类的。
继续追踪 getIfChanged
来到 BleCommandThread
这里很明显是发送蓝牙数据的线程。
com.yundongjia.tongble.BleCommandThread
while (!BluetoothUtils.advertiseExit) {
try {
if (this.f9356b == null) {
m16258a();
Thread.sleep(5L);
} else {
byte[] ifChanged = BluetoothContext.getDevice().getIfChanged();
if (ifChanged != null) {
this.f9357c.writeCommand(ifChanged);
}
Thread.sleep(10L);
}
} catch (Exception e) {
StringBuilder sb2 = new StringBuilder();
sb2.append("蓝牙广播出错 ");
sb2.append(e.getMessage());
}
}
这里 f9356b
是 BluetoothAdapter
用来看是否打开了蓝牙。而下面则是 getIfChanged
返回值,如果其返回了内容则调用 f9357c.writeCommand(ifChanged)
,而 f9357c
为 BleContext
。每次等待 0.01 秒后再尝试获取。(这是不是线程不安全?)
来到此处,就非常明朗了。
com.yundongjia.tongble.BleContext
public static UUID f9359c = UUID.fromString("0000ae3b-0000-1000-8000-00805f9b34fb");
public static UUID f9360d = UUID.fromString("0000ae3a-0000-1000-8000-00805f9b34fb");
public static UUID f9361e = UUID.fromString("0000af30-0000-1000-8000-00805f9b34fb");
这里明显就是 BLE 的 Service 和 Characteristic 的 UUID 了。而 writeCommand
则在下方。
public void writeCommand(byte[] bArr) {
for (BleDevice bleDevice : this.f9363a.values()) {
if (bleDevice.isConnected()) {
bleDevice.writeCommand(bArr);
}
}
}
嗯?又跑到别的地方去了?继续追踪。
com.yundongjia.tongble.BleDevice
public void writeCommand(byte[] bArr) {
if (this.f9368c == null) {
connect();
}
BluetoothGattCharacteristic bluetoothGattCharacteristic = this.f9367b;
if (bluetoothGattCharacteristic != null) {
bluetoothGattCharacteristic.setValue(bArr);
this.f9368c.writeCharacteristic(this.f9367b);
return;
}
BluetoothGatt bluetoothGatt = this.f9368c;
if (bluetoothGatt != null) {
bluetoothGatt.discoverServices();
}
String str = BleContext.f9362f;
StringBuilder sb2 = new StringBuilder();
sb2.append("发送命令失败返回 characteristic=");
sb2.append(this.f9367b);
}
虽然写法有点令人不适,但我们发现了它写入的位置,从这里摸到上面的 UUID。
public void onServicesDiscovered(BluetoothGatt bluetoothGatt, int i) {
BluetoothGattService service = bluetoothGatt.getService(BleContext.f9360d);
String str = BleContext.f9362f;
StringBuilder sb2 = new StringBuilder();
sb2.append("find Servcier");
sb2.append(service);
sb2.append(" connectstate ");
sb2.append(this.f9371f);
if (service != null) {
this.f9367b = service.getCharacteristic(BleContext.f9359c);
}
}
那么很明显 f9360d
为 Service, f9359c
为 Characteristic。
那么 f9361e
在干什么?这里就追踪到了 BleScanThread
。啊只是扫描过滤器... 这个就无所谓啦。
总结一下
服务 UUID 0000ae3a-0000-1000-8000-00805f9b34fb
属性 UUID 0000ae3b-0000-1000-8000-00805f9b34fb
基础数据是 [-52, 0, 0, 2, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, 51]
,然后根据速度和方向计算 [4:7]
之间的数据,然后将倒数第二位做累加校验和。
每 150 毫秒或者有变化时发送一次数据,官方最小检测时长是 0.01 秒一次。
客户端的方向摇杆的计算值如下(其实根据麦轮的方向自己想一下就行了)
向前
int abs = (int) (Math.abs(d) * 127.0d);
enginRun(EngineEnum.A, true, abs);
enginRun(EngineEnum.B, false, abs);
enginRun(EngineEnum.C, false, abs);
enginRun(EngineEnum.D, true, abs);
向后
int abs = (int) (Math.abs(d) * 127.0d);
enginRun(EngineEnum.A, false, abs);
enginRun(EngineEnum.B, true, abs);
enginRun(EngineEnum.C, true, abs);
enginRun(EngineEnum.D, false, abs);
向左
int abs = (int) (Math.abs(d) * 127.0d);
enginRun(EngineEnum.A, true, abs);
enginRun(EngineEnum.B, true, abs);
enginRun(EngineEnum.C, false, abs);
enginRun(EngineEnum.D, false, abs);
向右
int abs = (int) (Math.abs(d) * 127.0d);
enginRun(EngineEnum.A, false, abs);
enginRun(EngineEnum.B, false, abs);
enginRun(EngineEnum.C, true, abs);
enginRun(EngineEnum.D, true, abs);
向前漂移
enginRun(EngineEnum.A, true, 127);
enginRun(EngineEnum.B, true, 127);
enginRun(EngineEnum.C, true, 0);
enginRun(EngineEnum.D, true, 0);
向后漂移
enginRun(EngineEnum.A, false, 127);
enginRun(EngineEnum.B, false, 127);
enginRun(EngineEnum.C, false, 0);
enginRun(EngineEnum.D, false, 0);
顺时针转
enginRun(EngineEnum.A, true, 127);
enginRun(EngineEnum.B, true, 127);
enginRun(EngineEnum.C, true, 127);
enginRun(EngineEnum.D, true, 127);
逆时针转
enginRun(EngineEnum.A, false, 127);
enginRun(EngineEnum.B, false, 127);
enginRun(EngineEnum.C, false, 127);
enginRun(EngineEnum.D, false, 127);
以及停下
enginRun(EngineEnum.A, false, 0);
enginRun(EngineEnum.B, false, 0);
enginRun(EngineEnum.C, false, 0);
enginRun(EngineEnum.D, false, 0);
有了这些,我们足以跳过他们官方的 APP,用其他东西控制笨笨了!比如 Xbox 手柄?