Загрузка данных


#!/data/data/com.termux/files/usr/bin/bash
# NinebotBLEScanner installer — Termux / Linux / macOS
set -e

ROOT="NinebotBLEScanner"
PKG="com/ninebot/ble"

echo "[*] Создаю дерево: $ROOT"
mkdir -p "$ROOT/app/src/main/java/$PKG"
mkdir -p "$ROOT/app/src/main/res/layout"
mkdir -p "$ROOT/app/src/main/res/values"

# ---- root build.gradle ----
cat > "$ROOT/build.gradle" <<'EOF'
plugins {
    id 'com.android.application' version '8.1.4' apply false
}
EOF

# ---- settings.gradle ----
cat > "$ROOT/settings.gradle" <<'EOF'
pluginManagement {
    repositories { google(); mavenCentral(); gradlePluginPortal() }
}
dependencyResolutionManagement {
    repositories { google(); mavenCentral() }
}
rootProject.name = "NinebotBLEScanner"
include ':app'
EOF

# ---- app/build.gradle ----
cat > "$ROOT/app/build.gradle" <<'EOF'
plugins { id 'com.android.application' }
android {
    namespace 'com.ninebot.ble'
    compileSdk 34
    defaultConfig {
        applicationId "com.ninebot.ble"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}
EOF

# ---- AndroidManifest.xml ----
cat > "$ROOT/app/src/main/AndroidManifest.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
        android:usesPermissionFlags="neverForLocation"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
    <application android:label="NinebotBLE" android:theme="@android:style/Theme.Material.Dark">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>
EOF

# ---- layout ----
cat > "$ROOT/app/src/main/res/layout/activity_main.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:padding="12dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button android:id="@+id/btnScan"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="SCAN BLE"/>
    <Button android:id="@+id/btnStop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="STOP"/>
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
        <TextView android:id="@+id/tvLog"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textIsSelectable="true"
            android:fontFamily="monospace"
            android:textSize="11sp"/>
    </ScrollView>
</LinearLayout>
EOF

# ---- strings ----
cat > "$ROOT/app/src/main/res/values/strings.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<resources><string name="app_name">NinebotBLE</string></resources>
EOF

# ---- MainActivity.java ----
cat > "$ROOT/app/src/main/java/$PKG/MainActivity.java" <<'EOF'
package com.ninebot.ble;

import android.Manifest;
import android.bluetooth.*;
import android.bluetooth.le.*;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import java.util.*;

public class MainActivity extends AppCompatActivity {

    private static final UUID UUID_NINEBOT = UUID.fromString("0000fe95-0000-1000-8000-00805f9b34fb");
    private static final UUID UUID_WRITE   = UUID.fromString("00000001-0000-1000-8000-00805f9b34fb");
    private static final UUID UUID_NOTIFY  = UUID.fromString("00000002-0000-1000-8000-00805f9b34fb");

    // известные ключи Ninebot по моделям (байты аутентификации)
    private static final Map<String, byte[]> KEYS = new LinkedHashMap<>();
    static {
        KEYS.put("ES1/ES2",  new byte[]{(byte)0x97,(byte)0xB5,(byte)0x3D,(byte)0x4C,(byte)0x4D,(byte)0x6B,(byte)0x7C,(byte)0x2A,(byte)0x8A,(byte)0x2D,(byte)0x5C,(byte)0x8A,(byte)0x7F,(byte)0x3D,(byte)0x6E,(byte)0x4E});
        KEYS.put("Max/G30",  new byte[]{(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00});
        KEYS.put("Generic",  new byte[]{0x21,(byte)0x80,(byte)0x54,(byte)0x2B,(byte)0x4D,(byte)0x4E,(byte)0x6F,(byte)0x66,(byte)0x77,(byte)0x88,(byte)0x99,(byte)0xAA,(byte)0xBB,(byte)0xCC,(byte)0xDD,(byte)0xEE});
    }

    private BluetoothLeScanner scanner;
    private BluetoothGatt gatt;
    private TextView tvLog;
    private final Handler ui = new Handler(Looper.getMainLooper());
    private final Map<String, BluetoothDevice> found = new LinkedHashMap<>();

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);
        setContentView(R.layout.activity_main);
        tvLog = findViewById(R.id.tvLog);
        Button btnScan = findViewById(R.id.btnScan);
        Button btnStop = findViewById(R.id.btnStop);

        btnScan.setOnClickListener(v -> startScan());
        btnStop.setOnClickListener(v -> stopScan());

        requestPerms();
        BluetoothManager bm = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        if (bm != null && bm.getAdapter() != null) {
            scanner = bm.getAdapter().getBluetoothLeScanner();
        }
    }

    private void requestPerms() {
        List<String> p = new ArrayList<>();
        if (Build.VERSION.SDK_INT >= 31) {
            p.add(Manifest.permission.BLUETOOTH_SCAN);
            p.add(Manifest.permission.BLUETOOTH_CONNECT);
        } else {
            p.add(Manifest.permission.ACCESS_FINE_LOCATION);
        }
        ActivityCompat.requestPermissions(this, p.toArray(new String[0]), 1);
    }

    private void log(String s) {
        ui.post(() -> tvLog.append(s + "\n"));
    }

    private void startScan() {
        if (scanner == null) { log("[!] BLE недоступен"); return; }
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED
            && Build.VERSION.SDK_INT >= 31) { log("[!] нет разрешения SCAN"); return; }
        found.clear();
        log("[*] скан 10с...");
        ScanSettings ss = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
        scanner.startScan(null, ss, cb);
        ui.postDelayed(this::stopScan, 10000);
    }

    private void stopScan() {
        try { if (scanner != null) scanner.stopScan(cb); } catch (Exception ignored) {}
        log("[*] скан стоп. найдено: " + found.size());
        for (BluetoothDevice d : found.values()) {
            String name = d.getName() == null ? "?" : d.getName();
            if (name.toLowerCase().contains("ninebot") || name.toLowerCase().contains("segway")
                || name.toUpperCase().startsWith("NB") || name.toUpperCase().startsWith("MIS")) {
                log("[+] Ninebot target: " + name + " " + d.getAddress());
                connectAndAuth(d);
                return;
            }
        }
        log("[-] Ninebot не найден среди видимых");
    }

    private final ScanCallback cb = new ScanCallback() {
        @Override public void onScanResult(int t, ScanResult r) {
            BluetoothDevice d = r.getDevice();
            found.put(d.getAddress(), d);
            String n = d.getName() == null ? "?" : d.getName();
            log("• " + n + " | " + d.getAddress() + " | rssi " + r.getRssi());
        }
        @Override public void onScanFailed(int e) { log("[!] scan fail " + e); }
    };

    private void connectAndAuth(BluetoothDevice dev) {
        log("[*] connect -> " + dev.getAddress());
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
            && Build.VERSION.SDK_INT >= 31) return;
        gatt = dev.connectGatt(this, false, gattCb, BluetoothDevice.TRANSPORT_LE);
    }

    private final BluetoothGattCallback gattCb = new BluetoothGattCallback() {
        @Override public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                log("[+] connected, discover services");
                if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
                g.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                log("[-] disconnected");
            }
        }

        @Override public void onServicesDiscovered(BluetoothGatt g, int status) {
            log("[*] services: " + g.getServices().size());
            BluetoothGattCharacteristic w = null, n = null;
            for (BluetoothGattService s : g.getServices()) {
                log("  svc " + s.getUuid());
                for (BluetoothGattCharacteristic c : s.getCharacteristics()) {
                    log("    chr " + c.getUuid() + " props=" + c.getProperties());
                    if (c.getUuid().equals(UUID_WRITE)) w = c;
                    if (c.getUuid().equals(UUID_NOTIFY)) n = c;
                }
            }
            if (w == null) {
                // fallback: ищем любую write/notify пару
                for (BluetoothGattService s : g.getServices()) {
                    for (BluetoothGattCharacteristic c : s.getCharacteristics()) {
                        int p = c.getProperties();
                        if ((p & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0 && w == null) w = c;
                        if ((p & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 && n == null) n = c;
                    }
                }
            }
            if (w == null) { log("[!] write-характеристика не найдена"); return; }
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
            if (n != null) {
                g.setCharacteristicNotification(n, true);
                BluetoothGattDescriptor d = n.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
                if (d != null) { d.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); g.writeDescriptor(d); }
            }
            log("[*] пробую ключи аутентификации...");
            tryKeys(g, w);
        }

        @Override public void onCharacteristicChanged(BluetoothGatt g, BluetoothGattCharacteristic c) {
            byte[] v = c.getValue();
            log("[<-] " + hex(v));
        }

        @Override public void onCharacteristicWrite(BluetoothGatt g, BluetoothGattCharacteristic c, int s) {
            log("[->] write status=" + s + " " + hex(c.getValue()));
        }
    };

    private void tryKeys(BluetoothGatt g, BluetoothGattCharacteristic w) {
        for (Map.Entry<String, byte[]> e : KEYS.entrySet()) {
            byte[] frame = new byte[18];
            frame[0] = 0x5A; frame[1] = 0x5A; frame[2] = 0x10; // header Ninebot
            System.arraycopy(e.getValue(), 0, frame, 3, 16);
            frame[2] = (byte)(frame.length - 3);
            log("[*] try key: " + e.getKey());
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
            w.setValue(frame);
            g.writeCharacteristic(w);
            try { Thread.sleep(300); } catch (InterruptedException ignored) {}
        }
        log("[*] ключи отработаны. смотри ответы в логе [<-]");
    }

    private static String hex(byte[] b) {
        if (b == null) return "null";
        StringBuilder s = new StringBuilder();
        for (byte x : b) s.append(String.format("%02X ", x));
        return s.toString().trim();
    }
}
EOF

echo "[+] файлы созданы."

# --- попытка сборки ---
if command -v gradle >/dev/null 2>&1; then
    echo "[*] gradle найден — запускаю сборку"
    cd "$ROOT"
    gradle wrapper --gradle-version 8.2 || true
    ./gradlew assembleDebug || echo "[!] сборка упала — нужен Android SDK + local.properties"
    echo "[*] APK путь: app/build/outputs/apk/debug/app-debug.apk"
else
    echo "[!] gradle не найден. установи:"
    echo "    Termux: pkg install openjdk-17 gradle android-sdk"
    echo "    или открой папку в Android Studio"
fi

echo "[✓] готово. проект в ./$ROOT"