吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 509|回复: 9
收起左侧

[经验求助] 爱玛出行蓝牙主动控制优化

[复制链接]
xiaobaice 发表于 2026-6-24 20:21
100吾爱币
本帖最后由 xiaobaice 于 2026-6-24 20:27 编辑

下面两个js都是shortx软件里面运行的,是rhino运行环境。主要是爱玛出行APP的主动蓝牙控制功能。

虽然现在的ble可以进行open、close、power(near还未修复),但是运行过程很慢,不知道怎么优化一下从而实现像APP那样对的控制速度。
7800(鉴权): props=0x18 = 0x10 (NOTIFY) + 0x08 (WRITE)
7801(控制): props=0x18 = NOTIFY + WRITE
7802(查询): props=0x18 = NOTIFY + WRITE
7803(订阅): props=0x10 = NOTIFY (只有通知,没有写入)

// ShortX ExecuteJS:爱玛车辆列表
// 作用:读取/创建配置文件,调用爱玛 API 获取车辆列表,输出给 ShortX 列表对话框使用的 JSON。
// 配置路径:shortx.getShortXDir()/data/爱玛出行/config.json

var API_HOST = "https://app.aimaiot.cn";
var SIGN_SECRET = "doTVIzldF06eFQf0vBIinsZS3piuFEMJ";
var NONCE_CHARS = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
var CONFIG_DIR_NAME = "爱玛出行";
var CONFIG_FILE_NAME = "config.json";

importClass(java.io.BufferedReader);
importClass(java.io.File);
importClass(java.io.FileInputStream);
importClass(java.io.FileOutputStream);
importClass(java.io.InputStreamReader);
importClass(java.io.OutputStreamWriter);
importClass(java.lang.StringBuilder);
importClass(java.lang.System);
importClass(java.net.HttpURLConnection);
importClass(java.net.URL);
importClass(java.net.URLEncoder);
importClass(java.security.MessageDigest);
importClass(java.util.List);
importClass(org.json.JSONArray);
importClass(org.json.JSONObject);

function ensureDir(dirValue) {
    if (!dirValue.exists()) dirValue.mkdirs();
}

function getConfigFile() {
    var root = new File(shortx.getShortXDir(), "data");
    ensureDir(root);
    var dir = new File(root, CONFIG_DIR_NAME);
    ensureDir(dir);
    return new File(dir, CONFIG_FILE_NAME);
}

function readFileText(fileValue) {
    var fis = new FileInputStream(fileValue);
    try {
        var br = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
        var sb = new StringBuilder();
        var line;
        while ((line = br.readLine()) !== null) sb.append(line);
        br.close();
        return String(sb.toString());
    } finally {
        try { fis.close(); } catch (e1) {}
    }
}

function writeFileText(fileValue, textValue) {
    ensureDir(fileValue.getParentFile());
    var fos = new FileOutputStream(fileValue, false);
    try {
        var w = new OutputStreamWriter(fos, "UTF-8");
        w.write(String(textValue));
        w.flush();
        w.close();
    } finally {
        try { fos.close(); } catch (e2) {}
    }
}

function defaultConfig() {
    var cfg = new JSONObject();
    cfg.put("userId", "");
    cfg.put("accessToken", "");
    cfg.put("refreshToken", "");
    cfg.put("uuid", "");
    cfg.put("refreshOnStart", false);
    cfg.put("rssiThreshold", -65);
    cfg.put("scanTimeoutMs", 6000);
    cfg.put("targetVehicle", new JSONObject());
    return cfg;
}

function loadConfig() {
    var fileValue = getConfigFile();
    if (!fileValue.exists()) {
        var cfg = defaultConfig();
        writeFileText(fileValue, cfg.toString(2));
        return {
            created: true,
            filePath: String(fileValue.getAbsolutePath()),
            config: cfg
        };
    }
    var textValue = readFileText(fileValue);
    return {
        created: false,
        filePath: String(fileValue.getAbsolutePath()),
        config: new JSONObject(textValue)
    };
}

function readUser(configState) {
    var cfg = configState.config;
    if (!cfg.optString("refreshToken", "") && !cfg.optString("accessToken", "")) {
        throw "请先配置 " + configState.filePath + " 里的 accessToken 或 refreshToken";
    }
    return {
        configState: configState,
        userId: cfg.optString("userId", ""),
        accessToken: cfg.optString("accessToken", ""),
        refreshToken: cfg.optString("refreshToken", ""),
        uuid: cfg.optString("uuid", "")
    };
}

function saveUser(user) {
    var cfg = user.configState.config;
    cfg.put("userId", user.userId);
    cfg.put("accessToken", user.accessToken);
    cfg.put("refreshToken", user.refreshToken);
    cfg.put("uuid", user.uuid);
    writeFileText(new File(user.configState.filePath), cfg.toString(2));
}

function nonce() {
    var sb = new StringBuilder();
    for (var i = 0; i < 20; i++) {
        var p = Math.floor(Math.random() * NONCE_CHARS.length);
        sb.append(NONCE_CHARS.charAt(p));
    }
    return String(sb.toString());
}

function sha1Upper(s) {
    var md = MessageDigest.getInstance("SHA-1");
    var bytes = new java.lang.String(s).getBytes("UTF-8");
    var dig = md.digest(bytes);
    var out = "";
    for (var i = 0; i < dig.length; i++) {
        var b = dig[i] & 0xff;
        var h = b.toString(16);
        if (h.length < 2) h = "0" + h;
        out += h;
    }
    return out.toUpperCase();
}

function signedParams(route, params) {
    var p = {};
    if (params) {
        for (var k in params) p[k] = params[k];
    }
    p.timestamp = String(System.currentTimeMillis());
    p.nonce = nonce();
    p.url = route;

    var keys = [];
    for (var key in p) keys.push(key);
    keys.sort();

    var before = SIGN_SECRET;
    for (var i = 0; i < keys.length; i++) {
        var kk = keys[i];
        var vv = p[kk];
        if (vv && (vv instanceof List || vv instanceof JSONArray || Object.prototype.toString.call(vv) === "[object Array]")) continue;
        before += kk + String(vv);
    }
    before += SIGN_SECRET;
    p.sign = sha1Upper(before);
    return p;
}

function readText(streamValue) {
    var br = new BufferedReader(new InputStreamReader(streamValue, "UTF-8"));
    var sb = new StringBuilder();
    var line;
    while ((line = br.readLine()) !== null) sb.append(line);
    br.close();
    return String(sb.toString());
}

function toJsonObject(obj) {
    var jo = new JSONObject();
    for (var k in obj) jo.put(k, obj[k]);
    return jo;
}

function encodeQuery(params) {
    var arr = [];
    for (var k in params) {
        arr.push(URLEncoder.encode(k, "UTF-8") + "=" + URLEncoder.encode(String(params[k]), "UTF-8"));
    }
    return arr.join("&");
}

function httpJson(method, fullUrl, bodyObj, token, uuid) {
    var u = new URL(fullUrl);
    var conn = u.openConnection();
    conn.setRequestMethod(method);
    conn.setConnectTimeout(12000);
    conn.setReadTimeout(12000);
    conn.setRequestProperty("Content-Type", "application/json");
    conn.setRequestProperty("accept", "application/json;charset=UTF-8");
    conn.setRequestProperty("brandType", "0");
    conn.setRequestProperty("deviceType", "ANDROID");
    conn.setRequestProperty("platformId", "android");
    conn.setRequestProperty("currentVersion", "5.9.1");
    if (uuid) conn.setRequestProperty("UUID", uuid);
    if (token) conn.setRequestProperty("Authorization", "Bearer " + token);
    if (method === "POST") {
        conn.setDoOutput(true);
        var w = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
        w.write(toJsonObject(bodyObj).toString());
        w.flush();
        w.close();
    }
    var code = conn.getResponseCode();
    var textValue = readText(code >= 200 && code < 400 ? conn.getInputStream() : conn.getErrorStream());
    return { httpCode: code, json: new JSONObject(textValue), text: textValue };
}

function apiCall(user, method, prefix, route, params, allowRefresh) {
    if (allowRefresh && !user.accessToken && user.refreshToken) refreshToken(user);
    var body = signedParams(route, params || {});
    var full = API_HOST + prefix + route;
    if (method === "GET") full += "?" + encodeQuery(body);
    var res = httpJson(method, full, method === "POST" ? body : null, user.accessToken, user.uuid);
    var code = res.json.optInt("code", 0);
    if (allowRefresh && (res.httpCode === 401 || code === 3 || code === 401 || code === 403 || code === 4001 || code === 4002)) {
        refreshToken(user);
        res = httpJson(method, full, method === "POST" ? body : null, user.accessToken, user.uuid);
    }
    return res.json;
}

function refreshToken(user) {
    if (!user.refreshToken) throw "refreshToken 为空";
    var json = apiCall(user, "POST", "/user", "/appUser/refreshToken", { refreshToken: user.refreshToken }, false);
    if (json.optInt("code", -1) !== 0) throw "刷新 token 失败: " + json.toString();
    var payload = json.getJSONObject("data");
    user.accessToken = String(payload.getString("accessToken"));
    user.refreshToken = String(payload.optString("refreshToken", user.refreshToken));
    saveUser(user);
}

function mergeDetail(user, item) {
    if (item.optString("bluetoothMac", "") && item.optString("bluetoothKey", "")) return item;
    var vin = item.optString("vinCode", "");
    var cd = item.optString("cdKey", "");
    if (!vin || !cd) return item;
    var detailJson = apiCall(user, "POST", "/veh", "/vehicle/getVehicleDetail", { vinCode: vin, cdKey: cd }, true);
    if (detailJson.optInt("code", -1) !== 0 || !detailJson.has("data")) return item;
    var detail = detailJson.getJSONObject("data");
    var keys = detail.keys();
    while (keys.hasNext()) {
        var k = String(keys.next());
        if (!item.has(k) || String(item.opt(k)) === "") item.put(k, detail.opt(k));
    }
    return item;
}

function getVehicles(user) {
    var json = apiCall(user, "POST", "/veh", "/userVehicleRelation/myVehicleList", {}, true);
    if (json.optInt("code", -1) !== 0) throw "获取车辆列表失败: " + json.toString();
    var payloadList = json.optJSONArray("data");
    if (!payloadList) payloadList = new JSONArray();
    var out = new JSONArray();
    for (var i = 0; i < payloadList.length(); i++) {
        out.put(mergeDetail(user, payloadList.getJSONObject(i)));
    }
    return out;
}

function dialogList(items) {
    var list = new JSONArray();
    for (var i = 0; i < items.length(); i++) {
        var v = items.getJSONObject(i);
        var name = v.optString("vehicleNick", "");
        if (!name) name = v.optString("vehicleName", "");
        if (!name) name = v.optString("vinCode", "vehicle-" + i);
        var summary = v.optString("vinCode", "") + "  " + v.optString("bluetoothMac", "");
        var row = new JSONObject();
        row.put("name", name);
        row.put("summary", summary);
        row.put("__value", v.toString());
        list.put(row);
    }
    return list.toString();
}

function main() {
    var configState = loadConfig();
    if (configState.created) {
        return "已创建配置文件,请填写后重新运行:\n" + configState.filePath + "\n至少填写 refreshToken;如果有 uuid 也一起填写。";
    }
    var user = readUser(configState);
    if (configState.config.optBoolean("refreshOnStart", false) && user.refreshToken) refreshToken(user);
    return dialogList(getVehicles(user));
}

main();

// ShortX ExecuteJS:爱玛独立蓝牙 BLE 控制
// 说明:本脚本不调用爱玛 APP 的桥接蓝牙类,也不创建爱玛 APP Context。
// 模式:
//   "near"  -> 只扫描目标车 RSSI,输出 true/false
//   "power" -> 独立连接 BLE,握手后查询上下电状态
//   "open"  -> 独立连接 BLE,握手后上电
//   "close" -> 独立连接 BLE,握手后下电

var UUID_BIND_TEXT = "00007800-0000-1000-8000-00805F9B34FB";
var UUID_CONTROL_TEXT = "00007801-0000-1000-8000-00805F9B34FB";
var UUID_QUERY_TEXT = "00007802-0000-1000-8000-00805F9B34FB";
var UUID_REPORT_TEXT = "00007803-0000-1000-8000-00805F9B34FB";
var CCC_UUID_TEXT = "00002902-0000-1000-8000-00805f9b34fb";

// ===== 用户配置:运行模式和目标车辆 =====
var mode = typeof aimaMode !== "undefined" ? String(aimaMode) : "close";
var TARGET_VEHICLE_NICK = "乐维";
var TARGET_VEHICLE_VIN = "";
var TARGET_BLUETOOTH_MAC = "";
var TARGET_BLUETOOTH_KEY = "";
var TARGET_UUID = "";
var TARGET_BLE_VERSION = 2;

var rssiThreshold = typeof aimaRssiThreshold !== "undefined" ? parseInt(aimaRssiThreshold, 10) : -65;
var scanTimeoutMs = typeof aimaScanTimeoutMs !== "undefined" ? parseInt(aimaScanTimeoutMs, 10) : 6000;
var connectTimeoutMs = typeof aimaConnectTimeoutMs !== "undefined" ? parseInt(aimaConnectTimeoutMs, 10) : 30000;
var notifyTimeoutMs = typeof aimaNotifyTimeoutMs !== "undefined" ? parseInt(aimaNotifyTimeoutMs, 10) : 12000;
var powerNotifyTimeoutMs = typeof aimaPowerNotifyTimeoutMs !== "undefined" ? parseInt(aimaPowerNotifyTimeoutMs, 10) : 2000;

importClass(java.lang.StringBuilder);
importClass(java.lang.Thread);
importClass(java.util.UUID);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);
importClass(java.security.MessageDigest);
importClass(java.security.SecureRandom);
importClass(javax.crypto.Cipher);
importClass(javax.crypto.spec.SecretKeySpec);
importClass(android.bluetooth.BluetoothAdapter);
importClass(android.bluetooth.BluetoothDevice);
importClass(android.bluetooth.BluetoothGatt);
importClass(android.bluetooth.BluetoothGattCallback);
importClass(android.bluetooth.BluetoothGattCharacteristic);
importClass(android.bluetooth.BluetoothGattDescriptor);
importClass(android.bluetooth.BluetoothProfile);
importClass(android.bluetooth.le.ScanCallback);
importClass(android.os.Build);
importClass(org.json.JSONObject);

var JArray = Packages.java.lang.reflect.Array;
var ByteType = Packages.java.lang.Byte.TYPE;
var secureRandomObj = new SecureRandom();

function logText(textValue) {
    console.log("[爱玛独立BLE] " + String(textValue));
}

function makeJson(objValue) {
    var json = new JSONObject();
    for (var nameValue in objValue) {
        if (objValue[nameValue] === null || typeof objValue[nameValue] === "undefined") {
            json.put(nameValue, JSONObject.NULL);
        } else {
            json.put(nameValue, objValue[nameValue]);
        }
    }
    return String(json.toString());
}

function configuredVehicle() {
    var obj = new JSONObject();
    obj.put("vehicleNick", TARGET_VEHICLE_NICK);
    obj.put("vinCode", TARGET_VEHICLE_VIN);
    obj.put("bluetoothMac", TARGET_BLUETOOTH_MAC);
    obj.put("bluetoothKey", TARGET_BLUETOOTH_KEY);
    obj.put("uuid", TARGET_UUID);
    obj.put("bleVersion", TARGET_BLE_VERSION);
    return obj;
}

function vehicleMac(vehicleValue) {
    var textValue = vehicleValue.optString("bluetoothMac", "");
    if (!textValue) throw "车辆缺少 bluetoothMac";
    return String(textValue).toUpperCase();
}

function vehicleKey(vehicleValue) {
    var textValue = vehicleValue.optString("bluetoothKey", "");
    if (!textValue) throw "车辆缺少 bluetoothKey";
    return String(textValue);
}

function isNearMac(macText) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter || !adapter.isEnabled()) return false;
    var scanner = adapter.getBluetoothLeScanner();
    if (!scanner) return false;
    var latch = new CountDownLatch(1);
    var bestRssi = -999;
    var scanCb = new JavaAdapter(ScanCallback, {
        onScanResult: function(callbackType, scanResult) {
            var dev = scanResult.getDevice();
            if (dev && String(dev.getAddress()).toUpperCase() === String(macText).toUpperCase()) {
                var rssiValue = Number(scanResult.getRssi());
                if (rssiValue > bestRssi) bestRssi = rssiValue;
                if (bestRssi >= rssiThreshold) latch.countDown();
            }
        }
    });
    scanner.startScan(scanCb);
    latch.await(scanTimeoutMs, TimeUnit.MILLISECONDS);
    try { scanner.stopScan(scanCb); } catch (e2) {}
    logText("near 扫描结束 mac=" + macText + ", bestRssi=" + bestRssi + ", threshold=" + rssiThreshold);
    return bestRssi >= rssiThreshold;
}

function byteValue(numValue) {
    var v = Number(numValue) & 255;
    return v > 127 ? v - 256 : v;
}

function u8(numValue) {
    return Number(numValue) & 255;
}

function newBytes(lenValue) {
    return JArray.newInstance(ByteType, Number(lenValue));
}

function bytesLen(arrValue) {
    return JArray.getLength(arrValue);
}

function bytesGet(arrValue, indexValue) {
    return u8(JArray.getByte(arrValue, Number(indexValue)));
}

function bytesSet(arrValue, indexValue, numValue) {
    JArray.setByte(arrValue, Number(indexValue), byteValue(numValue));
}

function bytesFromNumbers(listValue) {
    var out = newBytes(listValue.length);
    for (var i = 0; i < listValue.length; i++) bytesSet(out, i, listValue[i]);
    return out;
}

function concatBytes() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += bytesLen(arguments[i]);
    var out = newBytes(total);
    var pos = 0;
    for (var p = 0; p < arguments.length; p++) {
        var one = arguments[p];
        for (var j = 0; j < bytesLen(one); j++) {
            bytesSet(out, pos, bytesGet(one, j));
            pos++;
        }
    }
    return out;
}

function sliceBytes(arrValue, startValue, lenValue) {
    var out = newBytes(lenValue);
    for (var i = 0; i < lenValue; i++) bytesSet(out, i, bytesGet(arrValue, startValue + i));
    return out;
}

function bytesToHex(arrValue) {
    var table = "0123456789ABCDEF";
    var sb = new StringBuilder();
    for (var i = 0; i < bytesLen(arrValue); i++) {
        var v = bytesGet(arrValue, i);
        sb.append(table.charAt((v >> 4) & 15));
        sb.append(table.charAt(v & 15));
    }
    return String(sb.toString());
}

