下面两个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;