流浪地球2 笨笨机器人 拼装模型蓝牙控制

立音喵
立音喵
2023年04月08日


笨笨智能积木

淘宝

虽说这上面写的是羊很大和商汤科技,不过经过拆解 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;
}

这里很明显是,如果 f9385btrue 则返回发送内容,否则如果上次发送和本次超过 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());
    }
}

这里 f9356bBluetoothAdapter 用来看是否打开了蓝牙。而下面则是 getIfChanged 返回值,如果其返回了内容则调用 f9357c.writeCommand(ifChanged),而 f9357cBleContext。每次等待 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 手柄?

评论区
Made with ♥ by LiYin
Yin Theme V2