function hexToBytes(hexValue) {
    var textValue = String(hexValue || "").replace(/\s+/g, "").toUpperCase();
    if (textValue.length % 2 !== 0) throw "hex 长度不是偶数";
    var out = newBytes(textValue.length / 2);
    for (var i = 0; i < textValue.length; i += 2) {
        bytesSet(out, i / 2, parseInt(textValue.substring(i, i + 2), 16));
    }
    return out;
}

function aesEcbRun(dataValue, keyValue, modeValue) {
    var secret = new SecretKeySpec(keyValue, "AES");
    var cipher = null;
    try {
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    } catch (e3) {
        cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
    }
    cipher.init(modeValue, secret);
    return cipher.doFinal(dataValue);
}

function sha256Bytes(dataValue) {
    var md = MessageDigest.getInstance("SHA-256");
    return md.digest(dataValue);
}

function deriveSessionKey(keyHexText, authBodyValue) {
    if (!authBodyValue || bytesLen(authBodyValue) < 16) throw "鉴权响应 body 长度不足 16";
    var pairedKey = hexToBytes(keyHexText);
    var randPart = sliceBytes(authBodyValue, 0, 16);
    return sliceBytes(sha256Bytes(concatBytes(pairedKey, randPart)), 0, 16);
}

function randomBytes16ForResetAuth() {
    var out = newBytes(16);
    for (var i = 0; i < 16; i++) bytesSet(out, i, secureRandomObj.nextInt(255));
    return out;
}

function buildResetAuthBody(keyHexText) {
    var randBytes = randomBytes16ForResetAuth();
    var pairedKey = hexToBytes(keyHexText);
    var digestBytes = sha256Bytes(concatBytes(randBytes, pairedKey));
    return concatBytes(randBytes, digestBytes);
}

function crc16LowByte(dataValue) {
    var crcValue = 0;
    for (var i = 0; i < bytesLen(dataValue); i++) {
        crcValue = (crcValue ^ ((bytesGet(dataValue, i) & 255) << 8)) & 65535;
        for (var j = 0; j < 8; j++) {
            if ((crcValue & 0x8000) !== 0) {
                crcValue = ((crcValue << 1) ^ 0xA8B3) & 65535;
            } else {
                crcValue = (crcValue << 1) & 65535;
            }
        }
    }
    return crcValue & 255;
}

function buildAimaFrame(cmdValue, bodyValue, keyValue) {
    var bodyLen = bodyValue ? bytesLen(bodyValue) : 0;
    var plain = newBytes(bodyLen + 3);
    bytesSet(plain, 0, bodyLen + 2);
    bytesSet(plain, 1, cmdValue);
    bytesSet(plain, 2, 0);
    for (var i = 0; i < bodyLen; i++) bytesSet(plain, 3 + i, bytesGet(bodyValue, i));
    bytesSet(plain, 2, crc16LowByte(plain));

    var payload = sliceBytes(plain, 1, bodyLen + 2);
    var encryptedFlag = keyValue !== null && typeof keyValue !== "undefined";
    var outPayload = encryptedFlag ? aesEcbRun(payload, keyValue, Cipher.ENCRYPT_MODE) : payload;
    var delta = Math.abs(bytesLen(outPayload) - bytesLen(payload)) & 0x8F;
    var head0 = (encryptedFlag ? 0x80 : 0) | delta;
    return concatBytes(bytesFromNumbers([head0, bytesLen(outPayload)]), outPayload);
}

function parseAimaFrame(dataValue, keyValue) {
    var lenValue = bytesLen(dataValue);
    if (lenValue < 4) return { ok: false, error: "通知长度过短", rawHex: bytesToHex(dataValue) };
    var head0 = bytesGet(dataValue, 0);
    var encryptedFlag = (head0 & 0x80) !== 0;
    var diffLen = head0 & 0x7F;
    var payloadLen = bytesGet(dataValue, 1);
    if (lenValue < 2 + payloadLen) return { ok: false, error: "payload 长度超过通知长度", rawHex: bytesToHex(dataValue) };
    var payload = sliceBytes(dataValue, 2, payloadLen);
    var originalLen = encryptedFlag ? payloadLen - diffLen : payloadLen;
    if (originalLen < 2) return { ok: false, error: "原始 payload 长度异常", rawHex: bytesToHex(dataValue) };
    if (encryptedFlag) {
        if (keyValue === null || typeof keyValue === "undefined") return { ok: false, error: "通知已加密但 session key 为空", rawHex: bytesToHex(dataValue) };
        var decrypted = aesEcbRun(payload, keyValue, Cipher.DECRYPT_MODE);
        if (bytesLen(decrypted) < originalLen) return { ok: false, error: "解密后长度不足", rawHex: bytesToHex(dataValue) };
        payload = sliceBytes(decrypted, 0, originalLen);
    }

    var cmdValue = bytesGet(payload, 0);
    var crcValue = bytesGet(payload, 1);
    var bodyLen = originalLen - 2;
    var bodyBytes = bodyLen > 0 ? sliceBytes(payload, 2, bodyLen) : bytesFromNumbers([]);
    var crcInput = concatBytes(bytesFromNumbers([originalLen, cmdValue, 0]), bodyBytes);
    var expectCrc = crc16LowByte(crcInput);
    if (expectCrc !== crcValue) {
        return {
            ok: false,
            error: "CRC 校验失败",
            rawHex: bytesToHex(dataValue),
            cmd: cmdValue,
            crc: crcValue,
            expectCrc: expectCrc,
            bodyHex: bytesToHex(bodyBytes)
        };
    }
    return {
        ok: true,
        cmd: cmdValue,
        encrypted: encryptedFlag,
        bodyBytes: bodyBytes,
        bodyHex: bytesToHex(bodyBytes),
        body0: bodyLen > 0 ? bytesGet(bodyBytes, 0) : -1,
        rawHex: bytesToHex(dataValue)
    };
}

function setFail(stateValue, textValue) {
    if (!stateValue.error) stateValue.error = String(textValue);
    try { stateValue.readyLatch.countDown(); } catch (e4) {}
    try { stateValue.resultLatch.countDown(); } catch (e5) {}
    try { stateValue.writeLatch.countDown(); } catch (e6) {}
}

function requestMtuOrReady(gattValue, stateValue) {
    stateValue.mtuRequested = true;
    if (Build.VERSION.SDK_INT >= 21) {
        var started = false;
        try { started = gattValue.requestMtu(511); } catch (e7) { started = false; }
        if (!started) stateValue.readyLatch.countDown();
    } else {
        stateValue.readyLatch.countDown();
    }
}

function findGattChars(gattValue, stateValue) {
    var services = gattValue.getServices();
    logText("GATT 服务数量=" + services.size());
    var bindUuidText = String(UUID_BIND_TEXT).toLowerCase();
    var controlUuidText = String(UUID_CONTROL_TEXT).toLowerCase();
    var queryUuidText = String(UUID_QUERY_TEXT).toLowerCase();
    var reportUuidText = String(UUID_REPORT_TEXT).toLowerCase();
    var serviceIt = services.iterator();
    var serviceIndex = 0;
    while (serviceIt.hasNext()) {
        var serviceObj = serviceIt.next();
        var serviceUuidText = String(serviceObj.getUuid().toString()).toLowerCase();
        var chars = serviceObj.getCharacteristics();
        logText("服务[" + serviceIndex + "] uuid=" + serviceUuidText + ", 特征数量=" + chars.size());
        var charIt = chars.iterator();
        var charIndex = 0;
        while (charIt.hasNext()) {
            var charObj = charIt.next();
            var uuidText = String(charObj.getUuid().toString()).toLowerCase();
            var propsValue = Number(charObj.getProperties());
            logText("  特征[" + serviceIndex + "." + charIndex + "] uuid=" + uuidText + ", props=0x" + propsValue.toString(16));
            if (uuidText === bindUuidText) {
                stateValue.bindChar = charObj;
                logText("  命中绑定/鉴权特征 7800");
            }
            if (uuidText === controlUuidText) {
                stateValue.controlChar = charObj;
                logText("  命中控制特征 7801");
            }
            if (uuidText === queryUuidText) {
                stateValue.queryChar = charObj;
                logText("  命中查询特征 7802");
            }
            if (uuidText === reportUuidText) {
                stateValue.reportChar = charObj;
                logText("  命中上报特征 7803");
            }
            if (uuidText === bindUuidText || uuidText === controlUuidText || uuidText === queryUuidText || uuidText === reportUuidText) {
                if ((propsValue & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0 || (propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
                    stateValue.notifyChars.push(charObj);
                    logText("  加入通知订阅队列: " + uuidText);
                }
            }
            charIndex++;
        }
        serviceIndex++;
    }
}

function enableNextNotify(gattValue, stateValue) {
    if (!stateValue.bindChar || !stateValue.controlChar || !stateValue.queryChar) {
        setFail(stateValue, "没有找到 7800/7801/7802 必要特征");
        return;
    }
    if (!stateValue.notifyChars || stateValue.notifyChars.length === 0) {
        setFail(stateValue, "没有找到可订阅的 7800/7801/7802/7803 通知特征");
        return;
    }
    if (stateValue.notifyIndex >= stateValue.notifyChars.length) {
        requestMtuOrReady(gattValue, stateValue);
        return;
    }

    var charObj = stateValue.notifyChars[stateValue.notifyIndex];
    stateValue.notifyIndex++;
    var propsValue = Number(charObj.getProperties());
    gattValue.setCharacteristicNotification(charObj, true);
    var desc = charObj.getDescriptor(UUID.fromString(CCC_UUID_TEXT));
    if (!desc) {
        enableNextNotify(gattValue, stateValue);
        return;
    }
    if ((propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
        desc.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    } else {
        desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    }
    if (!gattValue.writeDescriptor(desc)) {
        setFail(stateValue, "写入通知描述符失败 uuid=" + charObj.getUuid());
    }
}

function handleNotify(stateValue, dataValue) {
    var keyValue = null;
    if (stateValue.phase === "auth" && !stateValue.sessionKey) {
        keyValue = stateValue.authKey;
    } else if (stateValue.phase !== "resetAuth") {
        keyValue = stateValue.sessionKey;
    }
    var parsed = null;
    try {
        parsed = parseAimaFrame(dataValue, keyValue);
    } catch (parseErr) {
        logText("通知解析异常 phase=" + stateValue.phase + ", msg=" + parseErr + ", rawHex=" + bytesToHex(dataValue));
        return;
    }
    logText("收到通知 phase=" + stateValue.phase + ", info=" + makeJson({
        ok: parsed.ok,
        cmd: typeof parsed.cmd === "undefined" ? -1 : parsed.cmd,
        encrypted: parsed.encrypted === true,
        body0: typeof parsed.body0 === "undefined" ? -1 : parsed.body0,
        bodyHex: parsed.bodyHex || "",
        msg: parsed.error || ""
    }));
    if (!parsed.ok) return;
    if (parsed.cmd === 0x82) {
        stateValue.latestPowerInfo = parsed;
    }
    if (stateValue.phase === "auth" && parsed.cmd === 0x83 && parsed.bodyBytes && bytesLen(parsed.bodyBytes) >= 16) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "power" && parsed.cmd === 0x82) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "command" && parsed.cmd === 0x82) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
    }
}

function handleNotifyFromChar(stateValue, charValue, dataValue) {
    var uuidText = charValue ? String(charValue.getUuid()) : "";
    logText("收到原始通知 uuid=" + uuidText + ", len=" + bytesLen(dataValue) + ", hex=" + bytesToHex(dataValue));
    handleNotify(stateValue, dataValue);
}

function createGattCallback(stateValue) {
    return new JavaAdapter(BluetoothGattCallback, {
        onConnectionStateChange: function(gattObj, statusValue, newStateValue) {
            var devObj = null;
            var addrText = "";
            var nameText = "";
            try { devObj = gattObj.getDevice(); } catch (e11) { devObj = null; }
            if (devObj) {
                try { addrText = String(devObj.getAddress()); } catch (e12) { addrText = ""; }
                try { nameText = String(devObj.getName()); } catch (e13) { nameText = ""; }
            }
            var targetText = stateValue.targetMac || "";
            var matchedText = addrText && targetText ? String(String(addrText).toUpperCase() === String(targetText).toUpperCase()) : "unknown";
            logText("连接状态变化 status=" + statusValue + ", newState=" + newStateValue + ", targetMac=" + targetText + ", deviceMac=" + addrText + ", matched=" + matchedText + ", deviceName=" + nameText);
            if (Number(statusValue) === Number(BluetoothGatt.GATT_SUCCESS) && Number(newStateValue) === Number(BluetoothProfile.STATE_CONNECTED)) {
                stateValue.gatt = gattObj;
                if (!gattObj.discoverServices()) setFail(stateValue, "启动服务发现失败");
                return;
            }
            if (Number(newStateValue) === Number(BluetoothProfile.STATE_DISCONNECTED)) {
                setFail(stateValue, "蓝牙连接已断开 status=" + statusValue);
            }
        },
        onServicesDiscovered: function(gattObj, statusValue) {
            logText("服务发现 status=" + statusValue);
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "服务发现失败 status=" + statusValue);
                return;
            }
            findGattChars(gattObj, stateValue);
            enableNextNotify(gattObj, stateValue);
        },
        onDescriptorWrite: function(gattObj, descObj, statusValue) {
            logText("通知描述符写入 status=" + statusValue);
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "通知描述符写入失败 status=" + statusValue);
                return;
            }
            enableNextNotify(gattObj, stateValue);
        },
        onMtuChanged: function(gattObj, mtuValue, statusValue) {
            logText("MTU 结果 mtu=" + mtuValue + ", status=" + statusValue);
            if (stateValue.mtuRequested === true) {
                stateValue.readyLatch.countDown();
            } else {
                logText("忽略提前到达的 MTU 回调,等待服务发现和通知订阅完成");
            }
        },
        onCharacteristicWrite: function(gattObj, charObj, statusValue) {
            stateValue.writeStatus = Number(statusValue);
            try { stateValue.writeLatch.countDown(); } catch (e8) {}
        },
        onCharacteristicChanged: function(gattObj, charObj, valueBytes) {
            var dataValue = valueBytes ? valueBytes : charObj.getValue();
            handleNotifyFromChar(stateValue, charObj, dataValue);
        }
    });
}

function connectReady(macText) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter || !adapter.isEnabled()) throw "蓝牙未开启";
    var deviceObj = adapter.getRemoteDevice(macText);
    var state = {
        gatt: null,
        targetMac: macText,
        bindChar: null,
        controlChar: null,
        queryChar: null,
        reportChar: null,
        notifyChars: [],
        notifyIndex: 0,
        readyLatch: new CountDownLatch(1),
        resultLatch: new CountDownLatch(1),
        writeLatch: new CountDownLatch(1),
        writeStatus: -1,
        mtuRequested: false,
        authKey: null,
        sessionKey: null,
        latestPowerInfo: null,
        phase: "",
        resultInfo: null,
        error: ""
    };
    state.callbackObj = createGattCallback(state);
    logText("开始独立 GATT 连接 mac=" + macText);
    if (Build.VERSION.SDK_INT >= 23) {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj, BluetoothDevice.TRANSPORT_LE);
    } else {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj);
    }
    if (!state.gatt) throw "connectGatt 返回空";
    var ready = state.readyLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS);
    if (!ready) throw "连接/服务/通知/MTU 准备超时";
    if (state.error) throw state.error;
    if (!state.bindChar) throw "鉴权特征 7800 为空";
    if (!state.queryChar) throw "查询特征 7802 为空";
    if (!state.controlChar) throw "控制特征 7801 为空";
    return state;
}

function writeFrameToChar(stateValue, charValue, frameValue) {
    if (!charValue) throw "写入特征为空";
    var props = Number(charValue.getProperties());
    if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    } else if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    }
    stateValue.writeLatch = new CountDownLatch(1);
    stateValue.writeStatus = -1;
    charValue.setValue(frameValue);
    logText("写入帧 phase=" + stateValue.phase + ", uuid=" + charValue.getUuid() + ", len=" + bytesLen(frameValue) + ", hex=" + bytesToHex(frameValue));
    if (!stateValue.gatt.writeCharacteristic(charValue)) throw "写入特征启动失败";
    var done = stateValue.writeLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw "写入特征回调超时";
    if (stateValue.writeStatus !== Number(BluetoothGatt.GATT_SUCCESS)) throw "写入特征失败 status=" + stateValue.writeStatus;
}

function waitNotify(stateValue, phaseText) {
    var done = stateValue.resultLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "通知超时";
    if (!stateValue.resultInfo) throw phaseText + "没有解析结果";
    return stateValue.resultInfo;
}

function waitNotifyWithTimeout(stateValue, phaseText, timeoutValue) {
    var done = stateValue.resultLatch.await(timeoutValue, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "通知超时";
    if (!stateValue.resultInfo) throw phaseText + "没有解析结果";
    return stateValue.resultInfo;
}

function writeAndWaitNotify(stateValue, charValue, frameValue, phaseText) {
    stateValue.resultLatch = new CountDownLatch(1);
    stateValue.resultInfo = null;
    writeFrameToChar(stateValue, charValue, frameValue);
    return waitNotify(stateValue, phaseText);
}

function queryPowerWithRetry(stateValue) {
    var lastErrorText = "";
    if (stateValue.latestPowerInfo) {
        logText("使用已收到的 0x82 状态上报作为 power 结果");
        return stateValue.latestPowerInfo;
    }
    for (var attemptValue = 1; attemptValue <= 6; attemptValue++) {
        try {
            stateValue.phase = "power";
            stateValue.resultLatch = new CountDownLatch(1);
            stateValue.resultInfo = null;
            logText("状态查询尝试 attempt=" + attemptValue + "/6, timeoutMs=" + powerNotifyTimeoutMs);
            writeFrameToChar(stateValue, stateValue.queryChar, buildAimaFrame(0x02, bytesFromNumbers([]), stateValue.sessionKey));
            return waitNotifyWithTimeout(stateValue, "状态查询", powerNotifyTimeoutMs);
        } catch (queryErr) {
            lastErrorText = String(queryErr);
            logText("状态查询尝试失败 attempt=" + attemptValue + "/6, error=" + lastErrorText);
            if (attemptValue < 6) Thread.sleep(200);
        }
    }
    throw lastErrorText || "状态查询通知超时";
}

function closeGatt(stateValue) {
    if (!stateValue || !stateValue.gatt) return;
    try { stateValue.gatt.disconnect(); } catch (e9) {}
    try { stateValue.gatt.close(); } catch (e10) {}
}

function runBle(vehicleValue, modeText) {
    var macText = vehicleMac(vehicleValue);
    var keyText = vehicleKey(vehicleValue);
    var state = null;
    try {
        state = connectReady(macText);
        state.authKey = hexToBytes(keyText);

        state.phase = "resetAuth";
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x05, buildResetAuthBody(keyText), null));
        logText("重新鉴权前置帧已发送,等待后继续 0x03 鉴权");
        Thread.sleep(1000);

        state.phase = "auth";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x03, bytesFromNumbers([0]), state.authKey));
        var authInfo = waitNotify(state, "0x03鉴权");
        state.sessionKey = deriveSessionKey(keyText, authInfo.bodyBytes);
        logText("0x03 鉴权完成,已派生 session key");
        Thread.sleep(200);

        if (modeText === "power") {
            var powerInfo = queryPowerWithRetry(state);
            return makeJson({
                ok: powerInfo.cmd === 0x82,
                mode: modeText,
                mac: macText,
                cmd: powerInfo.cmd,
                powerStatus: powerInfo.body0,
                bodyHex: powerInfo.bodyHex,
                rawHex: powerInfo.rawHex
            });
        }

        if (modeText === "open" || modeText === "close") {
            state.phase = "command";
            state.resultLatch = new CountDownLatch(1);
            state.resultInfo = null;
            writeFrameToChar(state, state.controlChar, buildAimaFrame(0x02, bytesFromNumbers([modeText === "open" ? 1 : 0]), state.sessionKey));
            var cmdInfo = waitNotify(state, "上下电命令");
            return makeJson({
                ok: cmdInfo.cmd === 0x82,
                mode: modeText,
                mac: macText,
                cmd: cmdInfo.cmd,
                startupResultCode: cmdInfo.body0,
                bodyHex: cmdInfo.bodyHex,
                rawHex: cmdInfo.rawHex
            });
        }

        throw "不支持的模式: " + modeText;
    } finally {
        closeGatt(state);
    }
}

