上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

如何利用 auto.js 和 MQTT 定位手机

[复制链接]
  • TA的每日心情
    慵懒
    7 小时前
  • 签到天数: 917 天

    [LV.10]以坛为家III

    47

    主题

    200

    回帖

    966

    积分

    荣誉开发者

    积分
    966

    荣誉开发者油中2周年生态建设者

    发表于 2025-4-9 15:16:42 | 显示全部楼层 | 阅读模式

    本帖最后由 tfsn20 于 2025-4-9 20:43 编辑

    前几天我的手机不小心丢失了,为了能在其他设备上追踪手机位置,我写了一个 auto.js 脚本,并结合高德定位 SDK 和 MQTT 服务实现了一个简单的定位方案。以下是实现过程和具体步骤,供大家参考。

    前置条件与手机设置

    手机设置:

    GPS 必须始终开启。
    获取定位时手机需要保持联网状态(移动数据或 Wi-Fi),虽然脚本支持断网重连。

    AutoX.js v7 权限配置:

    开启无障碍服务。
    在设置中关闭省电策略,确保脚本后台运行。
    允许始终精准定位。
    授予联网控制权限。

    技术选型与配置

    高德定位 SDK:

    使用最新版 AMap_Location_V6.4.9_20241226。
    可参考牙叔的文章自行编译:https://blog.csdn.net/snailuncle2/article/details/115270156
    注意:编译时 Java 版本需小于等于 8。
    编译完成后放到手机文件夹一个位置,代码里是/storage/emulated/0/脚本/tools/AMap_Location_V6.4.9_20241226.dex。

    高德 API:

    登录高德开放平台(https://lbs.amap.com/),创建两个应用
    一个绑定 Android 平台,用于手机端定位。
    另一个绑定 Web 服务,用于逆地理编码(将经纬度转换为地址)。
    获取对应的 API Key 并在脚本中配置。

    MQTT 服务:

    使用 HiveMQ 官网提供的免费服务器(https://www.hivemq.com/),每月提供 10GB 免费流量。
    注意:注册时 QQ 邮箱无法使用,建议更换其他邮箱;连接时使用 MQTT URL(避免使用 WSS 端口)。
    image.png
    由于 HiveMQ 官网加载较慢且登录不便,可选注册 https://www.lddgo.net/network/mqtt 进行在线管理。
    image.png

    脚本特色

    脚本运行后,手机会持续监听 MQTT 的指定主题(gaode)。当浏览器发送触发消息时,脚本启动定位并将结果(经纬度及地址)发布到 gaode/location 主题。如果手机断网或关机,MQTT 会发送遗嘱消息到 gaode/will 主题提示状态。脚本支持网络自动重连,确保可靠性。

    使用方法

    打开浏览器登陆lddgo.net(HiveMQ 也可以)。
    连接 HiveMQ 的 WSS 端口,配置连接信息,连接信息可以同步到云端保存,由于主题不支持同步,所以可以订阅 # 主题(全部主题)方便查看消息。
    发送 gaode 主题消息触发定位。 注意发送QoS为1的消息,这样autojs至少能在断网重连后接收到一次
    image.png
    image.png
    image.png

    代码

    // 定位配置
    let apiKey = "8xxxxxx9"; // 替换为你的高德 API Key
    let webApiKey = "0xxxxxxxxxd"; // 替换为你的高德 Web 服务 API Key
    let timeoutLocation = 20 //获取定位超时时间/秒
    
    // MQTT 配置
    let mqttHost = "5xxxxxxxb84.s1.eu.hivemq.cloud";
    let mqttPort = xxxx;
    let clientId = "autojs-client"; // 确保 clientId 唯一
    let mqttUrl = "ssl://" + mqttHost + ":" + mqttPort;
    let username = "admin"; // 替换为你的 HiveMQ 用户名
    let password = "1xxx"; // 替换为你的 HiveMQ 密码
    let receiveTopic = "gaode" //接受来自MQTT服务器gaode主题的消息
    let sendTopic = "gaode/location"; //向MQTT服务器发送主题为gaode/location的消息
    let willTopic = "gaode/will"; // 遗嘱消息主题
    // 其他配置
    let keepAlive = 120 // 保持活跃间隔/秒
    let maxReconnectAttempts = 6; //网络重连次数
    let reconnectInterval = 1000 * 10; // 网络重连次数间隔 10秒间隔
    
    // 加载高德地图 Dex 文件
    let dexPath = "/storage/emulated/0/脚本/tools/AMap_Location_V6.4.9_20241226.dex";
    if (files.exists(dexPath)) {
        runtime.loadDex(dexPath);
        log("Dex 文件加载成功");
    } else {
        log("Dex 文件不存在: " + dexPath);
        exit();
    }
    
    // 引入高德地图相关类
    importClass(com.amap.api.location.AMapLocationClient);
    importClass(com.amap.api.location.AMapLocationClientOption);
    importClass(com.amap.api.location.AMapLocationListener);
    
    // 引入 MQTT 相关类
    importClass(org.eclipse.paho.client.mqttv3.MqttClient);
    importClass(org.eclipse.paho.client.mqttv3.MqttConnectOptions);
    importClass(org.eclipse.paho.client.mqttv3.IMqttMessageListener);
    importClass(org.eclipse.paho.client.mqttv3.MqttMessage);
    
    // 引入 Java 时间格式化类
    importClass(java.text.SimpleDateFormat);
    
    // 设置高德地图隐私合规和 API Key
    try {
        AMapLocationClient.updatePrivacyShow(context, true, true);
        AMapLocationClient.updatePrivacyAgree(context, true);
        log("隐私合规设置完成");
    } catch (e) {
        log("隐私合规设置失败: " + e);
        exit();
    }
    
    try {
        AMapLocationClient.setApiKey(apiKey);
        log("API Key 设置成功");
    } catch (e) {
        log("API Key 设置失败: " + e);
        exit();
    }
    
    // 创建 MQTT 客户端
    let mqttClient;
    try {
        mqttClient = new MqttClient(mqttUrl, clientId, null);
        let options = new MqttConnectOptions();
        options.setUserName(username);
        options.setPassword(new java.lang.String(password).toCharArray());
        options.setCleanSession(true); // 确保每次连接是新的会话
        options.setKeepAliveInterval(keepAlive); // 设置 Keep-Alive 为 keepAlive 秒
    
        // 创建当地时间格式化器
        let sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); // ISO 8601 格式,带时区偏移
        sdf.setTimeZone(java.util.TimeZone.getDefault()); // 使用设备默认时区
        let localTime = sdf.format(new java.util.Date()); // 获取当地时间字符串
        // 设置遗嘱消息
        let willMessageData = {
            clientId: clientId,
            status: "offline",
            timestamp: localTime
        };
        let willMessageString = JSON.stringify(willMessageData);
        let willPayload = new java.lang.String(willMessageString).getBytes("UTF-8");
        options.setWill(willTopic, willPayload, 1, true);
    
        // 设置 MQTT 回调
        mqttClient.setCallback({
            connectionLost: function (cause) {
                log("MQTT 连接断开: " + cause);
                attemptReconnect(options);
            },
            messageArrived: function (topic, message) {
                // 此处可以留空,因为我们已经在 subscribe 中处理了消息
            },
            deliveryComplete: function (token) {
                log("消息发送完成: " + token);
            }
        });
    
        mqttClient.connect(options);
        log("MQTT 连接成功");
    
        // 订阅主题并设置消息监听
        mqttClient.subscribe(receiveTopic, new IMqttMessageListener({
            messageArrived: function (topic, message) {
                log("收到消息: " + message.toString());
                if (isLocating) {
                    log("定位进行中,忽略本次请求");
                    return;
                }
                // 触发定位
                startLocation();
            }
        }));
    
    } catch (e) {
        log("MQTT 连接失败: " + e);
        exit();
    }
    
    // 自动重连函数
    function attemptReconnect(options) {
        let reconnectAttempts = 0;
        function tryReconnect() {
            // 等待一段时间后再次尝试
            java.lang.Thread.sleep(reconnectInterval);
            if (reconnectAttempts < maxReconnectAttempts) {
                reconnectAttempts++;
                log(`尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})`);
    
                try {
                    mqttClient.connect(options);
                    log("重连成功");
                    // 重连成功后重新订阅
                    mqttClient.subscribe(receiveTopic, new IMqttMessageListener({
                        messageArrived: function (topic, message) {
                            log("收到消息: " + message.toString());
                            if (isLocating) {
                                log("定位进行中,忽略本次请求");
                                return;
                            }
                            startLocation();
                        }
                    }));
                } catch (e) {
                    log("重连失败: " + e);
                    tryReconnect();
                }
            } else {
                log("达到最大重连次数,放弃重连");
                exit();
            }
        }
    
        // 开始第一次重连尝试
        tryReconnect();
    }
    
    // 定义定位客户端和状态变量
    let mLocationClient;
    let locationReceived = false;
    let timeoutId;
    let locationData = null;
    let isLocating = false; // 添加定位状态锁
    
    // 初始化定位客户端
    function initLocationClient() {
        try {
            mLocationClient = new AMapLocationClient(context);
            mLocationClient.setLocationListener(getAMapLocationListener());
    
            let mLocationOption = new AMapLocationClientOption();
            mLocationOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.SignIn);
            mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
            mLocationOption.setOnceLocation(true);
            mLocationOption.setNeedAddress(true);
            mLocationClient.setLocationOption(mLocationOption);
            log("定位客户端初始化成功");
        } catch (e) {
            log("定位客户端初始化失败: " + e);
        }
    }
    
    // 定位回调监听器
    function getAMapLocationListener() {
        return new AMapLocationListener({
            onLocationChanged: function (location) {
                log("回调定位触发");
                if (location != null) {
                    if (location.getErrorCode() == 0) {
                        log("定位成功");
                        log("纬度: " + location.getLatitude());
                        log("经度: " + location.getLongitude());
                        log("精度: " + location.getAccuracy());
                        log("速度: " + location.getSpeed());
                        log("提供者: " + location.getProvider());
                        locationData = {
                            纬度: location.getLatitude(),
                            经度: location.getLongitude(),
                            准确度: location.getAccuracy(),
                            速度: location.getSpeed(),
                            提供商: location.getProvider()
                        };
                        // 发送定位信息到 MQTT
                        sendLocationToMQTT(JSON.stringify(locationData), sendTopic);
    
                    } else {
                        log("定位失败: " + location.getErrorCode() + " - " + location.getErrorInfo());
                        sendLocationToMQTT("定位失败: " + location.getErrorCode() + " - " + location.getErrorInfo(), sendTopic);
                    }
                } else {
                    log("Location is null");
                }
                locationReceived = true; // 标记定位已完成
                isLocating = false; // 定位完成后释放锁
                if (timeoutId) {
                    clearTimeout(timeoutId); // 清除超时计时器
                }
            }
        });
    }
    
    //  发送定位信息到 MQTT
    function sendLocationToMQTT(messages, topic) {
        try {
            if (mqttClient && mqttClient.isConnected()) {
                let message = new MqttMessage(new java.lang.String(messages).getBytes("UTF-8"));
                message.setQos(1);
                mqttClient.publish(topic, message);
                log("信息已发送到 MQTT");
            } else {
                log("MQTT 客户端未连接,无法发送定位信息");
            }
        } catch (e) {
            log("发送信息失败: " + e);
        }
    }
    
    // 启动定位函数
    function startLocation() {
        if (!mLocationClient) {
            initLocationClient();
        }
        if (isLocating) {
            log("已在定位中,请稍候");
            return;
        }
        locationReceived = false;
        try {
            mLocationClient.stopLocation();
            mLocationClient.startLocation();
            isLocating = true;
            log("开始定位");
    
            // 设置超时
            timeoutId = setTimeout(() => {
                if (!locationReceived) {
                    log(`定位超时:${timeoutLocation}秒内未获取到结果`);
                    mLocationClient.stopLocation();
                    isLocating = false; // 超时后释放锁
                }
            }, 1000 * timeoutLocation);
    
            // 等待定位结果
            function waitForLocation() {
                if (locationReceived) {
                    // 在此处添加对定位数据的进一步处理
                    // 给定的经纬度
                    let longitude = locationData.经度;
                    let latitude = locationData.纬度;
    
                    // 构造逆地理编码 API 请求 URL
                    let url = "https://restapi.amap.com/v3/geocode/regeo?key=" + webApiKey +
                        "&location=" + longitude + "," + latitude +
                        "&extensions=all"; // extensions=all 返回详细地址信息
    
                    // 发送 HTTP 请求并获取地址信息
                    try {
                        let response = http.get(url);
                        if (response.statusCode == 200) {
                            let result = response.body.json();
                            if (result.status == "1") {
                                // 提取逆地理编码结果
                                let regeocode = result.regeocode;
                                let address = regeocode.formatted_address; // 完整地址
                                let addressComponent = regeocode.addressComponent;
                                let streetInfo = `${addressComponent.streetNumber.street}${addressComponent.streetNumber.number}位于定位的${addressComponent.streetNumber.direction}方向${addressComponent.streetNumber.distance}米远`;
                                // 输出详细信息
                                log("逆地理编码成功:");
                                log("完整地址: " + address);
                                log(`街道信息:${streetInfo}`);
                                sendLocationToMQTT(`完整地址: ${address}\n街道信息: ${streetInfo}`, sendTopic)
                            } else {
                                log("逆地理编码失败: " + result.info);
                            }
                        } else {
                            log("请求失败,状态码: " + response.statusCode);
                        }
                    } catch (e) {
                        log("请求出错: " + e);
                    }
                    log("定位完成");
                } else {
                    setTimeout(waitForLocation, 1000);
                }
            }
            setTimeout(waitForLocation, 1000);
        } catch (e) {
            log("定位启动失败: " + e);
            isLocating = false; // 定位启动失败释放锁
        }
    }
    
    // 初始化定位客户端
    initLocationClient();
    
    // 脚本退出时清理资源
    events.on("exit", () => {
        log("脚本退出");
        if (mLocationClient) {
            mLocationClient.stopLocation();
            mLocationClient.onDestroy();
        }
        if (mqttClient && mqttClient.isConnected()) {
            mqttClient.disconnect();
        }
        log("清理完成")
    });
    
    // 保持脚本运行
    setInterval(() => { }, 1000);

    发表回复

    本版积分规则

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