function main() {
    var vehicleValue = configuredVehicle();
    var macText = vehicleMac(vehicleValue);
    if (mode === "near") return isNearMac(macText) ? "true" : "false";
    if (mode === "power" || mode === "open" || mode === "close") return runBle(vehicleValue, mode);
    throw "不支持的模式: " + mode;
}

var finalText = "";
try {
    finalText = main();
} catch (err) {
    logText("执行失败: " + err);
    finalText = makeJson({ ok: false, mode: mode, error: String(err) });
}
finalText;

最佳答案

查看完整内容

目前只 在 near 状态发生变化时 才写全局变量。 这会带来一个问题:如果 RSSI 在 -50 上下小幅波动,near 一直为 true,则全局变量不会更新,其他触发器拿到的 rssi 可能是几轮之前的旧值。这与注释“按间隔覆盖写入 aimarssi”的预期不完全一致。 建议: · 要么每次扫描都写入(可以 10 秒一次,性能开销极小)。 · 要么至少保证 near 未变时也 定期强制更新一次(例如每 5 分钟写一次全量 JSON)。 · 如果只关心靠 ...

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

cos911 发表于 2026-6-24 20:21
xiaobaice 发表于 2026-7-6 00:44
resetAuth直接变成500也是可以的,我解包发现APP是发送后直接200后执行下一步。但目前我不太理解把7803加 ...

目前只 在 near 状态发生变化时 才写全局变量。
这会带来一个问题:如果 RSSI 在 -50 上下小幅波动,near 一直为 true,则全局变量不会更新,其他触发器拿到的 rssi 可能是几轮之前的旧值。这与注释“按间隔覆盖写入 aimarssi”的预期不完全一致。

建议:

· 要么每次扫描都写入(可以 10 秒一次,性能开销极小)。
· 要么至少保证 near 未变时也 定期强制更新一次(例如每 5 分钟写一次全量 JSON)。
· 如果只关心靠近/远离事件,当前策略没问题,但变量名和注释需要统一。

扫描失败后直接退出

当 scanResult.error 不为空时,循环直接 break,监控线程结束。
但有些错误是暂时的(如 errorCode 2:应用注册失败;或蓝牙临时不可用),直接退出可能让监控过早失效。

建议:

· 针对可恢复错误,记录日志后 continue 而不是 break,等待下次循环重试。
· 只有明确不可恢复的错误(如蓝牙被关闭、设备不支持 BLE)才退出。
· 或增加最大重试次数,避免无限重试导致日志刷屏。
蓝牙全局关闭未监听

如果系统蓝牙被关闭,扫描会失败并退出,但用户可能后续再打开蓝牙,脚本已死。
可选择性监听 BluetoothAdapter.ACTION_STATE_CHANGED,当蓝牙重新开启时继续扫描,或至少给出一个明确的结束原因。

scanOnce 中的轻微竞态

scanOnce 开始时设置 sessionObj.currentLatch 和 currentOutput,如果在设置之前有前一次扫描的延迟回调刚好触发,会检查到 currentOutput 为空而忽略,安全。
但更干净的写法是在 startScan 前用 synchronized 或保证赋值顺序,当前实现实际没问题,因为 Android 回调在 binder 线程,但在 ShortX 的 Rhino 环境下仍是顺序安全

. 关于“去掉 UUID 响应校验”的顾虑

你提到删掉 7803 的响应校验,只靠 RSSI 扫描会缺少“是否已连接/已认证”的确认。
对于你的独立扫描脚本来说:

· 它只负责提供 RSSI 和 near 标志,不直接执行上下电。
· 真正执行上下电的那个脚本可以使用 aimarssi.near 作为前提,再结合其他条件(如是否已连接 GATT、是否已通过认证等)来决定动作,这样职责分离更清晰。
· 因此,即使没有 UUID 校验,只要触发逻辑在另一个地方做了足够的安全判断,就没有问题。
  如果以后需要校验,可以在扫描到 near 后额外发起一次短暂的 GATT 连接,读取某个特征值来确认车辆存在,而不必依赖订阅。
fly农夫三拳 发表于 2026-6-25 12:15
 楼主| xiaobaice 发表于 2026-6-26 06:04
// ShortX ExecuteJS:爱玛独立 BLE 上下电切换(无事不扰,仅错日志版)

// ==================== [用户配置区] ====================
var TARGET_BLUETOOTH_KEY = ""; // 用户在此手动修改 Key

var connectTimeoutMs = 30000;      // 连接/服务发现超时 (毫秒)
var notifyTimeoutMs = 12000;       // 正常通知超时 (毫秒)
var powerNotifyTimeoutMs = 2000;   // 状态查询通知超时 (毫秒)
// ====================================================

var UUID_BIND_TEXT = "00007800-0000-1000-8000-00805F9B34FB";
var UUID_CONTROL_TEXT = "00007801-0000-1000-8000-00805F9B34FB";
var UUID_QUERY_TEXT = "00007802-0000-1000-8000-00805F9B34FB";
var CCC_UUID_TEXT = "00002902-0000-1000-8000-00805f9b34fb";

importClass(java.lang.StringBuilder);
importClass(java.lang.Thread);
importClass(java.util.UUID);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);
importClass(java.security.MessageDigest);
importClass(java.security.SecureRandom);
importClass(javax.crypto.Cipher);
importClass(javax.crypto.spec.SecretKeySpec);
importClass(android.bluetooth.BluetoothAdapter);
importClass(android.bluetooth.BluetoothDevice);
importClass(android.bluetooth.BluetoothGatt);
importClass(android.bluetooth.BluetoothGattCallback);
importClass(android.bluetooth.BluetoothGattCharacteristic);
importClass(android.bluetooth.BluetoothGattDescriptor);
importClass(android.bluetooth.BluetoothProfile);
importClass(android.os.Build);
importClass(org.json.JSONObject);

var JArray = Packages.java.lang.reflect.Array;
var ByteType = Packages.java.lang.Byte.TYPE;
var secureRandomObj = new SecureRandom();

function makeJson(objValue) {
    var json = new JSONObject();
    for (var nameValue in objValue) {
        if (objValue[nameValue] === null || typeof objValue[nameValue] === "undefined") {
            json.put(nameValue, JSONObject.NULL);
        } else {
            json.put(nameValue, objValue[nameValue]);
        }
    }
    return String(json.toString());
}

function targetBluetoothMac() {
    if (typeof btDeviceAddress === "undefined" || !btDeviceAddress) {
        console.error("[爱玛切换BLE] 错误: 上下文变量 btDeviceAddress 为空");
        throw "btDeviceAddress 为空";
    }
    return String(btDeviceAddress).toUpperCase();
}

function targetBluetoothKey() {
    var textValue = String(TARGET_BLUETOOTH_KEY || "");
    if (!textValue) {
        console.error("[爱玛切换BLE] 错误: 车辆缺少 TARGET_BLUETOOTH_KEY 配置");
        throw "TARGET_BLUETOOTH_KEY 为空";
    }
    return String(textValue);
}

function byteValue(numValue) {
    var v = Number(numValue) & 255;
    return v > 127 ? v - 256 : v;
}

function u8(numValue) {
    return Number(numValue) & 255;
}

function newBytes(lenValue) {
    return JArray.newInstance(ByteType, Number(lenValue));
}

function bytesLen(arrValue) {
    return JArray.getLength(arrValue);
}

function bytesGet(arrValue, indexValue) {
    return u8(JArray.getByte(arrValue, Number(indexValue)));
}

function bytesSet(arrValue, indexValue, numValue) {
    JArray.setByte(arrValue, Number(indexValue), byteValue(numValue));
}

function bytesFromNumbers(listValue) {
    var out = newBytes(listValue.length);
    for (var i = 0; i < listValue.length; i++) bytesSet(out, i, listValue[i]);
    return out;
}

function concatBytes() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += bytesLen(arguments[i]);
    var out = newBytes(total);
    var pos = 0;
    for (var p = 0; p < arguments.length; p++) {
        var one = arguments[p];
        for (var j = 0; j < bytesLen(one); j++) {
            bytesSet(out, pos, bytesGet(one, j));
            pos++;
        }
    }
    return out;
}

function sliceBytes(arrValue, startValue, lenValue) {
    var out = newBytes(lenValue);
    for (var i = 0; i < lenValue; i++) bytesSet(out, i, bytesGet(arrValue, startValue + i));
    return out;
}

function bytesToHex(arrValue) {
    var table = "0123456789ABCDEF";
    var sb = new StringBuilder();
    for (var i = 0; i < bytesLen(arrValue); i++) {
        var v = bytesGet(arrValue, i);
        sb.append(table.charAt((v >> 4) & 15));
        sb.append(table.charAt(v & 15));
    }
    return String(sb.toString());
}

function hexToBytes(hexValue) {
    var textValue = String(hexValue || "").replace(/\s+/g, "").toUpperCase();
    if (textValue.length % 2 !== 0) throw "hex 长度不是偶数";
    var out = newBytes(textValue.length / 2);
    for (var i = 0; i < textValue.length; i += 2) {
        bytesSet(out, i / 2, parseInt(textValue.substring(i, i + 2), 16));
    }
    return out;
}

function aesEcbRun(dataValue, keyValue, modeValue) {
    var secret = new SecretKeySpec(keyValue, "AES");
    var cipher = null;
    try {
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    } catch (e3) {
        cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
    }
    cipher.init(modeValue, secret);
    return cipher.doFinal(dataValue);
}

function sha256Bytes(dataValue) {
    var md = MessageDigest.getInstance("SHA-256");
    return md.digest(dataValue);
}

function deriveSessionKey(keyHexText, authBodyValue) {
    if (!authBodyValue || bytesLen(authBodyValue) < 16) throw "鉴权响应 body 长度不足 16";
    var pairedKey = hexToBytes(keyHexText);
    var randPart = sliceBytes(authBodyValue, 0, 16);
    return sliceBytes(sha256Bytes(concatBytes(pairedKey, randPart)), 0, 16);
}

function randomBytes16ForResetAuth() {
    var out = newBytes(16);
    for (var i = 0; i < 16; i++) bytesSet(out, i, secureRandomObj.nextInt(255));
    return out;
}

function buildResetAuthBody(keyHexText) {
    var randBytes = randomBytes16ForResetAuth();
    var pairedKey = hexToBytes(keyHexText);
    var digestBytes = sha256Bytes(concatBytes(randBytes, pairedKey));
    return concatBytes(randBytes, digestBytes);
}

function crc16LowByte(dataValue) {
    var crcValue = 0;
    for (var i = 0; i < bytesLen(dataValue); i++) {
        crcValue = (crcValue ^ ((bytesGet(dataValue, i) & 255) << 8)) & 65535;
        for (var j = 0; j < 8; j++) {
            if ((crcValue & 0x8000) !== 0) {
                crcValue = ((crcValue << 1) ^ 0xA8B3) & 65535;
            } else {
                crcValue = (crcValue << 1) & 65535;
            }
        }
    }
    return crcValue & 255;
}

function buildAimaFrame(cmdValue, bodyValue, keyValue) {
    var bodyLen = bodyValue ? bytesLen(bodyValue) : 0;
    var plain = newBytes(bodyLen + 3);
    bytesSet(plain, 0, bodyLen + 2);
    bytesSet(plain, 1, cmdValue);
    bytesSet(plain, 2, 0);
    for (var i = 0; i < bodyLen; i++) bytesSet(plain, 3 + i, bytesGet(bodyValue, i));
    bytesSet(plain, 2, crc16LowByte(plain));

    var payload = sliceBytes(plain, 1, bodyLen + 2);
    var encryptedFlag = keyValue !== null && typeof keyValue !== "undefined";
    var outPayload = encryptedFlag ? aesEcbRun(payload, keyValue, Cipher.ENCRYPT_MODE) : payload;
    var delta = Math.abs(bytesLen(outPayload) - bytesLen(payload)) & 0x8F;
    var head0 = (encryptedFlag ? 0x80 : 0) | delta;
    return concatBytes(bytesFromNumbers([head0, bytesLen(outPayload)]), outPayload);
}

function parseAimaFrame(dataValue, keyValue) {
    var lenValue = bytesLen(dataValue);
    if (lenValue < 4) return { ok: false, error: "通知长度过短", rawHex: bytesToHex(dataValue) };
    var head0 = bytesGet(dataValue, 0);
    var encryptedFlag = (head0 & 0x80) !== 0;
    var diffLen = head0 & 0x7F;
    var payloadLen = bytesGet(dataValue, 1);
    if (lenValue < 2 + payloadLen) return { ok: false, error: "payload 长度超过通知长度", rawHex: bytesToHex(dataValue) };
    var payload = sliceBytes(dataValue, 2, payloadLen);
    var originalLen = encryptedFlag ? payloadLen - diffLen : payloadLen;
    if (originalLen < 2) return { ok: false, error: "原始 payload 长度异常", rawHex: bytesToHex(dataValue) };
    if (encryptedFlag) {
        if (keyValue === null || typeof keyValue === "undefined") return { ok: false, error: "通知已加密但 session key 为空", rawHex: bytesToHex(dataValue) };
        var decrypted = aesEcbRun(payload, keyValue, Cipher.DECRYPT_MODE);
        if (bytesLen(decrypted) < originalLen) return { ok: false, error: "解密后长度不足", rawHex: bytesToHex(dataValue) };
        payload = sliceBytes(decrypted, 0, originalLen);
    }

    var cmdValue = bytesGet(payload, 0);
    var crcValue = bytesGet(payload, 1);
    var bodyLen = originalLen - 2;
    var bodyBytes = bodyLen > 0 ? sliceBytes(payload, 2, bodyLen) : bytesFromNumbers([]);
    var crcInput = concatBytes(bytesFromNumbers([originalLen, cmdValue, 0]), bodyBytes);
    var expectCrc = crc16LowByte(crcInput);
    if (expectCrc !== crcValue) {
        return {
            ok: false,
            error: "CRC 校验失败",
            rawHex: bytesToHex(dataValue),
            cmd: cmdValue,
            crc: crcValue,
            expectCrc: expectCrc,
            bodyHex: bytesToHex(bodyBytes)
        };
    }
    return {
        ok: true,
        cmd: cmdValue,
        encrypted: encryptedFlag,
        bodyBytes: bodyBytes,
        bodyHex: bytesToHex(bodyBytes),
        body0: bodyLen > 0 ? bytesGet(bodyBytes, 0) : -1,
        rawHex: bytesToHex(dataValue)
    };
}

function setFail(stateValue, textValue) {
    if (!stateValue.error) stateValue.error = String(textValue);
    try { stateValue.readyLatch.countDown(); } catch (e4) {}
    try { stateValue.resultLatch.countDown(); } catch (e5) {}
    try { stateValue.writeLatch.countDown(); } catch (e6) {}
}

function requestMtuOrReady(gattValue, stateValue) {
    stateValue.mtuRequested = true;
    if (Build.VERSION.SDK_INT >= 21) {
        var started = false;
        try { started = gattValue.requestMtu(511); } catch (e7) { started = false; }
        if (!started) stateValue.readyLatch.countDown();
    } else {
        stateValue.readyLatch.countDown();
    }
}

function findGattChars(gattValue, stateValue) {
    var services = gattValue.getServices();
    var bindUuidText = String(UUID_BIND_TEXT).toLowerCase();
    var controlUuidText = String(UUID_CONTROL_TEXT).toLowerCase();
    var queryUuidText = String(UUID_QUERY_TEXT).toLowerCase();
    var serviceIt = services.iterator();
    while (serviceIt.hasNext()) {
        var serviceObj = serviceIt.next();
        var chars = serviceObj.getCharacteristics();
        var charIt = chars.iterator();
        while (charIt.hasNext()) {
            var charObj = charIt.next();
            var uuidText = String(charObj.getUuid().toString()).toLowerCase();
            var propsValue = Number(charObj.getProperties());
            if (uuidText === bindUuidText) stateValue.bindChar = charObj;
            if (uuidText === controlUuidText) stateValue.controlChar = charObj;
            if (uuidText === queryUuidText) stateValue.queryChar = charObj;
            if (uuidText === bindUuidText || uuidText === controlUuidText || uuidText === queryUuidText) {
                if ((propsValue & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0 || (propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
                    stateValue.notifyChars.push(charObj);
                }
            }
        }
    }
}

function enableNextNotify(gattValue, stateValue) {
    if (!stateValue.bindChar || !stateValue.controlChar || !stateValue.queryChar) {
        setFail(stateValue, "没有找到 7800/7801/7802 必要特征");
        return;
    }
    if (!stateValue.notifyChars || stateValue.notifyChars.length === 0) {
        setFail(stateValue, "没有找到可订阅的通知特征");
        return;
    }
    if (stateValue.notifyIndex >= stateValue.notifyChars.length) {
        requestMtuOrReady(gattValue, stateValue);
        return;
    }

    var charObj = stateValue.notifyChars[stateValue.notifyIndex];
    stateValue.notifyIndex++;
    var propsValue = Number(charObj.getProperties());
    gattValue.setCharacteristicNotification(charObj, true);
    var desc = charObj.getDescriptor(UUID.fromString(CCC_UUID_TEXT));
    if (!desc) {
        enableNextNotify(gattValue, stateValue);
        return;
    }
    if ((propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
        desc.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    } else {
        desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    }
    if (!gattValue.writeDescriptor(desc)) {
        setFail(stateValue, "写入通知描述符失败 uuid=" + charObj.getUuid());
    }
}

function handleNotify(stateValue, dataValue, sourceUuidText) {
    var keyValue = null;
    if (stateValue.phase === "auth" && !stateValue.sessionKey) {
        keyValue = stateValue.authKey;
    } else if (stateValue.phase !== "resetAuth") {
        keyValue = stateValue.sessionKey;
    }
    var parsed = null;
    try {
        parsed = parseAimaFrame(dataValue, keyValue);
    } catch (parseErr) {
        return;
    }
    parsed.sourceUuid = sourceUuidText || "";
    if (!parsed.ok) return;
    if (stateValue.phase === "auth" && parsed.cmd === 0x83 && parsed.bodyBytes && bytesLen(parsed.bodyBytes) >= 16) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "power" && parsed.cmd === 0x82 && String(parsed.sourceUuid).toLowerCase() === String(UUID_QUERY_TEXT).toLowerCase()) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "command" && parsed.cmd === 0x82 && String(parsed.sourceUuid).toLowerCase() === String(UUID_CONTROL_TEXT).toLowerCase()) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
    }
}

function createGattCallback(stateValue) {
    return new JavaAdapter(BluetoothGattCallback, {
        onConnectionStateChange: function(gattObj, statusValue, newStateValue) {
            if (Number(statusValue) === Number(BluetoothGatt.GATT_SUCCESS) && Number(newStateValue) === Number(BluetoothProfile.STATE_CONNECTED)) {
                stateValue.gatt = gattObj;
                if (!gattObj.discoverServices()) setFail(stateValue, "启动服务发现失败");
                return;
            }
            if (Number(newStateValue) === Number(BluetoothProfile.STATE_DISCONNECTED)) {
                setFail(stateValue, "蓝牙连接已断开 status=" + statusValue);
            }
        },
        onServicesDiscovered: function(gattObj, statusValue) {
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "服务发现失败 status=" + statusValue);
                return;
            }
            findGattChars(gattObj, stateValue);
            enableNextNotify(gattObj, stateValue);
        },
        onDescriptorWrite: function(gattObj, descObj, statusValue) {
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "通知描述符写入失败 status=" + statusValue);
                return;
            }
            enableNextNotify(gattObj, stateValue);
        },
        onMtuChanged: function(gattObj, mtuValue, statusValue) {
            if (stateValue.mtuRequested === true) {
                stateValue.readyLatch.countDown();
            }
        },
        onCharacteristicWrite: function(gattObj, charObj, statusValue) {
            stateValue.writeStatus = Number(statusValue);
            try { stateValue.writeLatch.countDown(); } catch (e8) {}
        },
        onCharacteristicChanged: function(gattObj, charObj, valueBytes) {
            var dataValue = valueBytes ? valueBytes : charObj.getValue();
            var uuidText = charObj ? String(charObj.getUuid()) : "";
            handleNotify(stateValue, dataValue, uuidText);
        }
    });
}

function connectReady(macText) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter || !adapter.isEnabled()) throw "蓝牙功能不可用或未开启";
    var deviceObj = adapter.getRemoteDevice(macText);
    var state = {
        gatt: null,
        targetMac: macText,
        bindChar: null,
        controlChar: null,
        queryChar: null,
        notifyChars: [],
        notifyIndex: 0,
        readyLatch: new CountDownLatch(1),
        resultLatch: new CountDownLatch(1),
        writeLatch: new CountDownLatch(1),
        writeStatus: -1,
        mtuRequested: false,
        authKey: null,
        sessionKey: null,
        phase: "",
        resultInfo: null,
        error: ""
    };
    state.callbackObj = createGattCallback(state);
    if (Build.VERSION.SDK_INT >= 23) {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj, BluetoothDevice.TRANSPORT_LE);
    } else {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj);
    }
    if (!state.gatt) throw "connectGatt 返回空";
    var ready = state.readyLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS);
    if (!ready) throw "连接/服务发现/通知描述符配置超时";
    if (state.error) throw state.error;
    if (!state.bindChar || !state.queryChar || !state.controlChar) throw "未找到必要的爱玛服务特征锁";
    return state;
}

function writeFrameToChar(stateValue, charValue, frameValue) {
    if (!charValue) throw "写入特征为空";
    var props = Number(charValue.getProperties());
    if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    } else if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    }
    stateValue.writeLatch = new CountDownLatch(1);
    stateValue.writeStatus = -1;
    charValue.setValue(frameValue);
    if (!stateValue.gatt.writeCharacteristic(charValue)) throw "写入特征指令启动失败";
    var done = stateValue.writeLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw "特征值写入响应回调超时";
    if (stateValue.writeStatus !== Number(BluetoothGatt.GATT_SUCCESS)) throw "写入特征失败状态 status=" + stateValue.writeStatus;
}

function waitNotify(stateValue, phaseText) {
    var done = stateValue.resultLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "响应通知超时";
    if (!stateValue.resultInfo) throw phaseText + "缺少有效解析帧数据";
    return stateValue.resultInfo;
}

function waitNotifyWithTimeout(stateValue, phaseText, timeoutValue) {
    var done = stateValue.resultLatch.await(timeoutValue, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "响应通知超时";
    if (!stateValue.resultInfo) throw phaseText + "缺少有效解析状态数据";
    return stateValue.resultInfo;
}

function queryPowerWithRetry(stateValue) {
    var lastErrorText = "";
    for (var attemptValue = 1; attemptValue <= 6; attemptValue++) {
        try {
            stateValue.phase = "power";
            stateValue.resultLatch = new CountDownLatch(1);
            stateValue.resultInfo = null;
            writeFrameToChar(stateValue, stateValue.queryChar, buildAimaFrame(0x02, bytesFromNumbers([]), stateValue.sessionKey));
            return waitNotifyWithTimeout(stateValue, "状态查询", powerNotifyTimeoutMs);
        } catch (queryErr) {
            lastErrorText = String(queryErr);
            if (attemptValue < 6) Thread.sleep(200);
        }
    }
    throw lastErrorText || "状态查询重复尝试失败超时";
}

function closeGatt(stateValue) {
    if (!stateValue || !stateValue.gatt) return;
    try { stateValue.gatt.disconnect(); } catch (e9) {}
    try { stateValue.gatt.close(); } catch (e10) {}
}

function runToggle() {
    var macText = targetBluetoothMac();
    var keyText = targetBluetoothKey();
    var state = null;
    try {
        state = connectReady(macText);
        state.authKey = hexToBytes(keyText);

        state.phase = "resetAuth";
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x05, buildResetAuthBody(keyText), null));
        Thread.sleep(1000);

        state.phase = "auth";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x03, bytesFromNumbers([0]), state.authKey));
        var authInfo = waitNotify(state, "0x03鉴权");
        state.sessionKey = deriveSessionKey(keyText, authInfo.bodyBytes);
        Thread.sleep(200);

        var powerInfo = queryPowerWithRetry(state);
        var wasPowerOn = powerInfo.body0 !== 0;
        var targetAction = wasPowerOn ? "close" : "open";
        var targetBody = wasPowerOn ? 0 : 1;

        state.phase = "command";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.controlChar, buildAimaFrame(0x02, bytesFromNumbers([targetBody]), state.sessionKey));
        var cmdInfo = waitNotify(state, "上下电切换命令");
        return makeJson({
            ok: cmdInfo.cmd === 0x82,
            mode: "toggle",
            mac: macText,
            previousPowerStatus: powerInfo.body0,
            action: targetAction,
            commandBody: targetBody,
            cmd: cmdInfo.cmd,
            sourceUuid: cmdInfo.sourceUuid,
            resultCode: cmdInfo.body0,
            bodyHex: cmdInfo.bodyHex,
            rawHex: cmdInfo.rawHex
        });
    } finally {
        closeGatt(state);
    }
}

function main() {
    return runToggle();
}

var finalText = "";
try {
    finalText = main();
} catch (err) {
    console.error("[爱玛切换BLE] 致命异常中止: " + err); // 触发异常时打印控制台错误日志
    finalText = makeJson({ ok: false, mode: "toggle", error: String(err) });
    throw err; // 同时 throw 中断后续 ShortX 步骤
}
finalText;
cos911 发表于 2026-7-5 11:27
GATT 连接 + 服务发现        固定顺序等待,MTU 请求在通知订阅后才发起        connectTimeoutMs = 30000        可缩短超时,MTU 请求可提前或跳过
通知订阅        逐个特征使能,每个都需等待描述符写入回调        串行完成        可并行(但 GATT 限制,建议减少等待)
鉴权流程        重置鉴权帧后强制 sleep(1000)        固定 1 秒        可缩短至 300ms 或依赖回调
Power 查询        重试 6 次,每次超时 2s        总等待最多 12s        减少重试次数或缩短超时
整体连接        每次操作都重新建立连接(无法复用)        无法避免        若短时间连续操作,可考虑长连接(需改造架构)
当前 MTU 请求在服务发现和所有通知订阅完成后才发起,导致连接准备时间过长。可跳过 MTU 请求(多数蓝牙设备使用默认 23 字节已足够)或将其提前到服务发现后立即执行(与通知订阅并行)。
将鉴权前后的 Thread.sleep(1000) 和 Thread.sleep(200) 大幅缩减:
减少重试次数,改为 3 次,超时 1500ms
目前每次运行脚本都 connectGatt → 操作 → close。如果您的 ShortX 流程会连续执行多个操作(如先 power 再 open),可改造脚本为保持连接,通过全局变量缓存 state。但需注意连接超时断开风险,暂不推荐。
在关键节点(如 logText)打印时间戳,精确定位慢在哪一步。

检查实际蓝牙设备对 MTU 的支持,若支持 511 但请求耗时,可保留但提前执行。

确保 context 对象在 Rhino 环境中有效(代码中使用了 context,需确认 ShortX 已注入)。
 楼主| xiaobaice 发表于 2026-7-5 14:56
cos911 发表于 2026-7-5 11:27
GATT 连接 + 服务发现        固定顺序等待,MTU 请求在通知订阅后才发起        connectTimeoutMs = 30000        可缩短超时,M ...

下面是我之前优化的版本,你也可以看看
[JavaScript] 纯文本查看 复制代码
// ShortX ExecuteJS:根据全局变量 aimarssi 自动上下电

var UUID_BIND_TEXT = "00007800-0000-1000-8000-00805F9B34FB";
var UUID_CONTROL_TEXT = "00007801-0000-1000-8000-00805F9B34FB";
var UUID_QUERY_TEXT = "00007802-0000-1000-8000-00805F9B34FB";
var UUID_AIMA_SERVICE_TEXT = "0000FF78-0000-1000-8000-00805F9B34FB";
var CCC_UUID_TEXT = "00002902-0000-1000-8000-00805f9b34fb";

var CONNECT_TIMEOUT_MS = 8000;      // GATT 连接、服务发现、通知订阅和 MTU 准备超时
var NOTIFY_TIMEOUT_MS = 8000;       // 鉴权和上下电通知等待超时
var POWER_NOTIFY_TIMEOUT_MS = 2000;  // 状态查询通知等待超时
var RESET_AUTH_NOTIFY_TIMEOUT_MS = 500; // 0x05 resetAuth 等待 7800 原始通知的最长时间
var LOG_ENABLED = true;             // 是否输出 console 日志

importClass(java.lang.Thread);
importClass(java.util.UUID);
importClass(java.util.HexFormat);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);
importClass(java.security.MessageDigest);
importClass(java.security.SecureRandom);
importClass(javax.crypto.Cipher);
importClass(javax.crypto.spec.SecretKeySpec);
importClass(android.bluetooth.BluetoothAdapter);
importClass(android.bluetooth.BluetoothDevice);
importClass(android.bluetooth.BluetoothGatt);
importClass(android.bluetooth.BluetoothGattCallback);
importClass(android.bluetooth.BluetoothGattCharacteristic);
importClass(android.bluetooth.BluetoothGattDescriptor);
importClass(android.bluetooth.BluetoothProfile);
importClass(android.os.Build);

var JArray = Packages.java.lang.reflect.Array;
var ByteType = Packages.java.lang.Byte.TYPE;
var secureRandomObj = new SecureRandom();
var hexFormatter = HexFormat.of();

function logText(textValue) {
    if (!LOG_ENABLED) return;
    console.log("[爱玛RSSI自动BLE] " + String(textValue));
}

function readAimarssiState() {
    var parsed = JSON.parse(String(globalVarOf$aimarssi).trim());
    return {
        near: parsed.near === true,
        mac: String(parsed.mac).trim().toUpperCase(),
        rssi: Number(parsed.rssi),
        threshold: Number(parsed.threshold)
    };
}

function byteValue(numValue) {
    var v = Number(numValue) & 255;
    return v > 127 ? v - 256 : v;
}

function u8(numValue) {
    return Number(numValue) & 255;
}

function newBytes(lenValue) {
    return JArray.newInstance(ByteType, Number(lenValue));
}

function bytesLen(arrValue) {
    return JArray.getLength(arrValue);
}

function bytesGet(arrValue, indexValue) {
    return u8(JArray.getByte(arrValue, Number(indexValue)));
}

function bytesSet(arrValue, indexValue, numValue) {
    JArray.setByte(arrValue, Number(indexValue), byteValue(numValue));
}

function bytesFromNumbers(listValue) {
    var out = newBytes(listValue.length);
    for (var i = 0; i < listValue.length; i++) bytesSet(out, i, listValue[i]);
    return out;
}

function concatBytes() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += bytesLen(arguments[i]);
    var out = newBytes(total);
    var pos = 0;
    for (var p = 0; p < arguments.length; p++) {
        var one = arguments[p];
        for (var j = 0; j < bytesLen(one); j++) {
            bytesSet(out, pos, bytesGet(one, j));
            pos++;
        }
    }
    return out;
}

function sliceBytes(arrValue, startValue, lenValue) {
    var out = newBytes(lenValue);
    for (var i = 0; i < lenValue; i++) bytesSet(out, i, bytesGet(arrValue, startValue + i));
    return out;
}

function bytesToHex(arrValue) {
    return String(hexFormatter.formatHex(arrValue)).toUpperCase();
}

function hexToBytes(hexValue) {
    return hexFormatter.parseHex(String(hexValue || "").replace(/\s+/g, ""));
}

function aesEcbRun(dataValue, keyValue, modeValue) {
    var secret = new SecretKeySpec(keyValue, "AES");
    var cipher = null;
    try {
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    } catch (e3) {
        cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
    }
    cipher.init(modeValue, secret);
    return cipher.doFinal(dataValue);
}

function sha256Bytes(dataValue) {
    var md = MessageDigest.getInstance("SHA-256");
    return md.digest(dataValue);
}

function deriveSessionKey(keyHexText, authBodyValue) {
    if (!authBodyValue || bytesLen(authBodyValue) < 16) throw "鉴权响应 body 长度不足 16";
    var pairedKey = hexToBytes(keyHexText);
    var randPart = sliceBytes(authBodyValue, 0, 16);
    return sliceBytes(sha256Bytes(concatBytes(pairedKey, randPart)), 0, 16);
}

function randomBytes16ForResetAuth() {
    var out = newBytes(16);
    for (var i = 0; i < 16; i++) bytesSet(out, i, secureRandomObj.nextInt(255));
    return out;
}

function buildResetAuthBody(keyHexText) {
    var randBytes = randomBytes16ForResetAuth();
    var pairedKey = hexToBytes(keyHexText);
    var digestBytes = sha256Bytes(concatBytes(randBytes, pairedKey));
    return concatBytes(randBytes, digestBytes);
}

function crc16LowByte(dataValue) {
    var crcValue = 0;
    for (var i = 0; i < bytesLen(dataValue); i++) {
        crcValue = (crcValue ^ ((bytesGet(dataValue, i) & 255) << 8)) & 65535;
        for (var j = 0; j < 8; j++) {
            if ((crcValue & 0x8000) !== 0) {
                crcValue = ((crcValue << 1) ^ 0xA8B3) & 65535;
            } else {
                crcValue = (crcValue << 1) & 65535;
            }
        }
    }
    return crcValue & 255;
}

function buildAimaFrame(cmdValue, bodyValue, keyValue) {
    var bodyLen = bodyValue ? bytesLen(bodyValue) : 0;
    var plain = newBytes(bodyLen + 3);
    bytesSet(plain, 0, bodyLen + 2);
    bytesSet(plain, 1, cmdValue);
    bytesSet(plain, 2, 0);
    for (var i = 0; i < bodyLen; i++) bytesSet(plain, 3 + i, bytesGet(bodyValue, i));
    bytesSet(plain, 2, crc16LowByte(plain));

    var payload = sliceBytes(plain, 1, bodyLen + 2);
    var encryptedFlag = keyValue !== null && typeof keyValue !== "undefined";
    var outPayload = encryptedFlag ? aesEcbRun(payload, keyValue, Cipher.ENCRYPT_MODE) : payload;
    var delta = Math.abs(bytesLen(outPayload) - bytesLen(payload)) & 0x8F;
    var head0 = (encryptedFlag ? 0x80 : 0) | delta;
    return concatBytes(bytesFromNumbers([head0, bytesLen(outPayload)]), outPayload);
}

function parseAimaFrame(dataValue, keyValue) {
    var lenValue = bytesLen(dataValue);
    if (lenValue < 4) return { ok: false, error: "通知长度过短" };
    var head0 = bytesGet(dataValue, 0);
    var encryptedFlag = (head0 & 0x80) !== 0;
    var diffLen = head0 & 0x7F;
    var payloadLen = bytesGet(dataValue, 1);
    if (lenValue < 2 + payloadLen) return { ok: false, error: "payload 长度超过通知长度" };
    var payload = sliceBytes(dataValue, 2, payloadLen);
    var originalLen = encryptedFlag ? payloadLen - diffLen : payloadLen;
    if (originalLen < 2) return { ok: false, error: "原始 payload 长度异常" };
    if (encryptedFlag) {
        if (keyValue === null || typeof keyValue === "undefined") return { ok: false, error: "通知已加密但 session key 为空" };
        var decrypted = aesEcbRun(payload, keyValue, Cipher.DECRYPT_MODE);
        if (bytesLen(decrypted) < originalLen) return { ok: false, error: "解密后长度不足" };
        payload = sliceBytes(decrypted, 0, originalLen);
    }

    var cmdValue = bytesGet(payload, 0);
    var crcValue = bytesGet(payload, 1);
    var bodyLen = originalLen - 2;
    var bodyBytes = bodyLen > 0 ? sliceBytes(payload, 2, bodyLen) : bytesFromNumbers([]);
    var crcInput = concatBytes(bytesFromNumbers([originalLen, cmdValue, 0]), bodyBytes);
    var expectCrc = crc16LowByte(crcInput);
    if (expectCrc !== crcValue) {
        return {
            ok: false,
            error: "CRC 校验失败",
            cmd: cmdValue,
            crc: crcValue,
            expectCrc: expectCrc
        };
    }
    return {
        ok: true,
        cmd: cmdValue,
        encrypted: encryptedFlag,
        bodyBytes: bodyBytes,
        body0: bodyLen > 0 ? bytesGet(bodyBytes, 0) : -1
    };
}

function setFail(stateValue, textValue) {
    if (!stateValue.error) stateValue.error = String(textValue);
    stateValue.readyLatch.countDown();
    if (stateValue.waitLatch) stateValue.waitLatch.countDown();
    if (stateValue.resetAuthLatch) stateValue.resetAuthLatch.countDown();
    stateValue.writeLatch.countDown();
}

function requestMtuOrReady(gattValue, stateValue) {
    stateValue.mtuRequested = true;
    if (Build.VERSION.SDK_INT >= 21) {
        var started = false;
        try { started = gattValue.requestMtu(511); } catch (e7) { started = false; }
        if (!started) stateValue.readyLatch.countDown();
    } else {
        stateValue.readyLatch.countDown();
    }
}

function addNotifyChar(stateValue, charValue, nameText) {
    if (!charValue) {
        setFail(stateValue, "没有找到 " + nameText + " 特征");
        return false;
    }
    var propsValue = Number(charValue.getProperties());
    logText("命中特征 " + nameText + " uuid=" + charValue.getUuid() + ", props=0x" + propsValue.toString(16));
    if ((propsValue & BluetoothGattCharacteristic.PROPERTY_NOTIFY) === 0 &&
            (propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) === 0) {
        setFail(stateValue, nameText + " 特征不支持通知 props=0x" + propsValue.toString(16));
        return false;
    }
    stateValue.notifyChars.push(charValue);
    return true;
}

function resolveGattChars(gattValue, stateValue) {
    var serviceObj = gattValue.getService(UUID.fromString(UUID_AIMA_SERVICE_TEXT));
    if (!serviceObj) {
        setFail(stateValue, "没有找到爱玛服务 " + UUID_AIMA_SERVICE_TEXT);
        return false;
    }
    logText("命中爱玛服务 " + UUID_AIMA_SERVICE_TEXT);
    stateValue.bindChar = serviceObj.getCharacteristic(UUID.fromString(UUID_BIND_TEXT));
    stateValue.controlChar = serviceObj.getCharacteristic(UUID.fromString(UUID_CONTROL_TEXT));
    stateValue.queryChar = serviceObj.getCharacteristic(UUID.fromString(UUID_QUERY_TEXT));
    stateValue.notifyChars = [];
    stateValue.notifyIndex = 0;
    return addNotifyChar(stateValue, stateValue.bindChar, "7800 鉴权") &&
            addNotifyChar(stateValue, stateValue.controlChar, "7801 控制") &&
            addNotifyChar(stateValue, stateValue.queryChar, "7802 查询");
}

function enableNextNotify(gattValue, stateValue) {
    if (!stateValue.bindChar) {
        setFail(stateValue, "没有找到 7800 鉴权特征");
        return;
    }
    if (!stateValue.queryChar) {
        setFail(stateValue, "没有找到 7802 查询特征");
        return;
    }
    if (!stateValue.controlChar) {
        setFail(stateValue, "没有找到 7801 控制特征");
        return;
    }
    if (!stateValue.notifyChars || stateValue.notifyChars.length === 0) {
        setFail(stateValue, "没有找到当前模式需要订阅的通知特征");
        return;
    }
    if (stateValue.notifyIndex >= stateValue.notifyChars.length) {
        requestMtuOrReady(gattValue, stateValue);
        return;
    }

    var charObj = stateValue.notifyChars[stateValue.notifyIndex];
    stateValue.notifyIndex++;
    var propsValue = Number(charObj.getProperties());
    gattValue.setCharacteristicNotification(charObj, true);
    var desc = charObj.getDescriptor(UUID.fromString(CCC_UUID_TEXT));
    if (!desc) {
        enableNextNotify(gattValue, stateValue);
        return;
    }
    if ((propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
        desc.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    } else {
        desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    }
    if (!gattValue.writeDescriptor(desc)) {
        setFail(stateValue, "写入通知描述符失败 uuid=" + charObj.getUuid());
    }
}

function handleNotify(stateValue, dataValue, sourceUuidText) {
    var sourceText = String(sourceUuidText || "").toLowerCase();
    if (stateValue.phase === "resetAuth" && sourceText === String(UUID_BIND_TEXT).toLowerCase()) {
        if (LOG_ENABLED) logText("resetAuth 收到 7800 原始通知,提前继续 0x03");
        if (stateValue.resetAuthLatch) stateValue.resetAuthLatch.countDown();
        return;
    }

    var keyValue = null;
    if (stateValue.phase === "auth" && !stateValue.sessionKey) {
        keyValue = stateValue.authKey;
    } else if (stateValue.phase !== "resetAuth") {
        keyValue = stateValue.sessionKey;
    }
    var parsed = null;
    try {
        parsed = parseAimaFrame(dataValue, keyValue);
    } catch (parseErr) {
        if (LOG_ENABLED) logText("通知解析异常 phase=" + stateValue.phase + ", msg=" + parseErr + ", rawHex=" + bytesToHex(dataValue));
        return;
    }
    parsed.sourceUuid = sourceUuidText || "";
    if (LOG_ENABLED) logText("收到通知 phase=" + stateValue.phase + ", info=" + JSON.stringify({
        uuid: parsed.sourceUuid,
        ok: parsed.ok,
        cmd: typeof parsed.cmd === "undefined" ? -1 : parsed.cmd,
        encrypted: parsed.encrypted === true,
        body0: typeof parsed.body0 === "undefined" ? -1 : parsed.body0,
        msg: parsed.error || ""
    }));
    if (!parsed.ok) return;
    if (stateValue.waitLatch &&
            sourceText === stateValue.waitUuid &&
            Number(parsed.cmd) === Number(stateValue.waitCmd)) {
        stateValue.waitInfo = parsed;
        stateValue.waitLatch.countDown();
    }
}

function createGattCallback(stateValue) {
    return new JavaAdapter(BluetoothGattCallback, {
        onConnectionStateChange: function(gattObj, statusValue, newStateValue) {
            var devObj = gattObj.getDevice();
            var addrText = devObj ? String(devObj.getAddress()) : "";
            var nameText = devObj ? String(devObj.getName() || "") : "";
            var targetText = stateValue.targetMac || "";
            var matchedText = addrText && targetText ? String(String(addrText).toUpperCase() === String(targetText).toUpperCase()) : "unknown";
            logText("连接状态变化 status=" + statusValue + ", newState=" + newStateValue + ", targetMac=" + targetText + ", deviceMac=" + addrText + ", matched=" + matchedText + ", deviceName=" + nameText);
            if (Number(statusValue) === Number(BluetoothGatt.GATT_SUCCESS) && Number(newStateValue) === Number(BluetoothProfile.STATE_CONNECTED)) {
                stateValue.gatt = gattObj;
                if (!gattObj.discoverServices()) setFail(stateValue, "启动服务发现失败");
                return;
            }
            if (Number(newStateValue) === Number(BluetoothProfile.STATE_DISCONNECTED)) {
                setFail(stateValue, "蓝牙连接已断开 status=" + statusValue);
            }
        },
        onServicesDiscovered: function(gattObj, statusValue) {
            logText("服务发现 status=" + statusValue);
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "服务发现失败 status=" + statusValue);
                return;
            }
            if (!resolveGattChars(gattObj, stateValue)) return;
            enableNextNotify(gattObj, stateValue);
        },
        onDescriptorWrite: function(gattObj, descObj, statusValue) {
            logText("通知描述符写入 status=" + statusValue);
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "通知描述符写入失败 status=" + statusValue);
                return;
            }
            enableNextNotify(gattObj, stateValue);
        },
        onMtuChanged: function(gattObj, mtuValue, statusValue) {
            logText("MTU 结果 mtu=" + mtuValue + ", status=" + statusValue);
            if (stateValue.mtuRequested === true) {
                stateValue.readyLatch.countDown();
            } else {
                logText("忽略提前到达的 MTU 回调,等待服务发现和通知订阅完成");
            }
        },
        onCharacteristicWrite: function(gattObj, charObj, statusValue) {
            stateValue.writeStatus = Number(statusValue);
            stateValue.writeLatch.countDown();
        },
        onCharacteristicChanged: function(gattObj, charObj, valueBytes) {
            var dataValue = valueBytes ? valueBytes : charObj.getValue();
            var uuidText = charObj ? String(charObj.getUuid()) : "";
            if (LOG_ENABLED) logText("收到原始通知 uuid=" + uuidText + ", len=" + bytesLen(dataValue) + ", hex=" + bytesToHex(dataValue));
            handleNotify(stateValue, dataValue, uuidText);
        }
    });
}

function connectReady(macText) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter || !adapter.isEnabled()) throw "蓝牙未开启";
    var deviceObj = adapter.getRemoteDevice(macText);
    var state = {
        gatt: null,
        targetMac: macText,
        bindChar: null,
        controlChar: null,
        queryChar: null,
        notifyChars: [],
        notifyIndex: 0,
        readyLatch: new CountDownLatch(1),
        waitLatch: new CountDownLatch(1),
        resetAuthLatch: null,
        writeLatch: new CountDownLatch(1),
        writeStatus: -1,
        mtuRequested: false,
        authKey: null,
        sessionKey: null,
        phase: "",
        waitUuid: "",
        waitCmd: -1,
        waitInfo: null,
        error: ""
    };
    state.callbackObj = createGattCallback(state);
    logText("开始独立 GATT 连接 mac=" + macText);
    if (Build.VERSION.SDK_INT >= 23) {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj, BluetoothDevice.TRANSPORT_LE);
    } else {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj);
    }
    if (!state.gatt) throw "connectGatt 返回空";
    var ready = state.readyLatch.await(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    if (!ready) throw "连接/服务/通知/MTU 准备超时";
    if (state.error) throw state.error;
    if (!state.bindChar) throw "鉴权特征 7800 为空";
    if (!state.queryChar) throw "查询特征 7802 为空";
    if (!state.controlChar) throw "控制特征 7801 为空";
    return state;
}

function writeFrameToChar(stateValue, charValue, frameValue) {
    if (!charValue) throw "写入特征为空";
    var props = Number(charValue.getProperties());
    if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    } else if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    }
    stateValue.writeLatch = new CountDownLatch(1);
    stateValue.writeStatus = -1;
    charValue.setValue(frameValue);
    if (LOG_ENABLED) logText("写入帧 phase=" + stateValue.phase + ", uuid=" + charValue.getUuid() + ", len=" + bytesLen(frameValue) + ", hex=" + bytesToHex(frameValue));
    if (!stateValue.gatt.writeCharacteristic(charValue)) throw "写入特征启动失败";
    var done = stateValue.writeLatch.await(NOTIFY_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    if (!done) throw "写入特征回调超时";
    if (stateValue.writeStatus !== Number(BluetoothGatt.GATT_SUCCESS)) throw "写入特征失败 status=" + stateValue.writeStatus;
}

function sendAndAwait(stateValue, phaseValue, charValue, frameValue, expectUuidValue, expectCmdValue, timeoutValue) {
    stateValue.phase = phaseValue;
    stateValue.waitLatch = new CountDownLatch(1);
    stateValue.waitUuid = String(expectUuidValue).toLowerCase();
    stateValue.waitCmd = Number(expectCmdValue);
    stateValue.waitInfo = null;
    writeFrameToChar(stateValue, charValue, frameValue);
    var done = stateValue.waitLatch.await(timeoutValue || NOTIFY_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    var info = stateValue.waitInfo;
    stateValue.waitUuid = "";
    stateValue.waitCmd = -1;
    stateValue.waitInfo = null;
    if (stateValue.error) throw stateValue.error;
    if (!done) throw phaseValue + "通知超时";
    if (!info) throw phaseValue + "没有解析结果";
    return info;
}

function queryPowerWithRetry(stateValue) {
    var lastErrorText = "";
    for (var attemptValue = 1; attemptValue <= 6; attemptValue++) {
        try {
            logText("状态查询尝试 attempt=" + attemptValue + "/6, timeoutMs=" + POWER_NOTIFY_TIMEOUT_MS);
            return sendAndAwait(
                stateValue,
                "power",
                stateValue.queryChar,
                buildAimaFrame(0x02, bytesFromNumbers([]), stateValue.sessionKey),
                UUID_QUERY_TEXT,
                0x82,
                POWER_NOTIFY_TIMEOUT_MS
            );
        } catch (queryErr) {
            lastErrorText = String(queryErr);
            logText("状态查询尝试失败 attempt=" + attemptValue + "/6, error=" + lastErrorText);
            if (attemptValue < 6) Thread.sleep(200);
        }
    }
    throw lastErrorText || "状态查询通知超时";
}

function closeGatt(stateValue) {
    if (!stateValue || !stateValue.gatt) return;
    try { stateValue.gatt.disconnect(); } catch (e9) {}
    try { stateValue.gatt.close(); } catch (e10) {}
}

function runAimarssiPowerGuard() {
    var keyText = String(globalVarOf$aimakey).replace(/\s+/g, "");
    var rssiState = readAimarssiState();
    var macText = String(rssiState.mac).toUpperCase();
    var nearFlag = rssiState.near;
    logText("读取全局变量 aimarssi, near=" + nearFlag + ", mac=" + macText + ", rssi=" + rssiState.rssi + ", threshold=" + rssiState.threshold);

    var state = null;
    try {
        state = connectReady(macText);
        state.authKey = hexToBytes(keyText);

        state.phase = "resetAuth";
        state.resetAuthLatch = new CountDownLatch(1);
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x05, buildResetAuthBody(keyText), null));
        var resetNotified = state.resetAuthLatch.await(RESET_AUTH_NOTIFY_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        if (state.error) throw state.error;
        logText("重新鉴权前置帧已发送,resetAuthNotify=" + resetNotified + ",继续 0x03 鉴权");

        var authInfo = sendAndAwait(
            state,
            "auth",
            state.bindChar,
            buildAimaFrame(0x03, bytesFromNumbers([0]), state.authKey),
            UUID_BIND_TEXT,
            0x83,
            NOTIFY_TIMEOUT_MS
        );
        state.sessionKey = deriveSessionKey(keyText, authInfo.bodyBytes);
        logText("0x03 鉴权完成,已派生 session key");
        Thread.sleep(200);

        var powerInfo = queryPowerWithRetry(state);
        var powerOn = Number(powerInfo.body0) !== 0;
        var actionText = "stop";
        var commandBodyValue = -1;
        var reasonText = "";

        if (nearFlag && powerOn) {
            reasonText = "near_true_and_power_on";
        } else if (nearFlag && !powerOn) {
            actionText = "open";
            commandBodyValue = 1;
            reasonText = "near_true_and_power_off";
        } else if (!nearFlag && powerOn) {
            actionText = "close";
            commandBodyValue = 0;
            reasonText = "near_false_and_power_on";
        } else {
            reasonText = "near_false_and_power_off";
        }

        if (actionText === "stop") {
            logText("无需执行上下电命令,reason=" + reasonText + ", powerStatus=" + powerInfo.body0);
            return JSON.stringify({
                ok: true,
                mode: "aimarssiPowerGuard",
                mac: macText,
                near: nearFlag,
                rssi: rssiState.rssi,
                threshold: rssiState.threshold,
                powerStatus: powerInfo.body0,
                action: actionText,
                reason: reasonText
            });
        }

        logText("执行上下电命令 action=" + actionText + ", commandBody=" + commandBodyValue + ", reason=" + reasonText);
        var cmdInfo = sendAndAwait(
            state,
            "command",
            state.controlChar,
            buildAimaFrame(0x02, bytesFromNumbers([commandBodyValue]), state.sessionKey),
            UUID_CONTROL_TEXT,
            0x82,
            NOTIFY_TIMEOUT_MS
        );
        return JSON.stringify({
            ok: cmdInfo.cmd === 0x82,
            mode: "aimarssiPowerGuard",
            mac: macText,
            near: nearFlag,
            rssi: rssiState.rssi,
            threshold: rssiState.threshold,
            powerStatus: powerInfo.body0,
            action: actionText,
            commandBody: commandBodyValue,
            reason: reasonText,
            cmd: cmdInfo.cmd,
            sourceUuid: cmdInfo.sourceUuid,
            startupResultCode: cmdInfo.body0,
            resultCode: cmdInfo.body0
        });
    } finally {
        closeGatt(state);
    }
}

var finalText = "";
try {
    finalText = runAimarssiPowerGuard();
} catch (err) {
    logText("执行失败: " + err);
    finalText = JSON.stringify({ ok: false, mode: "aimarssiPowerGuard", action: "stop", error: String(err) });
}
finalText;
cos911 发表于 2026-7-5 22:03
cos911 发表于 2026-7-5 22:10
xiaobaice 发表于 2026-7-5 14:56
下面是我之前优化的版本,你也可以看看
[JavaScript] 纯文本查看 复制代码
// ShortX ExecuteJS:根据全局变 ...[/quote]

[mw_shl_code=python,true]// ShortX ExecuteJS:爱玛独立 BLE 上下电切换(优化版)
// 优化点:
// 1. 移除通知来源 UUID 严格校验,避免 0x82 状态上报被静默丢弃
// 2. closeGatt 增加延迟,确保断开指令完成发送
// 3. Power 查询重试次数从 6 次减少到 3 次
// 4. 缩短固定延时 (1000ms→300ms, 200ms→100ms)
// 5. 补充 7803 特征扫描提升兼容性
// 6. 优化错误提示,明确配置指引

// ==================== [用户配置区] ====================
var TARGET_BLUETOOTH_KEY = ""; // &#9888;&#65039; 用户在此手动修改车辆的蓝牙密钥

var connectTimeoutMs = 30000;      // 连接/服务发现超时 (毫秒)
var notifyTimeoutMs = 12000;       // 正常通知超时 (毫秒)
var powerNotifyTimeoutMs = 1500;   // 状态查询通知超时 (毫秒) - 优化:从 2000ms 降到 1500ms
// ====================================================

var UUID_BIND_TEXT = "00007800-0000-1000-8000-00805F9B34FB";
var UUID_CONTROL_TEXT = "00007801-0000-1000-8000-00805F9B34FB";
var UUID_QUERY_TEXT = "00007802-0000-1000-8000-00805F9B34FB";
var UUID_REPORT_TEXT = "00007803-0000-1000-8000-00805F9B34FB"; // 优化:新增 7803 特征扫描
var CCC_UUID_TEXT = "00002902-0000-1000-8000-00805f9b34fb";

importClass(java.lang.StringBuilder);
importClass(java.lang.Thread);
importClass(java.util.UUID);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);
importClass(java.security.MessageDigest);
importClass(java.security.SecureRandom);
importClass(javax.crypto.Cipher);
importClass(javax.crypto.spec.SecretKeySpec);
importClass(android.bluetooth.BluetoothAdapter);
importClass(android.bluetooth.BluetoothDevice);
importClass(android.bluetooth.BluetoothGatt);
importClass(android.bluetooth.BluetoothGattCallback);
importClass(android.bluetooth.BluetoothGattCharacteristic);
importClass(android.bluetooth.BluetoothGattDescriptor);
importClass(android.bluetooth.BluetoothProfile);
importClass(android.os.Build);
importClass(org.json.JSONObject);

var JArray = Packages.java.lang.reflect.Array;
var ByteType = Packages.java.lang.Byte.TYPE;
var secureRandomObj = new SecureRandom();

function makeJson(objValue) {
    var json = new JSONObject();
    for (var nameValue in objValue) {
        if (objValue[nameValue] === null || typeof objValue[nameValue] === "undefined") {
            json.put(nameValue, JSONObject.NULL);
        } else {
            json.put(nameValue, objValue[nameValue]);
        }
    }
    return String(json.toString());
}

function targetBluetoothMac() {
    if (typeof btDeviceAddress === "undefined" || !btDeviceAddress) {
        console.error("[爱玛切换BLE] 错误: 上下文变量 btDeviceAddress 为空");
        throw "btDeviceAddress 为空";
    }
    return String(btDeviceAddress).toUpperCase();
}

function targetBluetoothKey() {
    var textValue = String(TARGET_BLUETOOTH_KEY || "");
    if (!textValue) {
        console.error("[爱玛切换BLE] 错误: TARGET_BLUETOOTH_KEY 未配置,请在脚本开头填写车辆的蓝牙密钥");
        throw "TARGET_BLUETOOTH_KEY 为空";
    }
    return String(textValue);
}

function byteValue(numValue) {
    var v = Number(numValue) & 255;
    return v > 127 ? v - 256 : v;
}

function u8(numValue) {
    return Number(numValue) & 255;
}

function newBytes(lenValue) {
    return JArray.newInstance(ByteType, Number(lenValue));
}

function bytesLen(arrValue) {
    return JArray.getLength(arrValue);
}

function bytesGet(arrValue, indexValue) {
    return u8(JArray.getByte(arrValue, Number(indexValue)));
}

function bytesSet(arrValue, indexValue, numValue) {
    JArray.setByte(arrValue, Number(indexValue), byteValue(numValue));
}

function bytesFromNumbers(listValue) {
    var out = newBytes(listValue.length);
    for (var i = 0; i < listValue.length; i++) bytesSet(out, i, listValue[i]);
    return out;
}

function concatBytes() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += bytesLen(arguments[i]);
    var out = newBytes(total);
    var pos = 0;
    for (var p = 0; p < arguments.length; p++) {
        var one = arguments[p];
        for (var j = 0; j < bytesLen(one); j++) {
            bytesSet(out, pos, bytesGet(one, j));
            pos++;
        }
    }
    return out;
}

function sliceBytes(arrValue, startValue, lenValue) {
    var out = newBytes(lenValue);
    for (var i = 0; i < lenValue; i++) bytesSet(out, i, bytesGet(arrValue, startValue + i));
    return out;
}

function bytesToHex(arrValue) {
    var table = "0123456789ABCDEF";
    var sb = new StringBuilder();
    for (var i = 0; i < bytesLen(arrValue); i++) {
        var v = bytesGet(arrValue, i);
        sb.append(table.charAt((v >> 4) & 15));
        sb.append(table.charAt(v & 15));
    }
    return String(sb.toString());
}

function hexToBytes(hexValue) {
    var textValue = String(hexValue || "").replace(/\s+/g, "").toUpperCase();
    if (textValue.length % 2 !== 0) throw "hex 长度不是偶数";
    var out = newBytes(textValue.length / 2);
    for (var i = 0; i < textValue.length; i += 2) {
        bytesSet(out, i / 2, parseInt(textValue.substring(i, i + 2), 16));
    }
    return out;
}

function aesEcbRun(dataValue, keyValue, modeValue) {
    var secret = new SecretKeySpec(keyValue, "AES");
    var cipher = null;
    try {
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    } catch (e3) {
        cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
    }
    cipher.init(modeValue, secret);
    return cipher.doFinal(dataValue);
}

function sha256Bytes(dataValue) {
    var md = MessageDigest.getInstance("SHA-256");
    return md.digest(dataValue);
}

function deriveSessionKey(keyHexText, authBodyValue) {
    if (!authBodyValue || bytesLen(authBodyValue) < 16) throw "鉴权响应 body 长度不足 16";
    var pairedKey = hexToBytes(keyHexText);
    var randPart = sliceBytes(authBodyValue, 0, 16);
    return sliceBytes(sha256Bytes(concatBytes(pairedKey, randPart)), 0, 16);
}

function randomBytes16ForResetAuth() {
    var out = newBytes(16);
    for (var i = 0; i < 16; i++) bytesSet(out, i, secureRandomObj.nextInt(255));
    return out;
}

function buildResetAuthBody(keyHexText) {
    var randBytes = randomBytes16ForResetAuth();
    var pairedKey = hexToBytes(keyHexText);
    var digestBytes = sha256Bytes(concatBytes(randBytes, pairedKey));
    return concatBytes(randBytes, digestBytes);
}

function crc16LowByte(dataValue) {
    var crcValue = 0;
    for (var i = 0; i < bytesLen(dataValue); i++) {
        crcValue = (crcValue ^ ((bytesGet(dataValue, i) & 255) << 8)) & 65535;
        for (var j = 0; j < 8; j++) {
            if ((crcValue & 0x8000) !== 0) {
                crcValue = ((crcValue << 1) ^ 0xA8B3) & 65535;
            } else {
                crcValue = (crcValue << 1) & 65535;
            }
        }
    }
    return crcValue & 255;
}

function buildAimaFrame(cmdValue, bodyValue, keyValue) {
    var bodyLen = bodyValue ? bytesLen(bodyValue) : 0;
    var plain = newBytes(bodyLen + 3);
    bytesSet(plain, 0, bodyLen + 2);
    bytesSet(plain, 1, cmdValue);
    bytesSet(plain, 2, 0);
    for (var i = 0; i < bodyLen; i++) bytesSet(plain, 3 + i, bytesGet(bodyValue, i));
    bytesSet(plain, 2, crc16LowByte(plain));

    var payload = sliceBytes(plain, 1, bodyLen + 2);
    var encryptedFlag = keyValue !== null && typeof keyValue !== "undefined";
    var outPayload = encryptedFlag ? aesEcbRun(payload, keyValue, Cipher.ENCRYPT_MODE) : payload;
    var delta = Math.abs(bytesLen(outPayload) - bytesLen(payload)) & 0x8F;
    var head0 = (encryptedFlag ? 0x80 : 0) | delta;
    return concatBytes(bytesFromNumbers([head0, bytesLen(outPayload)]), outPayload);
}

function parseAimaFrame(dataValue, keyValue) {
    var lenValue = bytesLen(dataValue);
    if (lenValue < 4) return { ok: false, error: "通知长度过短", rawHex: bytesToHex(dataValue) };
    var head0 = bytesGet(dataValue, 0);
    var encryptedFlag = (head0 & 0x80) !== 0;
    var diffLen = head0 & 0x7F;
    var payloadLen = bytesGet(dataValue, 1);
    if (lenValue < 2 + payloadLen) return { ok: false, error: "payload 长度超过通知长度", rawHex: bytesToHex(dataValue) };
    var payload = sliceBytes(dataValue, 2, payloadLen);
    var originalLen = encryptedFlag ? payloadLen - diffLen : payloadLen;
    if (originalLen < 2) return { ok: false, error: "原始 payload 长度异常", rawHex: bytesToHex(dataValue) };
    if (encryptedFlag) {
        if (keyValue === null || typeof keyValue === "undefined") return { ok: false, error: "通知已加密但 session key 为空", rawHex: bytesToHex(dataValue) };
        var decrypted = aesEcbRun(payload, keyValue, Cipher.DECRYPT_MODE);
        if (bytesLen(decrypted) < originalLen) return { ok: false, error: "解密后长度不足", rawHex: bytesToHex(dataValue) };
        payload = sliceBytes(decrypted, 0, originalLen);
    }

    var cmdValue = bytesGet(payload, 0);
    var crcValue = bytesGet(payload, 1);
    var bodyLen = originalLen - 2;
    var bodyBytes = bodyLen > 0 ? sliceBytes(payload, 2, bodyLen) : bytesFromNumbers([]);
    var crcInput = concatBytes(bytesFromNumbers([originalLen, cmdValue, 0]), bodyBytes);
    var expectCrc = crc16LowByte(crcInput);
    if (expectCrc !== crcValue) {
        return {
            ok: false,
            error: "CRC 校验失败",
            rawHex: bytesToHex(dataValue),
            cmd: cmdValue,
            crc: crcValue,
            expectCrc: expectCrc,
            bodyHex: bytesToHex(bodyBytes)
        };
    }
    return {
        ok: true,
        cmd: cmdValue,
        encrypted: encryptedFlag,
        bodyBytes: bodyBytes,
        bodyHex: bytesToHex(bodyBytes),
        body0: bodyLen > 0 ? bytesGet(bodyBytes, 0) : -1,
        rawHex: bytesToHex(dataValue)
    };
}

function setFail(stateValue, textValue) {
    if (!stateValue.error) stateValue.error = String(textValue);
    try { stateValue.readyLatch.countDown(); } catch (e4) {}
    try { stateValue.resultLatch.countDown(); } catch (e5) {}
    try { stateValue.writeLatch.countDown(); } catch (e6) {}
}

function requestMtuOrReady(gattValue, stateValue) {
    stateValue.mtuRequested = true;
    if (Build.VERSION.SDK_INT >= 21) {
        var started = false;
        try { started = gattValue.requestMtu(511); } catch (e7) { started = false; }
        if (!started) stateValue.readyLatch.countDown();
    } else {
        stateValue.readyLatch.countDown();
    }
}

function findGattChars(gattValue, stateValue) {
    var services = gattValue.getServices();
    var bindUuidText = String(UUID_BIND_TEXT).toLowerCase();
    var controlUuidText = String(UUID_CONTROL_TEXT).toLowerCase();
    var queryUuidText = String(UUID_QUERY_TEXT).toLowerCase();
    var reportUuidText = String(UUID_REPORT_TEXT).toLowerCase(); // 优化:新增 7803
    var serviceIt = services.iterator();
    while (serviceIt.hasNext()) {
        var serviceObj = serviceIt.next();
        var chars = serviceObj.getCharacteristics();
        var charIt = chars.iterator();
        while (charIt.hasNext()) {
            var charObj = charIt.next();
            var uuidText = String(charObj.getUuid().toString()).toLowerCase();
            var propsValue = Number(charObj.getProperties());
            if (uuidText === bindUuidText) stateValue.bindChar = charObj;
            if (uuidText === controlUuidText) stateValue.controlChar = charObj;
            if (uuidText === queryUuidText) stateValue.queryChar = charObj;
            // 优化:检测 7803 但不强制要求
            if (uuidText === reportUuidText) stateValue.reportChar = charObj;
            // 优化:将 7803 也加入通知订阅队列(提升兼容性)
            if (uuidText === bindUuidText || uuidText === controlUuidText || uuidText === queryUuidText || uuidText === reportUuidText) {
                if ((propsValue & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0 || (propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
                    stateValue.notifyChars.push(charObj);
                }
            }
        }
    }
}

function enableNextNotify(gattValue, stateValue) {
    if (!stateValue.bindChar || !stateValue.controlChar || !stateValue.queryChar) {
        setFail(stateValue, "没有找到 7800/7801/7802 必要特征");
        return;
    }
    if (!stateValue.notifyChars || stateValue.notifyChars.length === 0) {
        setFail(stateValue, "没有找到可订阅的通知特征");
        return;
    }
    if (stateValue.notifyIndex >= stateValue.notifyChars.length) {
        requestMtuOrReady(gattValue, stateValue);
        return;
    }

    var charObj = stateValue.notifyChars[stateValue.notifyIndex];
    stateValue.notifyIndex++;
    var propsValue = Number(charObj.getProperties());
    gattValue.setCharacteristicNotification(charObj, true);
    var desc = charObj.getDescriptor(UUID.fromString(CCC_UUID_TEXT));
    if (!desc) {
        enableNextNotify(gattValue, stateValue);
        return;
    }
    if ((propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
        desc.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    } else {
        desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    }
    if (!gattValue.writeDescriptor(desc)) {
        setFail(stateValue, "写入通知描述符失败 uuid=" + charObj.getUuid());
    }
}

// 优化:移除 handleNotify 中的 sourceUuid 严格校验,避免 0x82 响应被丢弃
function handleNotify(stateValue, dataValue) {
    var keyValue = null;
    if (stateValue.phase === "auth" && !stateValue.sessionKey) {
        keyValue = stateValue.authKey;
    } else if (stateValue.phase !== "resetAuth") {
        keyValue = stateValue.sessionKey;
    }
    var parsed = null;
    try {
        parsed = parseAimaFrame(dataValue, keyValue);
    } catch (parseErr) {
        return;
    }
    if (!parsed.ok) return;
    
    // 优化:去掉 sourceUuid 校验,只根据阶段和命令码匹配
    if (stateValue.phase === "auth" && parsed.cmd === 0x83 && parsed.bodyBytes && bytesLen(parsed.bodyBytes) >= 16) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "power" && parsed.cmd === 0x82) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "command" && parsed.cmd === 0x82) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
    }
}

function createGattCallback(stateValue) {
    return new JavaAdapter(BluetoothGattCallback, {
        onConnectionStateChange: function(gattObj, statusValue, newStateValue) {
            if (Number(statusValue) === Number(BluetoothGatt.GATT_SUCCESS) && Number(newStateValue) === Number(BluetoothProfile.STATE_CONNECTED)) {
                stateValue.gatt = gattObj;
                if (!gattObj.discoverServices()) setFail(stateValue, "启动服务发现失败");
                return;
            }
            if (Number(newStateValue) === Number(BluetoothProfile.STATE_DISCONNECTED)) {
                setFail(stateValue, "蓝牙连接已断开 status=" + statusValue);
            }
        },
        onServicesDiscovered: function(gattObj, statusValue) {
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "服务发现失败 status=" + statusValue);
                return;
            }
            findGattChars(gattObj, stateValue);
            enableNextNotify(gattObj, stateValue);
        },
        onDescriptorWrite: function(gattObj, descObj, statusValue) {
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "通知描述符写入失败 status=" + statusValue);
                return;
            }
            enableNextNotify(gattObj, stateValue);
        },
        onMtuChanged: function(gattObj, mtuValue, statusValue) {
            if (stateValue.mtuRequested === true) {
                stateValue.readyLatch.countDown();
            }
        },
        onCharacteristicWrite: function(gattObj, charObj, statusValue) {
            stateValue.writeStatus = Number(statusValue);
            try { stateValue.writeLatch.countDown(); } catch (e8) {}
        },
        onCharacteristicChanged: function(gattObj, charObj, valueBytes) {
            var dataValue = valueBytes ? valueBytes : charObj.getValue();
            handleNotify(stateValue, dataValue); // 优化:不再传递 uuid
        }
    });
}

function connectReady(macText) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter || !adapter.isEnabled()) throw "蓝牙功能不可用或未开启";
    var deviceObj = adapter.getRemoteDevice(macText);
    var state = {
        gatt: null,
        targetMac: macText,
        bindChar: null,
        controlChar: null,
        queryChar: null,
        reportChar: null, // 优化:新增 7803
        notifyChars: [],
        notifyIndex: 0,
        readyLatch: new CountDownLatch(1),
        resultLatch: new CountDownLatch(1),
        writeLatch: new CountDownLatch(1),
        writeStatus: -1,
        mtuRequested: false,
        authKey: null,
        sessionKey: null,
        phase: "",
        resultInfo: null,
        error: ""
    };
    state.callbackObj = createGattCallback(state);
    if (Build.VERSION.SDK_INT >= 23) {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj, BluetoothDevice.TRANSPORT_LE);
    } else {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj);
    }
    if (!state.gatt) throw "connectGatt 返回空";
    var ready = state.readyLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS);
    if (!ready) throw "连接/服务发现/通知描述符配置超时";
    if (state.error) throw state.error;
    if (!state.bindChar || !state.queryChar || !state.controlChar) throw "未找到必要的爱玛服务特征锁";
    return state;
}

function writeFrameToChar(stateValue, charValue, frameValue) {
    if (!charValue) throw "写入特征为空";
    var props = Number(charValue.getProperties());
    if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    } else if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    }
    stateValue.writeLatch = new CountDownLatch(1);
    stateValue.writeStatus = -1;
    charValue.setValue(frameValue);
    if (!stateValue.gatt.writeCharacteristic(charValue)) throw "写入特征指令启动失败";
    var done = stateValue.writeLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw "特征值写入响应回调超时";
    if (stateValue.writeStatus !== Number(BluetoothGatt.GATT_SUCCESS)) throw "写入特征失败状态 status=" + stateValue.writeStatus;
}

function waitNotify(stateValue, phaseText) {
    var done = stateValue.resultLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "响应通知超时";
    if (!stateValue.resultInfo) throw phaseText + "缺少有效解析帧数据";
    return stateValue.resultInfo;
}

function waitNotifyWithTimeout(stateValue, phaseText, timeoutValue) {
    var done = stateValue.resultLatch.await(timeoutValue, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "响应通知超时";
    if (!stateValue.resultInfo) throw phaseText + "缺少有效解析状态数据";
    return stateValue.resultInfo;
}

// 优化:Power 查询重试次数从 6 次减少到 3 次
function queryPowerWithRetry(stateValue) {
    var lastErrorText = "";
    for (var attemptValue = 1; attemptValue <= 3; attemptValue++) {
        try {
            stateValue.phase = "power";
            stateValue.resultLatch = new CountDownLatch(1);
            stateValue.resultInfo = null;
            writeFrameToChar(stateValue, stateValue.queryChar, buildAimaFrame(0x02, bytesFromNumbers([]), stateValue.sessionKey));
            return waitNotifyWithTimeout(stateValue, "状态查询", powerNotifyTimeoutMs);
        } catch (queryErr) {
            lastErrorText = String(queryErr);
            if (attemptValue < 3) Thread.sleep(200);
        }
    }
    throw lastErrorText || "状态查询重复尝试失败超时";
}

// 优化:closeGatt 增加延迟,确保断开指令完成发送
function closeGatt(stateValue) {
    if (!stateValue || !stateValue.gatt) return;
    try { Thread.sleep(200); } catch (e) {}
    try { stateValue.gatt.disconnect(); } catch (e9) {}
    try { Thread.sleep(100); } catch (e) {}
    try { stateValue.gatt.close(); } catch (e10) {}
}

function runToggle() {
    var macText = targetBluetoothMac();
    var keyText = targetBluetoothKey();
    var state = null;
    try {
        state = connectReady(macText);
        state.authKey = hexToBytes(keyText);

        state.phase = "resetAuth";
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x05, buildResetAuthBody(keyText), null));
        // 优化:缩短固定延时 1000ms → 300ms
        Thread.sleep(300);

        state.phase = "auth";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x03, bytesFromNumbers([0]), state.authKey));
        var authInfo = waitNotify(state, "0x03鉴权");
        state.sessionKey = deriveSessionKey(keyText, authInfo.bodyBytes);
        // 优化:缩短固定延时 200ms → 100ms
        Thread.sleep(100);

        var powerInfo = queryPowerWithRetry(state);
        var wasPowerOn = powerInfo.body0 !== 0;
        var targetAction = wasPowerOn ? "close" : "open";
        var targetBody = wasPowerOn ? 0 : 1;

        state.phase = "command";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.controlChar, buildAimaFrame(0x02, bytesFromNumbers([targetBody]), state.sessionKey));
        var cmdInfo = waitNotify(state, "上下电切换命令");
        return makeJson({
            ok: cmdInfo.cmd === 0x82,
            mode: "toggle",
            mac: macText,
            previousPowerStatus: powerInfo.body0,
            action: targetAction,
            commandBody: targetBody,
            cmd: cmdInfo.cmd,
            resultCode: cmdInfo.body0,
            bodyHex: cmdInfo.bodyHex,
            rawHex: cmdInfo.rawHex
        });
    } finally {
        closeGatt(state);
    }
}

function main() {
    return runToggle();
}

var finalText = "";
try {
    finalText = main();
} catch (err) {
    console.error("[爱玛切换BLE] 致命异常中止: " + err);
    finalText = makeJson({ ok: false, mode: "toggle", error: String(err) });
    throw err;
}
finalText;

我又帮你优化设计了一下,你看看
cos911 发表于 2026-7-5 22:24
xiaobaice 发表于 2026-7-5 14:56
下面是我之前优化的版本,你也可以看看
[JavaScript] 纯文本查看 复制代码
// ShortX ExecuteJS:根据全局变 ...[/quote]

[mw_shl_code=javascript,true]// ShortX ExecuteJS:爱玛独立 BLE 上下电切换(优化版)
// 优化点:
// 1. 移除通知来源 UUID 严格校验,避免 0x82 状态上报被静默丢弃
// 2. closeGatt 增加延迟,确保断开指令完成发送
// 3. Power 查询重试次数从 6 次减少到 3 次
// 4. 缩短固定延时 (1000ms→300ms, 200ms→100ms)
// 5. 补充 7803 特征扫描提升兼容性
// 6. 优化错误提示,明确配置指引

// ==================== [用户配置区] ====================
var TARGET_BLUETOOTH_KEY = ""; // &#9888;&#65039; 用户在此手动修改车辆的蓝牙密钥

var connectTimeoutMs = 30000;      // 连接/服务发现超时 (毫秒)
var notifyTimeoutMs = 12000;       // 正常通知超时 (毫秒)
var powerNotifyTimeoutMs = 1500;   // 状态查询通知超时 (毫秒) - 优化:从 2000ms 降到 1500ms
// ====================================================

var UUID_BIND_TEXT = "00007800-0000-1000-8000-00805F9B34FB";
var UUID_CONTROL_TEXT = "00007801-0000-1000-8000-00805F9B34FB";
var UUID_QUERY_TEXT = "00007802-0000-1000-8000-00805F9B34FB";
var UUID_REPORT_TEXT = "00007803-0000-1000-8000-00805F9B34FB"; // 优化:新增 7803 特征扫描
var CCC_UUID_TEXT = "00002902-0000-1000-8000-00805f9b34fb";

importClass(java.lang.StringBuilder);
importClass(java.lang.Thread);
importClass(java.util.UUID);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);
importClass(java.security.MessageDigest);
importClass(java.security.SecureRandom);
importClass(javax.crypto.Cipher);
importClass(javax.crypto.spec.SecretKeySpec);
importClass(android.bluetooth.BluetoothAdapter);
importClass(android.bluetooth.BluetoothDevice);
importClass(android.bluetooth.BluetoothGatt);
importClass(android.bluetooth.BluetoothGattCallback);
importClass(android.bluetooth.BluetoothGattCharacteristic);
importClass(android.bluetooth.BluetoothGattDescriptor);
importClass(android.bluetooth.BluetoothProfile);
importClass(android.os.Build);
importClass(org.json.JSONObject);

var JArray = Packages.java.lang.reflect.Array;
var ByteType = Packages.java.lang.Byte.TYPE;
var secureRandomObj = new SecureRandom();

function makeJson(objValue) {
    var json = new JSONObject();
    for (var nameValue in objValue) {
        if (objValue[nameValue] === null || typeof objValue[nameValue] === "undefined") {
            json.put(nameValue, JSONObject.NULL);
        } else {
            json.put(nameValue, objValue[nameValue]);
        }
    }
    return String(json.toString());
}

function targetBluetoothMac() {
    if (typeof btDeviceAddress === "undefined" || !btDeviceAddress) {
        console.error("[爱玛切换BLE] 错误: 上下文变量 btDeviceAddress 为空");
        throw "btDeviceAddress 为空";
    }
    return String(btDeviceAddress).toUpperCase();
}

function targetBluetoothKey() {
    var textValue = String(TARGET_BLUETOOTH_KEY || "");
    if (!textValue) {
        console.error("[爱玛切换BLE] 错误: TARGET_BLUETOOTH_KEY 未配置,请在脚本开头填写车辆的蓝牙密钥");
        throw "TARGET_BLUETOOTH_KEY 为空";
    }
    return String(textValue);
}

function byteValue(numValue) {
    var v = Number(numValue) & 255;
    return v > 127 ? v - 256 : v;
}

function u8(numValue) {
    return Number(numValue) & 255;
}

function newBytes(lenValue) {
    return JArray.newInstance(ByteType, Number(lenValue));
}

function bytesLen(arrValue) {
    return JArray.getLength(arrValue);
}

function bytesGet(arrValue, indexValue) {
    return u8(JArray.getByte(arrValue, Number(indexValue)));
}

function bytesSet(arrValue, indexValue, numValue) {
    JArray.setByte(arrValue, Number(indexValue), byteValue(numValue));
}

function bytesFromNumbers(listValue) {
    var out = newBytes(listValue.length);
    for (var i = 0; i < listValue.length; i++) bytesSet(out, i, listValue[i]);
    return out;
}

function concatBytes() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += bytesLen(arguments[i]);
    var out = newBytes(total);
    var pos = 0;
    for (var p = 0; p < arguments.length; p++) {
        var one = arguments[p];
        for (var j = 0; j < bytesLen(one); j++) {
            bytesSet(out, pos, bytesGet(one, j));
            pos++;
        }
    }
    return out;
}

function sliceBytes(arrValue, startValue, lenValue) {
    var out = newBytes(lenValue);
    for (var i = 0; i < lenValue; i++) bytesSet(out, i, bytesGet(arrValue, startValue + i));
    return out;
}

function bytesToHex(arrValue) {
    var table = "0123456789ABCDEF";
    var sb = new StringBuilder();
    for (var i = 0; i < bytesLen(arrValue); i++) {
        var v = bytesGet(arrValue, i);
        sb.append(table.charAt((v >> 4) & 15));
        sb.append(table.charAt(v & 15));
    }
    return String(sb.toString());
}

function hexToBytes(hexValue) {
    var textValue = String(hexValue || "").replace(/\s+/g, "").toUpperCase();
    if (textValue.length % 2 !== 0) throw "hex 长度不是偶数";
    var out = newBytes(textValue.length / 2);
    for (var i = 0; i < textValue.length; i += 2) {
        bytesSet(out, i / 2, parseInt(textValue.substring(i, i + 2), 16));
    }
    return out;
}

function aesEcbRun(dataValue, keyValue, modeValue) {
    var secret = new SecretKeySpec(keyValue, "AES");
    var cipher = null;
    try {
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    } catch (e3) {
        cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
    }
    cipher.init(modeValue, secret);
    return cipher.doFinal(dataValue);
}

function sha256Bytes(dataValue) {
    var md = MessageDigest.getInstance("SHA-256");
    return md.digest(dataValue);
}

function deriveSessionKey(keyHexText, authBodyValue) {
    if (!authBodyValue || bytesLen(authBodyValue) < 16) throw "鉴权响应 body 长度不足 16";
    var pairedKey = hexToBytes(keyHexText);
    var randPart = sliceBytes(authBodyValue, 0, 16);
    return sliceBytes(sha256Bytes(concatBytes(pairedKey, randPart)), 0, 16);
}

function randomBytes16ForResetAuth() {
    var out = newBytes(16);
    for (var i = 0; i < 16; i++) bytesSet(out, i, secureRandomObj.nextInt(255));
    return out;
}

function buildResetAuthBody(keyHexText) {
    var randBytes = randomBytes16ForResetAuth();
    var pairedKey = hexToBytes(keyHexText);
    var digestBytes = sha256Bytes(concatBytes(randBytes, pairedKey));
    return concatBytes(randBytes, digestBytes);
}

function crc16LowByte(dataValue) {
    var crcValue = 0;
    for (var i = 0; i < bytesLen(dataValue); i++) {
        crcValue = (crcValue ^ ((bytesGet(dataValue, i) & 255) << 8)) & 65535;
        for (var j = 0; j < 8; j++) {
            if ((crcValue & 0x8000) !== 0) {
                crcValue = ((crcValue << 1) ^ 0xA8B3) & 65535;
            } else {
                crcValue = (crcValue << 1) & 65535;
            }
        }
    }
    return crcValue & 255;
}

function buildAimaFrame(cmdValue, bodyValue, keyValue) {
    var bodyLen = bodyValue ? bytesLen(bodyValue) : 0;
    var plain = newBytes(bodyLen + 3);
    bytesSet(plain, 0, bodyLen + 2);
    bytesSet(plain, 1, cmdValue);
    bytesSet(plain, 2, 0);
    for (var i = 0; i < bodyLen; i++) bytesSet(plain, 3 + i, bytesGet(bodyValue, i));
    bytesSet(plain, 2, crc16LowByte(plain));

    var payload = sliceBytes(plain, 1, bodyLen + 2);
    var encryptedFlag = keyValue !== null && typeof keyValue !== "undefined";
    var outPayload = encryptedFlag ? aesEcbRun(payload, keyValue, Cipher.ENCRYPT_MODE) : payload;
    var delta = Math.abs(bytesLen(outPayload) - bytesLen(payload)) & 0x8F;
    var head0 = (encryptedFlag ? 0x80 : 0) | delta;
    return concatBytes(bytesFromNumbers([head0, bytesLen(outPayload)]), outPayload);
}

function parseAimaFrame(dataValue, keyValue) {
    var lenValue = bytesLen(dataValue);
    if (lenValue < 4) return { ok: false, error: "通知长度过短", rawHex: bytesToHex(dataValue) };
    var head0 = bytesGet(dataValue, 0);
    var encryptedFlag = (head0 & 0x80) !== 0;
    var diffLen = head0 & 0x7F;
    var payloadLen = bytesGet(dataValue, 1);
    if (lenValue < 2 + payloadLen) return { ok: false, error: "payload 长度超过通知长度", rawHex: bytesToHex(dataValue) };
    var payload = sliceBytes(dataValue, 2, payloadLen);
    var originalLen = encryptedFlag ? payloadLen - diffLen : payloadLen;
    if (originalLen < 2) return { ok: false, error: "原始 payload 长度异常", rawHex: bytesToHex(dataValue) };
    if (encryptedFlag) {
        if (keyValue === null || typeof keyValue === "undefined") return { ok: false, error: "通知已加密但 session key 为空", rawHex: bytesToHex(dataValue) };
        var decrypted = aesEcbRun(payload, keyValue, Cipher.DECRYPT_MODE);
        if (bytesLen(decrypted) < originalLen) return { ok: false, error: "解密后长度不足", rawHex: bytesToHex(dataValue) };
        payload = sliceBytes(decrypted, 0, originalLen);
    }

    var cmdValue = bytesGet(payload, 0);
    var crcValue = bytesGet(payload, 1);
    var bodyLen = originalLen - 2;
    var bodyBytes = bodyLen > 0 ? sliceBytes(payload, 2, bodyLen) : bytesFromNumbers([]);
    var crcInput = concatBytes(bytesFromNumbers([originalLen, cmdValue, 0]), bodyBytes);
    var expectCrc = crc16LowByte(crcInput);
    if (expectCrc !== crcValue) {
        return {
            ok: false,
            error: "CRC 校验失败",
            rawHex: bytesToHex(dataValue),
            cmd: cmdValue,
            crc: crcValue,
            expectCrc: expectCrc,
            bodyHex: bytesToHex(bodyBytes)
        };
    }
    return {
        ok: true,
        cmd: cmdValue,
        encrypted: encryptedFlag,
        bodyBytes: bodyBytes,
        bodyHex: bytesToHex(bodyBytes),
        body0: bodyLen > 0 ? bytesGet(bodyBytes, 0) : -1,
        rawHex: bytesToHex(dataValue)
    };
}

function setFail(stateValue, textValue) {
    if (!stateValue.error) stateValue.error = String(textValue);
    try { stateValue.readyLatch.countDown(); } catch (e4) {}
    try { stateValue.resultLatch.countDown(); } catch (e5) {}
    try { stateValue.writeLatch.countDown(); } catch (e6) {}
}

function requestMtuOrReady(gattValue, stateValue) {
    stateValue.mtuRequested = true;
    if (Build.VERSION.SDK_INT >= 21) {
        var started = false;
        try { started = gattValue.requestMtu(511); } catch (e7) { started = false; }
        if (!started) stateValue.readyLatch.countDown();
    } else {
        stateValue.readyLatch.countDown();
    }
}

function findGattChars(gattValue, stateValue) {
    var services = gattValue.getServices();
    var bindUuidText = String(UUID_BIND_TEXT).toLowerCase();
    var controlUuidText = String(UUID_CONTROL_TEXT).toLowerCase();
    var queryUuidText = String(UUID_QUERY_TEXT).toLowerCase();
    var reportUuidText = String(UUID_REPORT_TEXT).toLowerCase(); // 优化:新增 7803
    var serviceIt = services.iterator();
    while (serviceIt.hasNext()) {
        var serviceObj = serviceIt.next();
        var chars = serviceObj.getCharacteristics();
        var charIt = chars.iterator();
        while (charIt.hasNext()) {
            var charObj = charIt.next();
            var uuidText = String(charObj.getUuid().toString()).toLowerCase();
            var propsValue = Number(charObj.getProperties());
            if (uuidText === bindUuidText) stateValue.bindChar = charObj;
            if (uuidText === controlUuidText) stateValue.controlChar = charObj;
            if (uuidText === queryUuidText) stateValue.queryChar = charObj;
            // 优化:检测 7803 但不强制要求
            if (uuidText === reportUuidText) stateValue.reportChar = charObj;
            // 优化:将 7803 也加入通知订阅队列(提升兼容性)
            if (uuidText === bindUuidText || uuidText === controlUuidText || uuidText === queryUuidText || uuidText === reportUuidText) {
                if ((propsValue & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0 || (propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
                    stateValue.notifyChars.push(charObj);
                }
            }
        }
    }
}

function enableNextNotify(gattValue, stateValue) {
    if (!stateValue.bindChar || !stateValue.controlChar || !stateValue.queryChar) {
        setFail(stateValue, "没有找到 7800/7801/7802 必要特征");
        return;
    }
    if (!stateValue.notifyChars || stateValue.notifyChars.length === 0) {
        setFail(stateValue, "没有找到可订阅的通知特征");
        return;
    }
    if (stateValue.notifyIndex >= stateValue.notifyChars.length) {
        requestMtuOrReady(gattValue, stateValue);
        return;
    }

    var charObj = stateValue.notifyChars[stateValue.notifyIndex];
    stateValue.notifyIndex++;
    var propsValue = Number(charObj.getProperties());
    gattValue.setCharacteristicNotification(charObj, true);
    var desc = charObj.getDescriptor(UUID.fromString(CCC_UUID_TEXT));
    if (!desc) {
        enableNextNotify(gattValue, stateValue);
        return;
    }
    if ((propsValue & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0) {
        desc.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    } else {
        desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    }
    if (!gattValue.writeDescriptor(desc)) {
        setFail(stateValue, "写入通知描述符失败 uuid=" + charObj.getUuid());
    }
}

// 优化:移除 handleNotify 中的 sourceUuid 严格校验,避免 0x82 响应被丢弃
function handleNotify(stateValue, dataValue) {
    var keyValue = null;
    if (stateValue.phase === "auth" && !stateValue.sessionKey) {
        keyValue = stateValue.authKey;
    } else if (stateValue.phase !== "resetAuth") {
        keyValue = stateValue.sessionKey;
    }
    var parsed = null;
    try {
        parsed = parseAimaFrame(dataValue, keyValue);
    } catch (parseErr) {
        return;
    }
    if (!parsed.ok) return;
    
    // 优化:去掉 sourceUuid 校验,只根据阶段和命令码匹配
    if (stateValue.phase === "auth" && parsed.cmd === 0x83 && parsed.bodyBytes && bytesLen(parsed.bodyBytes) >= 16) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "power" && parsed.cmd === 0x82) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
        return;
    }
    if (stateValue.phase === "command" && parsed.cmd === 0x82) {
        stateValue.resultInfo = parsed;
        stateValue.resultLatch.countDown();
    }
}

function createGattCallback(stateValue) {
    return new JavaAdapter(BluetoothGattCallback, {
        onConnectionStateChange: function(gattObj, statusValue, newStateValue) {
            if (Number(statusValue) === Number(BluetoothGatt.GATT_SUCCESS) && Number(newStateValue) === Number(BluetoothProfile.STATE_CONNECTED)) {
                stateValue.gatt = gattObj;
                if (!gattObj.discoverServices()) setFail(stateValue, "启动服务发现失败");
                return;
            }
            if (Number(newStateValue) === Number(BluetoothProfile.STATE_DISCONNECTED)) {
                setFail(stateValue, "蓝牙连接已断开 status=" + statusValue);
            }
        },
        onServicesDiscovered: function(gattObj, statusValue) {
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "服务发现失败 status=" + statusValue);
                return;
            }
            findGattChars(gattObj, stateValue);
            enableNextNotify(gattObj, stateValue);
        },
        onDescriptorWrite: function(gattObj, descObj, statusValue) {
            if (Number(statusValue) !== Number(BluetoothGatt.GATT_SUCCESS)) {
                setFail(stateValue, "通知描述符写入失败 status=" + statusValue);
                return;
            }
            enableNextNotify(gattObj, stateValue);
        },
        onMtuChanged: function(gattObj, mtuValue, statusValue) {
            if (stateValue.mtuRequested === true) {
                stateValue.readyLatch.countDown();
            }
        },
        onCharacteristicWrite: function(gattObj, charObj, statusValue) {
            stateValue.writeStatus = Number(statusValue);
            try { stateValue.writeLatch.countDown(); } catch (e8) {}
        },
        onCharacteristicChanged: function(gattObj, charObj, valueBytes) {
            var dataValue = valueBytes ? valueBytes : charObj.getValue();
            handleNotify(stateValue, dataValue); // 优化:不再传递 uuid
        }
    });
}

function connectReady(macText) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter || !adapter.isEnabled()) throw "蓝牙功能不可用或未开启";
    var deviceObj = adapter.getRemoteDevice(macText);
    var state = {
        gatt: null,
        targetMac: macText,
        bindChar: null,
        controlChar: null,
        queryChar: null,
        reportChar: null, // 优化:新增 7803
        notifyChars: [],
        notifyIndex: 0,
        readyLatch: new CountDownLatch(1),
        resultLatch: new CountDownLatch(1),
        writeLatch: new CountDownLatch(1),
        writeStatus: -1,
        mtuRequested: false,
        authKey: null,
        sessionKey: null,
        phase: "",
        resultInfo: null,
        error: ""
    };
    state.callbackObj = createGattCallback(state);
    if (Build.VERSION.SDK_INT >= 23) {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj, BluetoothDevice.TRANSPORT_LE);
    } else {
        state.gatt = deviceObj.connectGatt(context, false, state.callbackObj);
    }
    if (!state.gatt) throw "connectGatt 返回空";
    var ready = state.readyLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS);
    if (!ready) throw "连接/服务发现/通知描述符配置超时";
    if (state.error) throw state.error;
    if (!state.bindChar || !state.queryChar || !state.controlChar) throw "未找到必要的爱玛服务特征锁";
    return state;
}

function writeFrameToChar(stateValue, charValue, frameValue) {
    if (!charValue) throw "写入特征为空";
    var props = Number(charValue.getProperties());
    if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    } else if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0) {
        charValue.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    }
    stateValue.writeLatch = new CountDownLatch(1);
    stateValue.writeStatus = -1;
    charValue.setValue(frameValue);
    if (!stateValue.gatt.writeCharacteristic(charValue)) throw "写入特征指令启动失败";
    var done = stateValue.writeLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw "特征值写入响应回调超时";
    if (stateValue.writeStatus !== Number(BluetoothGatt.GATT_SUCCESS)) throw "写入特征失败状态 status=" + stateValue.writeStatus;
}

function waitNotify(stateValue, phaseText) {
    var done = stateValue.resultLatch.await(notifyTimeoutMs, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "响应通知超时";
    if (!stateValue.resultInfo) throw phaseText + "缺少有效解析帧数据";
    return stateValue.resultInfo;
}

function waitNotifyWithTimeout(stateValue, phaseText, timeoutValue) {
    var done = stateValue.resultLatch.await(timeoutValue, TimeUnit.MILLISECONDS);
    if (!done) throw phaseText + "响应通知超时";
    if (!stateValue.resultInfo) throw phaseText + "缺少有效解析状态数据";
    return stateValue.resultInfo;
}

// 优化:Power 查询重试次数从 6 次减少到 3 次
function queryPowerWithRetry(stateValue) {
    var lastErrorText = "";
    for (var attemptValue = 1; attemptValue <= 3; attemptValue++) {
        try {
            stateValue.phase = "power";
            stateValue.resultLatch = new CountDownLatch(1);
            stateValue.resultInfo = null;
            writeFrameToChar(stateValue, stateValue.queryChar, buildAimaFrame(0x02, bytesFromNumbers([]), stateValue.sessionKey));
            return waitNotifyWithTimeout(stateValue, "状态查询", powerNotifyTimeoutMs);
        } catch (queryErr) {
            lastErrorText = String(queryErr);
            if (attemptValue < 3) Thread.sleep(200);
        }
    }
    throw lastErrorText || "状态查询重复尝试失败超时";
}

// 优化:closeGatt 增加延迟,确保断开指令完成发送
function closeGatt(stateValue) {
    if (!stateValue || !stateValue.gatt) return;
    try { Thread.sleep(200); } catch (e) {}
    try { stateValue.gatt.disconnect(); } catch (e9) {}
    try { Thread.sleep(100); } catch (e) {}
    try { stateValue.gatt.close(); } catch (e10) {}
}

function runToggle() {
    var macText = targetBluetoothMac();
    var keyText = targetBluetoothKey();
    var state = null;
    try {
        state = connectReady(macText);
        state.authKey = hexToBytes(keyText);

        state.phase = "resetAuth";
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x05, buildResetAuthBody(keyText), null));
        // 优化:缩短固定延时 1000ms → 300ms
        Thread.sleep(300);

        state.phase = "auth";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.bindChar, buildAimaFrame(0x03, bytesFromNumbers([0]), state.authKey));
        var authInfo = waitNotify(state, "0x03鉴权");
        state.sessionKey = deriveSessionKey(keyText, authInfo.bodyBytes);
        // 优化:缩短固定延时 200ms → 100ms
        Thread.sleep(100);

        var powerInfo = queryPowerWithRetry(state);
        var wasPowerOn = powerInfo.body0 !== 0;
        var targetAction = wasPowerOn ? "close" : "open";
        var targetBody = wasPowerOn ? 0 : 1;

        state.phase = "command";
        state.resultLatch = new CountDownLatch(1);
        state.resultInfo = null;
        writeFrameToChar(state, state.controlChar, buildAimaFrame(0x02, bytesFromNumbers([targetBody]), state.sessionKey));
        var cmdInfo = waitNotify(state, "上下电切换命令");
        return makeJson({
            ok: cmdInfo.cmd === 0x82,
            mode: "toggle",
            mac: macText,
            previousPowerStatus: powerInfo.body0,
            action: targetAction,
            commandBody: targetBody,
            cmd: cmdInfo.cmd,
            resultCode: cmdInfo.body0,
            bodyHex: cmdInfo.bodyHex,
            rawHex: cmdInfo.rawHex
        });
    } finally {
        closeGatt(state);
    }
}

function main() {
    return runToggle();
}

var finalText = "";
try {
    finalText = main();
} catch (err) {
    console.error("[爱玛切换BLE] 致命异常中止: " + err);
    finalText = makeJson({ ok: false, mode: "toggle", error: String(err) });
    throw err;
}
finalText;

刚才点错语言了,你再看看
 楼主| xiaobaice 发表于 2026-7-6 00:44
本帖最后由 xiaobaice 于 2026-7-6 00:46 编辑
cos911 发表于 2026-7-5 22:24
[JavaScript] 纯文本查看 复制代码
// ShortX ExecuteJS:爱玛独立 BLE 上下电切换(优化版)
// 优化点:
 ...[/quote]
resetAuth直接变成500也是可以的,我解包发现APP是发送后直接200后执行下一步。但目前我不太理解把7803加上有什么功能,我优化后的是把7803删掉了,因为这是一个订阅,是车辆ble主动发送,而脚本只是一次运行。我有一个单独的rssi扫描,我觉得单独的扫描可能会好一点。去掉uuid的响应校验我比较存疑(因为现在车辆不在身边了,也无法校验)
[mw_shl_code=javascript,true]// ShortX ExecuteJS:爱玛蓝牙连接触发后循环扫描当前 MAC 的 RSSI
// 触发器上下文:{btDeviceAddress}
// 输出:启动后台监控后立即返回;后台线程按间隔覆盖写入全局变量 aimarssi=JSON

var GLOBAL_RSSI_VAR_NAME = "aimarssi";
var RSSI_THRESHOLD = -50;      // RSSI >= 该值视为靠近
var SCAN_TIMEOUT_MS = 1500;    // 单轮 BLE 扫描超时
var LOOP_INTERVAL_MS = 10000;  // RSSI 扫描间隔
var LOG_ENABLED = false;       // 是否输出 console 日志

importClass(java.lang.Thread);
importClass(java.lang.Runnable);
importClass(java.util.ArrayList);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);
importClass(java.util.concurrent.atomic.AtomicBoolean);
importClass(android.bluetooth.BluetoothAdapter);
importClass(android.bluetooth.BluetoothDevice);
importClass(android.bluetooth.le.ScanCallback);
importClass(android.bluetooth.le.ScanFilter);
importClass(android.bluetooth.le.ScanSettings);
importClass(android.content.BroadcastReceiver);
importClass(android.content.Context);
importClass(android.content.IntentFilter);
importClass(android.os.Build);

function logText(textValue) {
    if (!LOG_ENABLED) return;
    console.log("[爱玛RSSI监控] " + String(textValue));
}

function readTriggerMac() {
    var textValue = String(btDeviceAddress || "");
    textValue = String(textValue).trim().toUpperCase();
    if (!textValue) throw "缺少蓝牙连接触发器上下文 btDeviceAddress";
    return textValue;
}

function writeNearIfChanged(lastNearValue, nearValue, rssiValue, macText) {
    var currentNearValue = nearValue === true;
    if (lastNearValue !== null && lastNearValue === currentNearValue) {
        logText("near 未变化,不写全局变量 near=" + currentNearValue + ", mac=" + macText + ", rssi=" + rssiValue + ", threshold=" + RSSI_THRESHOLD);
        return lastNearValue;
    }
    var valueText = JSON.stringify({
        near: currentNearValue,
        mac: String(macText).toUpperCase(),
        rssi: Number(rssiValue),
        threshold: RSSI_THRESHOLD,
        source: "bleScan",
        timeMs: Packages.java.lang.System.currentTimeMillis()
    });
    shortx.writeGlobalVarWithOp(GLOBAL_RSSI_VAR_NAME, valueText, 3);
    logText("写入全局变量 " + GLOBAL_RSSI_VAR_NAME + "=" + valueText);
    return currentNearValue;
}

function createScanSession(macText, stopFlagValue) {
    var adapter = BluetoothAdapter.getDefaultAdapter();
    if (!adapter) throw "设备不支持蓝牙";
    var targetMacText = String(macText).toUpperCase();
    var filter = new ScanFilter.Builder().setDeviceAddress(targetMacText).build();
    var filters = new ArrayList();
    filters.add(filter);
    var settingsBuilder = new ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
    if (Build.VERSION.SDK_INT >= 23) {
        settingsBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
    }

    var sessionObj = {
        adapter: adapter,
        scanner: null,
        filters: filters,
        settings: settingsBuilder.build(),
        currentLatch: null,
        currentOutput: null,
        scanning: false
    };

    function finishCurrentScan() {
        if (sessionObj.currentLatch) sessionObj.currentLatch.countDown();
    }

    function acceptResult(scanResult) {
        var output = sessionObj.currentOutput;
        if (!output) return;
        var dev = scanResult.getDevice();
        if (!dev || String(dev.getAddress()).toUpperCase() !== targetMacText) return;
        output.ok = true;
        output.found = true;
        output.rssi = Number(scanResult.getRssi());
        finishCurrentScan();
    }

    sessionObj.scanCb = new JavaAdapter(ScanCallback, {
        onScanResult: function(callbackType, scanResult) {
            if (stopFlagValue.get()) {
                finishCurrentScan();
                return;
            }
            acceptResult(scanResult);
        },
        onBatchScanResults: function(resultsValue) {
            var iter = resultsValue.iterator();
            while (iter.hasNext()) {
                var oneResult = iter.next();
                acceptResult(oneResult);
                if (!sessionObj.currentOutput || sessionObj.currentOutput.found || stopFlagValue.get()) break;
            }
        },
        onScanFailed: function(errorCodeValue) {
            var output = sessionObj.currentOutput;
            if (!output) return;
            output.failCode = Number(errorCodeValue);
            output.error = "扫描失败 errorCode=" + output.failCode;
            finishCurrentScan();
        }
    });

    sessionObj.stopCurrentScan = function() {
        try {
            if (sessionObj.scanner && sessionObj.scanning) sessionObj.scanner.stopScan(sessionObj.scanCb);
        } catch (stopErr) {}
        sessionObj.scanning = false;
        finishCurrentScan();
    };

    sessionObj.scanOnce = function() {
        var output = {
            ok: false,
            found: false,
            rssi: -999,
            failCode: -1,
            error: ""
        };
        if (!sessionObj.adapter.isEnabled()) {
            output.error = "蓝牙未开启";
            return output;
        }
        var scanner = sessionObj.adapter.getBluetoothLeScanner();
        if (!scanner) {
            output.error = "设备不支持 BLE 扫描";
            return output;
        }
        sessionObj.scanner = scanner;
        sessionObj.currentOutput = output;
        sessionObj.currentLatch = new CountDownLatch(1);
        sessionObj.scanning = true;
        try {
            scanner.startScan(sessionObj.filters, sessionObj.settings, sessionObj.scanCb);
            sessionObj.currentLatch.await(SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            if (!output.error) output.ok = true;
        } catch (scanErr3) {
            output.error = "扫描过程异常: " + scanErr3;
        } finally {
            try { scanner.stopScan(sessionObj.scanCb); } catch (stopErr2) {}
            sessionObj.scanning = false;
            sessionObj.currentLatch = null;
            sessionObj.currentOutput = null;
        }
        return output;
    };

    return sessionObj;
}

function registerDisconnectReceiver(macText, stopFlagValue, stopLatchValue, scanSessionValue) {
    var receiverObj = new JavaAdapter(BroadcastReceiver, {
        onReceive: function(ctxValue, intentValue) {
            if (String(intentValue.getAction() || "") !== BluetoothDevice.ACTION_ACL_DISCONNECTED) return;
            var devObj = intentValue.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            var addrText = devObj ? String(devObj.getAddress()).toUpperCase() : "";
            if (addrText === String(macText).toUpperCase()) {
                logText("收到目标蓝牙断开广播 mac=" + addrText + ",停止 RSSI 监控");
                stopFlagValue.set(true);
                if (scanSessionValue) scanSessionValue.stopCurrentScan();
                stopLatchValue.countDown();
            }
        }
    });
    var filterObj = new IntentFilter();
    filterObj.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
    try {
        if (Build.VERSION.SDK_INT >= 33) {
            context.registerReceiver(receiverObj, filterObj, Context.RECEIVER_NOT_EXPORTED);
        } else {
            context.registerReceiver(receiverObj, filterObj);
        }
    } catch (regErr1) {
        context.registerReceiver(receiverObj, filterObj);
    }
    return receiverObj;
}

function startMonitor(macText) {
    var targetMacText = String(macText).toUpperCase();
    var stopFlagObj = new AtomicBoolean(false);
    var stopLatchObj = new CountDownLatch(1);
    var worker = new Thread(new JavaAdapter(Runnable, {
        run: function() {
            var receiverObj = null;
            var scanSession = null;
            var lastNearValue = null;
            try {
                scanSession = createScanSession(targetMacText, stopFlagObj);
                receiverObj = registerDisconnectReceiver(targetMacText, stopFlagObj, stopLatchObj, scanSession);
                logText("后台 RSSI 监控启动 mac=" + targetMacText + ", intervalMs=" + LOOP_INTERVAL_MS + ", scanTimeoutMs=" + SCAN_TIMEOUT_MS + ", threshold=" + RSSI_THRESHOLD);
                while (!stopFlagObj.get()) {
                    var scanResult = scanSession.scanOnce();
                    if (stopFlagObj.get()) break;
                    if (!scanResult.ok && scanResult.error) {
                        logText("扫描异常,停止监控且不写 " + GLOBAL_RSSI_VAR_NAME + ": " + scanResult.error);
                        break;
                    }
                    if (scanResult.found) {
                        lastNearValue = writeNearIfChanged(lastNearValue, Number(scanResult.rssi) >= RSSI_THRESHOLD, scanResult.rssi, targetMacText);
                    } else {
                        lastNearValue = writeNearIfChanged(lastNearValue, false, -999, targetMacText);
                    }
                    try { stopLatchObj.await(LOOP_INTERVAL_MS, TimeUnit.MILLISECONDS); } catch (waitErr) {}
                }
            } catch (runErr) {
                logText("监控线程异常退出,不写 " + GLOBAL_RSSI_VAR_NAME + ": " + runErr);
            } finally {
                if (scanSession) scanSession.stopCurrentScan();
                if (receiverObj) {
                    try { context.unregisterReceiver(receiverObj); } catch (unregErr) {}
                }
                logText("后台 RSSI 监控结束 mac=" + targetMacText);
            }
        }
    }));
    worker.setDaemon(false);
    worker.start();
}

var finalText = "";
try {
    var triggerMacText = readTriggerMac();
    startMonitor(triggerMacText);
    finalText = JSON.stringify({
        ok: true,
        mode: "rssiMonitor",
        mac: triggerMacText,
        globalVar: GLOBAL_RSSI_VAR_NAME,
        threshold: RSSI_THRESHOLD,
        scanTimeoutMs: SCAN_TIMEOUT_MS,
        intervalMs: LOOP_INTERVAL_MS
    });
} catch (err) {
    logText("启动失败: " + err);
    finalText = JSON.stringify({ ok: false, mode: "rssiMonitor", error: String(err) });
}
finalText;

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-10 11:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表