跳转至

LuatOS 课程-011 讲:GNSS应用开发

Hello,大家好,我是黄何。欢迎大家来到合宙 LuatOS 直播课堂,今天我要给大家分享一个非常实用的物联网项目——基于LuatOS的智能定位系统

想象一下,你需要开发一个学生卡定位器,要求是:精准定位、超长续航、轨迹平滑、多平台支持。听起来是不是很有挑战性?今天这个项目就完美解决了所有这些问题!

项目亮点

  • 🛰️ 三合一定位:GNSS + 基站 + WiFi,永不丢失
  • 🔋 续航:智能功耗管理,运动才定位
  • 🛣️ 轨迹优化:减少80%GPS静态漂移以及运动漂移,路线更符合真实路径
  • 🌐 多平台兼容:一次开发,同时上报多个服务器
  • 🎯 工业标准:完整JT808协议实现
  • 🚩合宙特色:exgnss扩展库和exvib扩展库

适合场景

  • 👨🎓 学生安全卡、老人定位器
  • 🐕 宠物追踪、牲畜管理
  • 🚗 车辆监控、物流追踪
  • 📦 资产定位、贵重物品跟踪

前言

当前项目为演示项目,使用到的web平台为合宙自建的内部服务器,暂时不对外开放,此项目仅仅是为了给大家演示一下exgnss扩展库和exvib库如何融入到定位器相关项目内,也会重点讲解gnss应用场景有关的代码和业务逻辑,至于后期如何开放该平台等事宜,等待内部讨论后给出。

项目功能:

  1. 经纬度数据、电量、卫星信号质量、速度、电池状态(充电中/未充电)等数据上报到合宙定位测试服务器
  2. 根据服务器下发的指令 进入/退出快速定位模式
  3. 功耗优化,区分运动和静止状态

项目需求:

  1. 由Air8000A内部加速度传感器判断是否开启/关闭GNSS
  2. 按照JT808协议和服务器进行通讯
  3. 可以上传多个服务器(为后期做拓展)
  4. 尽可能的减少静态/动态漂移带来的轨迹失真
  5. 与服务器直接保持长连接状态,并且尽可能的做到功耗最低

使用到的最为重要的两个扩展库介绍:

exgnss:

exgnss库实际上是把底层提供的libgnss接口封装了一层,让配置过程更快速一点,更方便用户快速上手,exgnss库最重要的三个接口为:

exgnss.setup(gnssotps) --配置GNGSS输出相关
exgnss.open(mode,para)--硬件层面打开GNSS
exgnss.close(mode,para)--硬件层面关闭GNSS

至于这三个接口的具体使用方法,以及传参,这里有接口/参数说明以及demo示例,后面项目中也用到了,这里带大家简单过一次。就不做赘述了

常量定义:

exgnss.DEFAULT

常量含义:exgnss应用模式1,常开模式;
数据类型:number
示例代码:--- exgnss应用模式1.
        -- 打开gnss后,gnss定位成功时,如果有回调函数,会调用回调函数
        -- 使用此应用模式调用exgnss.open打开的“gnss应用”,必须主动调用exgnss.close
        -- 或者exgnss.close_all才能关闭此“gnss应用”,主动关闭时,即使有回调函数,也不会调用回调函数
        -- 通俗点说就是一直打开,除非自己手动关闭掉
        local function mode_cb(tag)
            log.info("TAGmode_cb+++++++++",tag)
            log.info("nmea", "rmc", json.encode(exgnss.rmc(2)))
        end
        exgnss.open(exgnss.DEFAULT,{tag="MODE",cb=mode_cb})

本简易定位器demo中用到的就是这个常量

还有exgnss.TIMERORSUC(到规定时间或者GNSS定位成功后[开拓地带约为35S]关闭GNSS)和

exgnss.TIMER[到规定时间就关,不管GNSS定位成功与否]

exgnss=require("exgnss")  --因为是扩展库不是核心库 所以需要require(类似C语言中的#incloud)

local function mode_cb(tag)
    log.info("定位应用的回调函数",tag)
    log.info("让我看看当前经纬度", json.encode(exgnss.rmc(2)))
end

local function gnss_fnc()
    local gnssotps={
        gnssmode=1, --1为卫星全定位,2为单北斗
        agps_enable=true,    --是否使用AGPS,开启AGPS后定位速度更快,会访问服务器下载星历,对Air8000来说,星历时效性:北斗1小时,GPS2小时,默认下载星历的时间为1小时,即一小时内只会下载一次
        debug=true,    --是否输出调试信息(GNSS芯片输出的nmea数据)

     --其他参数用户可以看接口说明,本demo中用不到其他参数
    }
     --设置gnss参数
    exgnss.setup(gnssotps)
    --开启gnss应用,到规定的60S就关闭,不管定位成功与否
    exgnss.open(exgnss.TIMER,{tag="用户自定义应用名",val=60,cb=mode_cb})  --使用TIMER模式,开启60s后关闭

    sys.wait(40000)
    log.info("关闭一个gnss应用")
    --关闭一个gnss应用
    exgnss.close(exgnss.TIMER,{tag="用户自定义应用名"})--关闭tag为“用户自定义应用名”应用
    --查询gnss应用状态
    log.info("gnss应用状态",exgnss.is_active(exgnss.TIMER,{tag="用户自定义应用名"}))
    sys.wait(10000)
    --关闭所有gnss应用
    exgnss.close_all()
    --查询最后一次定位结果
    local loc= exgnss.last_loc()
    if loc then
        log.info("lastloc", loc.lat,loc.lng)
    end
end

sys.taskInit(gnss_fnc)


--GNSS定位状态的消息处理函数:
local function gnss_state(event, ticks)
    -- event取值有
    -- "FIXED":string类型 定位成功
    -- "LOSE": string类型 定位丢失
    -- "CLOSE": string类型 GNSS关闭,仅配合使用exgnss.lua有效
    -- ticks number类型 是事件发生的时间,一般可以忽略
    log.info("exgnss", "state", event)
    if event=="FIXED" then
        --获取rmc数据
        --json.encode默认输出"7f"格式保留7位小数,可以根据自己需要的格式调整小数位,本示例保留5位小数
        log.info("nmea", "rmc0", json.encode(exgnss.rmc(0),"5f"))
    end
end
sys.subscribe("GNSS_STATE",gnss_state)

exvib:

exvib库实际上是操作了Air780EGP/Air780EGG/Air780EGH/Air8000A/Air8000AB/Air8000N/Air8000U/Air8000D/Air8000DB模组内的一颗G-Sensor加速度传感器,模组内部硬件具体怎么接的,咱们这个项目中无需关心,exvib库的三种模式主要用于以下场景:

  1. 微小震动检测:用于检测轻微震动的场景,例如用手敲击桌面,加速度量程2g;
  2. 运动检测:用于电动车或汽车行驶时的检测和人行走和跑步时的检测,加速度量程4g
  3. 跌倒检测:用于人或物体瞬间跌倒时的检测,加速度量程8g

咱们这个项目 使用的就是运动检测,exvib库就三个接口,分别为:

exvib.read_xyz()--读三轴(G-Sensor)传过来的XYZ轴数据
exvib.open(mode)--打开三轴(G-Sensor)
exvib.close()--关闭三轴(G-Sensor)

exvib库完整的说明可以看这里,后面项目具体代码中也有,这三个接口不难理解,唯一注意的一点是exvib.open这个接口

exvib.open(mode)

参数含义:加速度传感器的应用模式;
数据类型:number
取值范围:
1 - 微小震动检测,用于检测轻微震动的场景,例如用手敲击桌面,加速度量程2g
2 - 运动检测,用于电动车或汽车行驶时的检测和人行走和跑步时的检测,加速度量程4g
3 - 跌倒检测,用于人或物体瞬间跌倒时的检测,加速度量程8g
是否必选:是;

第一部分:项目整体架构

1.1 模块化架构设计

架构特点

1. 分层设计

  • 定位核心层:commom(管理了exgnss和trackCompensate)

  • 传感器层:vibration(三轴)

  • 电源管理层:charge、manage

  • 通讯协议层:api、jt808

  • 服务器层:srvs、auxServer

  • 具体后面讲代码的时候会一个个lua文件打开,仔细说明,这里就不做过多赘述了

2. 事件驱动

-- 发布订阅模式,完全解耦
sys.publish("SYS_STATUS_RUN")      -- 运动事件
sys.subscribe("GNSS_STATE", fn)    -- 定位状态变化
sys.waitUntil("IP_READY", 10000)   -- 等待网络就绪

3. 配置驱动

-- cfg.lua 统一管理所有配置
_G.GNSS_LOWPOWER_ENABLE = true -- 低功耗开关
_G.GNSS_LOWPOWER_INTERVAL = 60 -- 定位间隔(单位:S)
-- 轨迹补偿参数
trackCompensate.setConfig({
    scene = "person", -- 人员模式
    autoAdaptive = true, -- 自动适应
    distanceThreshold = 50 -- 漂移过滤阈值
})

4. gnss应用场景管理

-- 避免资源冲突,智能管理GNSS
exgnss.open(exgnss.DEFAULT, {tag = "common_app"})
exgnss.open(exgnss.TIMERORSUC, {tag = "lowpower"})
-- 两个tag都关闭时,GNSS才真正关闭

1.2 核心模块介绍

模块 功能 关键接口
common.lua 定位状态机 monitorRecord(), setfastUpload()
vibration.lua 运动检测 isRun(), getStaticDuration()
manage.lua 功耗管理 wake(), sleep(), powerOff()
charge.lua 电池管理 getBatteryPercent(), isCharge()
trackCompensate.lua 轨迹补偿 compensate(), setConfig()
srvs.lua 服务器管理 dataSend(), add(), isConnected()
jt808.lua 协议处理 positionPackage(), rxAnalyze()

第二部分:智能定位状态机

一、什么是状态机

​ 状态机的全称叫做有限状态机,是计算机的运行的基本的元素。

​ 状态机由三个要素组成:

​ 1,有限个状态,

​ 2,触发状态迁移的事件,

​ 3,不同状态之间迁移的逻辑。

​ 我们在设计软件之前,先把系统的状态机设计清楚, 再开发软件,可以使得软件更容易评审,也更容易编写测试用例,多人的协作也会更加容易。

​ 状态机设计清楚之后, 可以让产品经理,研发工程师,项目经理,测试,形成了统一的话语体系,基于状态机,以及状态机下的各个参数的沟通, 变得高效,简单。

二、设备的定位状态机

用我们现在正在讲解的项目为例, 状态机如下:

1、 4个状态描述

(1)捕获GPS

​ 没有GPS定位成功,并且设备是运动的,设备都会处于捕获状态。

​ 这是设备最耗电的一个状态。

(2)追踪GPS

​ 捕获成功后,进入追踪状态,追踪状态比捕获省电一些。

(3)静止GPS

​ 追踪状态下,检测到设备静止,就进行静止GPS,这个时候,关闭GPS,上报最近一次的GPS位置。

(4)LBS定位状态

​ 捕获失败,并且设备静止,进入 LBS 状态,上报 LBS 和 wifi 的信息,服务器端进行基站和WIFI的组合定位。

2、触发状态迁移的事件

(1)捕获成功;

(2)捕获失败;

(3)追踪过程中丢GPS;

(4)静止;

(5)运动;

(6)按键

(7)收到服务器的指令

3、 状态迁移的逻辑

三、状态机设计理念

核心问题:如何平衡定位精度与功耗?

合宙的方案:智能切换 → 精度够用且续航长(一周)

2.2 各状态详细逻辑

CAPTURE模式 - 定位捕获

-- 代码位置:common.lua 第200-250行
function captureMode()
    logF("========== 进入CAPTURE模式 ==========")

    -- 1. 打开GNSS,启用AGPS
    exgnss.open(exgnss.DEFAULT, {
        tag = "common_app"
    })

    -- 2. WiFi扫描辅助定位
    wlan.scan()

    -- 3. 最多尝试10次(首次)或4次(后续)
    for i = 1, maxAttempts do
        srvs.dataSend() -- 触发定位数据采集-- 等待定位结果
        local result = sys.waitUntil("GNSS_STATE", 3000)
        if result == "FIXED" or exgnss.is_fix() then
            logF("定位成功!耗时", i * 3, "秒")
            return "TRACKING" -- 进入追踪模式
        end
        logF("定位尝试", i, "/", maxAttempts, "失败")
    end
    -- 4. 定位失败处理
    if not manage.isRun() then -- 静止状态
        logF("静止状态,降级到基站定位")
        return "STATIC_LBS"
    else
        logF("运动状态,继续尝试")
        return "CAPTURE" -- 继续尝试
    end
end

TRACKING模式 - 运动追踪

-- 关键特性:
-- 1. 1Hz定位频率,实时性高
-- 2. 自动轨迹补偿,减少漂移
-- 3. 双模式上传:正常5分钟/快速3秒
-- 4. 智能休眠检测:20秒无震动
-- 上传频率控制
if fastUpload then -- 快速模式:3秒/次(紧急情况)
    srvs.dataSend() -- 立即上传
else -- 正常模式:缓存+批量上传
    if waitUploadTimes >= 30 then -- 约5分钟
        uploadCache() -- 批量上传缓存数据
    end
end

STATIC模式 - 静止优化

-- STATIC_GNSS:使用最后有效位置
-- STATIC_LBS:使用基站定位(GNSS失败时)
-- 共同特点:
-- 1. 关闭GNSS,功耗降至1mA
-- 2. 定期心跳保持连接
-- 3. 运动立即唤醒,无感知切换

2.3 状态切换条件表

切换方向 触发条件 响应时间 用户感知
静止→运动 5秒内≥2次震动 <5秒 立即定位
运动→静止 20秒内0次震动 20秒 平滑过渡
定位丢失 GNSS信号丢失 <3秒 自动重连
快速上传 服务器指令 立即 密集追踪

第三部分:轨迹补偿算法详解

3.1 GPS为什么需要补偿?

现场演示:请大家看这张图,红色框为原始GPS轨迹:

问题根源(上一节也讲过了)

  1. 多路径效应:信号反射,误差10-50米
  2. 卫星几何:仰角低时误差大
  3. 大气延迟:电离层影响
  4. 时钟误差:接收机时钟不准

3.2 四层补偿算法

3.3 核心算法代码解析

算法1:距离阈值过滤

-- trackCompensate.lua 第80-120行
local function distanceFilter(lat, lng, params)
    if #historyBuffer == 0 then
        return lat, lng -- 无历史数据,直接使用
    end

    local last = historyBuffer[#historyBuffer]
    local distance = calculateDistance(last.lat, last.lng, lat, lng)
    -- 自适应阈值:人员50米,车辆200米
    local threshold = params.distanceThreshold
    if distance > threshold then
        logF("漂移点过滤:距离", distance, "米 > 阈值", threshold, "米")
        return last.lat, last.lng -- 返回历史位置
    end
    return lat, lng -- 正常点
end

算法2:航向平滑(解决360°跳变)

-- 关键问题:359° → 1° 实际只变了2°,但差值算成358°
-- 解决方案:使用三角函数处理角度循环
local function smoothCourse(currentCourse, historyCourses)
    local sumSin = 0
    local sumCos = 0
    -- 加权平均,最近的数据权重高
    for i, course in ipairs(historyCourses) do
        local weight = 1.0 / (i + 1) -- 权重递减
        sumSin = sumSin + math.sin(math.rad(course)) * weight
        sumCos = sumCos + math.cos(math.rad(course)) * weight
    end
    -- 计算平均航向
    local avgCourse = math.deg(math.atan2(sumSin, sumCos))
    avgCourse = (avgCourse + 360) % 360 -- 确保0-360范围
    -- 检查是否跳变
    local diff = math.abs(currentCourse - avgCourse)
    if diff > 180 then
        diff = 360 - diff
        -- 处理循环
    end
    if diff > params.courseChangeThreshold then
        logF("航向跳变补偿:", currentCourse, "→", avgCourse)
        return avgCourse
    end
    return currentCourse
end

算法3:速度突变过滤

-- 原理:GNSS速度有时会从0突然跳到30km/h
-- 验证:用实际位移计算的速度更可靠
local function smoothSpeed(gnssSpeed, distance, timeDiff)
    -- 计算基于位移的实际速度
    local displacementSpeed = 0
    if timeDiff > 0 then
        displacementSpeed = (distance / timeDiff) * 3.6 -- m/s → km/h
    end
    -- 差异过大时,使用位移速度
    local speedDiff = math.abs(gnssSpeed - displacementSpeed)
    if speedDiff > params.speedChangeThreshold then
        logF("速度突变:GNSS=", gnssSpeed, "位移=", displacementSpeed, "差值=", speedDiff)
        return displacementSpeed
    end
    return gnssSpeed
end

算法4:拐点补偿

-- 解决急转弯被拉直的问题
local function cornerCompensation(lat, lng, bearing, speed)
    -- 1. 检测转弯角度
    local angleDiff = calculateAngleDifference()

    -- 2. 急转弯判定(人员120°,车辆90°)
    if angleDiff > params.cornerAngleThreshold then
        -- 3. 沿当前航向预测位置
        local distance = (speed / 3.6) * 3
        -- 假设3秒间隔
        local estimatedLat = lat + (distance / 6371000) * math.cos(math.rad(bearing))
        local estimatedLng = lng + (distance / 6371000) * math.sin(math.rad(bearing)) / math.cos(math.rad(lat))

        -- 4. 检查预测位置是否合理
        local predictionError = calculateDistance(lat, lng, estimatedLat, estimatedLng)
        if predictionError < params.distanceThreshold then
            logF("拐点补偿:角度", angleDiff, "°")
            return estimatedLat, estimatedLng
        end
    end
    return lat, lng
end

3.4 双场景自适应系统

-- 人员模式 vs 车辆模式
local config = {
    scene = "person", -- 默认人员模式
    autoAdaptive = true, -- 自动切换
    personParams = {
        distanceThreshold = 50, -- 50米漂移过滤
        speedChangeThreshold = 10, -- 10km/h速度突变
        courseChangeThreshold = 60, -- 60°航向跳变
        cornerAngleThreshold = 120 -- 120°急转弯
    },
    vehicleParams = {
        distanceThreshold = 200, -- 200米漂移过滤
        speedChangeThreshold = 50, -- 50km/h速度突变
        courseChangeThreshold = 30, -- 30°航向跳变
        cornerAngleThreshold = 90 -- 90°急转弯
    },

    adaptiveSpeedThreshold = 15 -- 15km/h切换阈值
}

-- 自动场景判断
local function getCurrentParams()
    if config.autoAdaptive and #historyBuffer >= 3 then
        -- 计算最近3个点的平均速度
        local avgSpeed = 0
        for i = #historyBuffer - 2, #historyBuffer do
            avgSpeed = avgSpeed + historyBuffer[i].speed
        end
        avgSpeed = avgSpeed / 3
        if avgSpeed >= config.adaptiveSpeedThreshold then
            logF("自动切换:车辆模式(速度", avgSpeed, "km/h)")
            return config.vehicleParams
        else
            logF("自动切换:人员模式(速度", avgSpeed, "km/h)")
            return config.personParams
        end
    end
    -- 固定模式
    return config.scene == "vehicle" and config.vehicleParams or config.personParams
end

3.5 优化效果对比

测试数据

原始轨迹数据:
  点1: 31.123456, 121.654321, 速度5, 航向180
  点2: 31.123500, 121.654300, 速度25, 航向185 ← 速度突变!
  点3: 31.123800, 121.654000, 速度6, 航向359 ← 航向跳变!

优化后数据:
  点1: 31.123456, 121.654321, 速度5, 航向180
  点2: 31.123480, 121.654310, 速度5, 航向182 ← 速度修正
  点3: 31.123520, 121.654280, 速度5, 航向181 ← 航向修正

量化提升

指标 优化前 优化后 提升幅度
静态漂移 10-50米 2-10米 80%减少
航向跳变 20次/小时 1次/小时 95%减少
速度突变 15次/小时 2次/小时 87%减少
轨迹平滑度 锯齿状 平滑曲线 显著提升

第四部分:功耗优化策略

4.1 三层功耗管理架构

-- 第一层:硬件级控制
-- 引用计数管理,避免资源冲突
local function hardwarePowerControl() -- 不同模块使用独立tag
    exgnss.open(exgnss.DEFAULT, {
        tag = "common_app"
    }) -- 主定位
    exgnss.open(exgnss.TIMERORSUC, {
        tag = "lowpower"
    }) -- 低功耗模式
    -- 只有所有tag都关闭,硬件才真正断电
    -- 避免:A模块关闭影响B模块使用
end
-- 第二层:应用级控制
-- 状态机自动切换
local function applicationPowerControl()
    -- TRACKING模式:GNSS常开
    -- STATIC模式:GNSS关闭
    -- 根据运动状态自动调整
    if manage.isRun() then
        logF("运动状态,保持GNSS开启")
    else
        logF("静止状态,关闭GNSS节省功耗")
        exgnss.close(exgnss.DEFAULT, {
            tag = "common_app"
        })
    end
end
-- 第三层:系统级控制
-- 统一休眠管理
local function systemPowerControl() -- manage模块引用计数
    manage.wake("READ_GNSS_DATA") -- 读取数据时唤醒
    manage.sleep("READ_GNSS_DATA") -- 读取完成后休眠

    manage.wake("charge") -- 充电时唤醒
    manage.sleep("charge") -- 充电结束休眠
    -- 检查所有tag,全部休眠时进入低功耗模式
    local allSleeping = true
    for tag, state in pairs(manage.tags) do
        if state > 0 then
            allSleeping = false
            break
        end
    end
    if allSleeping then
        pm.power(pm.WORK_MODE, 1) -- 进入长连接低功耗模式休眠
        logF("所有模块休眠,进入LIGHT模式")
    end
end

4.2 震动检测优化算法

-- vibration.lua 核心逻辑
local function vibrationOptimization()
-- 静止→运动:快速响应(5秒内2次震动)
-- 避免误判:防止偶尔震动误触发
-- 运动→静止:延迟确认(20秒内0次震动)
-- 避免误判:防止行走时停顿误判为静止
-- 有效震动检测:10秒内5次震动
-- 防止误触:过滤无效震动(如放口袋里的晃动)
-- 冷却机制:有效震动后30分钟内不再重复触发
-- 避免频繁:如车辆持续震动场景
end

优化空间:

  1. 进一步降低GNSS功耗:按需定位
  2. 优化网络连接:心跳间隔调整
  3. 深度休眠:夜间完全关闭

第五部分:数据通信架构及代码完整实现

5.1 JT808协议完整实现

5.2 数据包结构详解

---```lua
-- JT808 0x0200 位置信息报文结构
local function buildPositionPacket()
    -- 1. 消息头(12字节)
    local header = api.NumToBigBin(0x0200, 2) -- 消息ID
    .. api.NumToBigBin(bodyLen, 2) -- 消息体属性
    .. simID -- 终端手机号(6字节BCD)
    .. api.NumToBigBin(msgSn, 2) -- 消息流水号

    -- 2. 消息体基础部分(28字节)
    local body = api.NumToBigBin(0, 4) -- 报警标志
    .. api.NumToBigBin(status, 4) -- 状态位
    .. api.NumToBigBin(lat, 4) -- 纬度(度*1000000)
    .. api.NumToBigBin(lng, 4) -- 经度(度*1000000)
    .. api.NumToBigBin(altitude, 2) -- 海拔(米)
    .. api.NumToBigBin(speed, 2) -- 速度(0.1km/h)
    .. api.NumToBigBin(course, 2) -- 方向(度)
    .. timeBCD -- 时间(6字节BCD)

    -- 3. 扩展信息(附加项)
    -- 里程信息
    body = body .. api.NumToBigBin(0x01, 1) -- 附加项ID
    .. api.NumToBigBin(4, 1) -- 长度4字节
    .. api.NumToBigBin(mileage, 4) -- 里程值

    -- 电量信息
    body = body .. api.NumToBigBin(0x04, 1) -- 附加项ID
    .. api.NumToBigBin(2, 1) -- 长度2字节
    .. api.NumToBigBin(chargeState, 1) -- 充电状态
    .. api.NumToBigBin(batteryPercent, 1) -- 电量百分比

    -- WiFi信息(如有)
    if wifiList and #wifiList > 0 then
        body = body .. api.NumToBigBin(0x54, 1) -- WiFi附加项
        .. buildWifiInfo(wifiList)
    end

    -- 4. 组装完整报文
    local packet = header .. body
    packet = packet .. string.char(api.XorCheck(packet)) -- 校验码
    packet = msgEncode(packet) -- 转义处理
    packet = "\x7E" .. packet .. "\x7E" -- 添加起始结束符

    return packet
end

5.3 多服务器管理架构

local srvs = {
    servers = {}, -- 服务器实例列表-- 添加服务器
    add = function(self, server)
        table.insert(self.servers, server)
        logF("添加服务器,当前数量:", #self.servers)
    end,

    -- 发送数据到所有服务器
    dataSend = function(self, data)
        logF("========== 开始发送数据 ==========")

        -- 数据预处理和日志
        if data then
            self:_logDataInfo(data)
        else
            logF("数据为空,自动调用common.monitorRecord()")
            data = common.monitorRecord()
        end -- 分发到所有服务器
        local successCount = 0
        for i, srv in ipairs(self.servers) do
            if srv.dataSend then
                local ok, err = pcall(srv.dataSend, data)
                if ok then
                    successCount = successCount + 1
                    logF("服务器", i, "发送成功")
                else
                    logF("服务器", i, "发送失败:", err)
                end
            end
        end

        logF("发送完成,成功:", successCount, "/", #self.servers)
        return successCount
    end,

    -- 检查连接状态(任意一个连接成功即返回true)
    isConnected = function(self)
        for _, srv in ipairs(self.servers) do
            if srv.isConnected and srv.isConnected() then
                return true
            end
        end
        return false
    end
}

-- 使用示例
local auxServer = require "auxServer"
local productionServer = require "productionServer"

srvs:add(auxServer) -- 测试服务器
srvs:add(productionServer) -- 生产服务器
-- 一次调用,多处发送
srvs:dataSend(positionData)

5.4 数据上传频率控制

-- 双模式上传策略
local uploadConfig = {
    normal = {
        interval = 5 * 60, -- 5分钟
        cacheSize = 30, -- 缓存30次后上传
        description = "正常模式:平衡功耗与实时性"
    },

    fast = {
        interval = 3, -- 3秒
        immediate = true, -- 立即上传
        description = "快速模式:紧急追踪场景"
    }
}

-- 模式切换接口
function common.setfastUpload(duration)
    if duration > 0 then -- 进入快速上传模式
        logF("进入快速上传模式,持续", duration, "分钟")
        fastUpload = true -- 立即触发一次上传
        srvs.dataSend()

        -- 定时自动退出
        sys.timerStart(function()
            fastUpload = false
            logF("快速上传模式结束")
        end, duration * 60 * 1000)
    else -- 退出快速上传模式
        fastUpload = false
        logF("退出快速上传模式")
    end
end
-- 触发方式
-- 1. 服务器指令:0x8202消息设置上传间隔
-- 2. 本地逻辑:检测到特殊场景(如放学时间)
-- 3. 手动调用:
common.setfastUpload(10)

第五部分:项目代码逐行过

项目代码:[Gitee仓库链接]

打开代码一行行过就行,不用写出来了

第六部分:实际应用演示

6.1 演示准备

硬件准备

  1. Air8000A开发板(已刷本项目固件)
  2. SIM卡(已开通数据业务)
  3. 锂电池(3000mAh)
  4. 电脑(串口调试+Web平台)

软件准备

  1. LuaTools(串口调试/代码下载)
  2. Web监控平台(显示轨迹)

注:目前定位监控平台不对外开放,仅在直播过程中,演示使用一下

第七部分:扩展与定制

7.1 如何适配新硬件?

-- 步骤1:修改硬件配置
-- 在bootup.lua中调整GPIO和参数
local hardwareConfig = {
    gnssUart = 2, -- GNSS串口号
    gnssBaudrate = 115200, -- 波特率
    powerKeyPin = 46, -- 电源键GPIO
    chargeDetectPin = 40, -- 充电检测GPIO
    vibrationPin = gpio.WAKEUP2, -- 震动传感器
    leds = { -- LED指示灯
        network = 1,
        gnss = 17,
        charge = 21
    }
}

-- 步骤2:调整功耗参数
if _G.NEW_HARDWARE then
    _G.GNSS_LOWPOWER_ENABLE = true
    _G.GNSS_LOWPOWER_INTERVAL = 120 -- 新硬件续航更长
end
-- 步骤3:校准电池曲线
local newBatteryCurve = {4200, 4180, 4160, 4140, 4120 -- 100%-96%-- ... 根据实际硬件调整
}

7.2 添加新服务器

-- 新建一个服务器模块:myServer.lualocal 
myServer = {}

function myServer.dataSend(data) -- 1. 建立TCP连接
    local socket = socket.create(nil, "myServer")
    socket.config(socket, myConfig.port, false)
    socket.connect(socket, myConfig.host, myConfig.port)

    -- 2. 数据格式转换(如果需要)
    local myFormatData = convertToMyFormat(data)

    -- 3. 发送数据
    local result = socket.tx(socket, myFormatData)

    -- 4. 返回结果
    return result
end
function myServer.isConnected() -- 检查连接状态
    return connectStatus
end
-- 在bootup.lua中添加
local myServer = require "myServer"
srvs.add(myServer)

7.3 自定义轨迹算法

-- 继承或替换trackCompensate模块
local myTrack = require "trackCompensate" -- 方法1:修改配置
myTrack.setConfig({
    scene = "vehicle",
    personParams = {
        distanceThreshold = 30, -- 更严格的过滤
        cornerAngleThreshold = 90 -- 更早的拐点检测
    }
})

-- 方法2:完全自定义算法
local function myCompensateAlgorithm(lat, lng, course, speed)
    -- 添加卡尔曼滤波
    local filtered = kalmanFilter(lat, lng)

    -- 添加地图匹配(如果有地图数据)
    if hasMapData then
        return mapMatching(filtered.lat, filtered.lng)
    end
    return filtered.lat, filtered.lng, course, speed
end
-- 在common.lua中替换调用
-- 原调用:trackCompensate.compensate()
-- 新调用:myCompensateAlgorithm()

7.4 添加新功能模块

-- 示例:添加温度监控模块
local temperature = {}

function temperature.init() -- 初始化温度传感器
    sensor.init()

    -- 定时读取温度
    sys.timerLoopStart(function()
        local temp = sensor.read()
        logF("当前温度:", temp, "°C")

        -- 高温报警
        if temp > 50 then
            sys.publish("HIGH_TEMP_ALARM", temp)
        end
    end, 60000) -- 每分钟检查一次
end
function temperature.getCurrent()
    return currentTemperature
end
-- 在bootup.lua中加载
_G.temperature = require "temperature"
temperature.init()

第八部分:Q&A环节

8.1 技术问题

Q1:AGPS如何工作的?需要服务器支持吗?

A:这个在上一讲中有提到,AGPS(辅助GPS)通过两个步骤加速定位:

  1. 基站定位:通过附近的基站(单基站单独定位,多基站的话进行三角定位),确定大致位置(精度50-1500米)
  2. 星历下载:从服务器获取当前可用的卫星信息,减少搜索时间

在我们的项目中,exgnss库已经内置了完整的AGPS逻辑,包括:

  • 自动获取基站信息
  • 从互联网下载星历数据
  • 时间同步校准
  • 无需额外的客户自建服务器支持,使用合宙的AGPS服务

Q2:轨迹补偿算法会增加延迟吗?

A:几乎零延迟。原因:

  1. 轻量计算:所有算法都是简单的数学运算,在毫秒级完成
  2. 本地处理:在设备端实时处理,不依赖网络
  3. 历史缓存:最多保存5个点,内存占用极小
  4. 异步执行:在数据上报周期(5分钟)内完成,不影响实时性

实际测试中算法执行时间 < 10ms,完全可以忽略。

Q3:如何保证数据不丢失?

A:四级数据保障机制:

-- 1. 本地缓存
local dataCache = {} -- 最大200条
if #dataCache > 200 then -- FIFO淘汰,但记录已删除数量
    fskv.set("lost_data_count", lostCount + 1)
end
-- 2. 失败重试
local retryCount = 0
while not sendSuccess and retryCount < 3 do
    sendSuccess = srvs.dataSend(data)
    retryCount = retryCount + 1
    if not sendSuccess then
        sys.wait(1000) -- 1秒后重试
    end
end
-- 3. 确认机制
-- JT808协议要求服务器回复0x8001确认包
-- 未收到确认会触发重传

-- 4. 持久化存储
-- 关键数据(如最后位置)写入fskv
-- 断电后仍可恢复

Q4:最多支持多少个服务器同时连接?

A:理论上64个socket通道限制,实际受硬件内存限制:

  • 内存限制:每个TCP连接约10-20KB内存
  • Air8000A:建议不超过8个同时连接
  • 网络带宽:每个连接需要独立的数据流

优化建议:

  1. 主备模式:主服务器故障时切换备用
  2. 分级上报:关键数据报所有服务器,普通数据只报主服务器
  3. 按需连接:非实时服务器可以间歇性连接

8.2 业务问题

Q5:这个项目适合学生卡场景吗?有什么特殊考虑?

A:非常适合!这个示例demo就是根据合宙定制的学生卡项目进行了脱敏和算法优化而来,

学生场景特点

  • 作息规律:上学、放学时间固定
  • 活动范围:家→学校→辅导班
  • 运动模式:步行+公共交通
  • 安全需求:实时追踪+电子围栏

可以额外加的优化

-- 1. 上下学时间自动快速上传
local schoolTimeConfig = {
    goToSchool = "07:00-08:30",
    goHome = "15:30-18:00"
}

-- 2. 电子围栏自动预警
function checkGeoFence(lat, lng)
    if not isInSchool(lat, lng) and isSchoolTime() then
        sys.publish("OUT_OF_SCHOOL_ALARM")
    end
end
-- 3. 低电量家长提醒
if batteryPercent < 20 then
    sendSmsToParent("设备电量低,请及时充电")
end

Q6:项目部署需要哪些准备工作?

A:四步部署法:

第一步:硬件准备

1. 采购硬件清单所有部件
(本项目只用到了Air8000A核心板、一块电池、以及一张物联网卡[可以拿手机副卡零时顶替一下])
2. 烧录固件(本项目代码)
4. 插入SIM卡(开通数据业务)

第二步:服务器准备(这里就不做过多介绍了,每个客户的服务器都不一样,本示例中的服务器为合宙临时测试服务器,即使客户烧录了代码,没有账号也看不到设备,本web平台仅作展示使用,具体后期怎么开放出来,内部还在商量中)

1. 准备云服务器(12G足够
2. 部署JT808协议解析服务
3. 配置数据库(MySQL/PostgreSQL
4. 部署Web管理平台

第三步:配置对接

1. cfg.lua中配置服务器地址
2. 在平台注册设备ID
3. 测试数据收发
4. 配置报警规则和电子围栏

第四步:批量部署

1. 使用量产工具批量烧录
2. 自动化测试每台设备
3. 包装和发货
4. 提供用户使用文档

8.3 进阶问题

Q7:如何进一步降低功耗?

A:五级功耗优化策略:

已实现(当前项目):

  1. GNSS按需开关
  2. 状态机智能切换
  3. 心跳间隔优化

可进一步优化

-- 1. 深度睡眠模式

pm.power(pm.WORK_MODE, 3) -- 深度睡眠,功耗<12uA
-- 2. 事件唤醒机制
gpio.setup(wakeupPin, function()
    pm.power(pm.WORK_MODE, 0) -- 震动唤醒,进入工作模式
end, gpio.PULLUP, gpio.RISING)

-- 3. 自适应心跳
local function adaptiveHeartbeat()
    if isMovingFast then
        return 10 -- 快速移动,10秒心跳
    elseif isNight then
        return 300 -- 夜间,5分钟心跳
    else
        return 60 -- 默认60秒
    end
end

具体实现

function handleSignalLoss()-- 1. 检测信号丢失
if not exgnss.is_fix() and lastFixTime then
local lossDuration = os.time() - lastFixTime

        -- 2. 短期丢失:惯性导航
        if lossDuration < 120 then  -- 2分钟内
        local estimated = inertialNavigation(
                lastPosition, 
                lastSpeed, 
                lastCourse,
                lossDuration
            )
            return estimated
        end
        -- 3. 长期丢失:切换LBSelse
            logF("GNSS信号丢失超过2分钟,切换基站定位")
            return lbsLocation()
        endendend-- 信号恢复后的轨迹修正
        function correctAfterRecovery(estimatedPoints, actualPoints)
        -- 使用B样条曲线平滑过渡
        local smoothed = bsplineSmooth(estimatedPoints, actualPoints)
    return smoothed
end

8.4 天线相关问题

天线相关问题又分了很多种,有如下几个大类

  1. 天线使用问题
  2. 天线设计问题
  3. 星系切换问题
  4. 干扰问题

8.4.1 天线使用问题

有源/无源天线混淆

有部分开发者经常遇到,自己去了户外,按理说应该在35S左右就能定位成功了啊,怎么自己一两分钟都没几颗星,等了10多20分钟依旧还是定位不成功,同步对比手机,发现差距不止一点点,此时应该先检查GNSS天线设计问题,看看自己是不是将有源天线插给了无源天线预留的底座,或者无源天线插给了有源天线预留的底座

8.4.2 天线设计问题

更多客户遇到的,不是户外定位不到,而是户外定位速度极其的慢的问题,常见于无源天线(因为无源天线对结构、PCB、走线要求都比较高),如果自己设计没有注意下面几点,是很有可能定位不到/定位极其的慢的。

8.4.2.1 无源天线设计注意事项
  • 我们的GPS模块上均内置18dBm增益的GPS LNA,可以直接将陶瓷介质的无源天线焊接在模块GPS_ANT PIN脚处使用。 产品布局的时候,GPS陶瓷天线朝上摆放;模块可以放到PCB的另一面。这样就可以做到GPS_ANT PIN到天线焊盘走线尽可能短。
  • 匹配电路;如果天线焊盘离模块的GPS_ANT PIN脚很近,那么可以不预留匹配电路。如果由于结构等其他原因造成GPS天线远离模块GPS_ANT PIN,那么建议预留pi型匹配电路。模块 GPS_ANT PIN到GPS天线焊盘之间走线必须做50欧姆特性阻抗控制;如果是多层板,建议阻抗线走L1层,L2层镂空参考L3的地。2层板走线线宽可以参考GSM天线部分走线线宽。
  • 天线下方不要走线并做漏铜处理做天线的反射面;见下图:

  • 天线周边不要有干扰源,特别是DCDC等器件;另外周边也不要有比GPS天线高的金属器件:如下图:

8.4.2.2 有源天线注意事项

有源天线构造与实物,见下图

红框内GPS有源天线组成部分为:陶瓷天线、声表滤波器、低噪声放大电路、射频线缆、RF接头。 其中低噪声放大电路是将信号进行放大和滤波的部分。

  1. PCB尺寸对天线性能的影响 承载陶瓷天线的PCB形状及面积。由于GPS有触地反弹的特性,当背景是7cm×7cm无间断大地时,patch天线的效能可以发挥到极致。虽然受外观结构等因素制约,但尽量保持相当的面积且形状均匀。另外放大电路增益的选择必须配合后端LNA增益;一般不建议有源天线增益超过29dBm,否则信号过饱和可能会导致自激。
  2. 内外置天线兼容和供电处理; 参考电路如下,R5和R6是为了兼容陶瓷PATCH天线和有源天线做的共PAD兼容设计;L6和C38是有源天线供电电源滤波电路。

  1. GPS模块使用外置天线时的供电处理。PCB部分如下图

8.4.2.3 GPS天线选型建议
  1. 在终端结构空间容许,能够统一保证GPS天线面朝上的安装使用状态;并且周边没有大的金属物件遮挡的情况下,建议使用GPS陶瓷天线,在空间容许的情况下尽量选择大尺寸的陶瓷天线。
  2. 在不能保证终端使用状态,且空间受限:比如手机,带定位功能的胸牌;建议使用FPC天线
  3. 在明确终端安装环境恶劣,并且对GPS性能有较高要求的;建议使用GPS有源天线
  4. 在不能保证产品安装使用状态,但是空间不受限制,也可以选择类似于GSM的外置棒状天线。
8.4.2.4 对天线厂家的要求
  1. VSWR(电压驻波比):GPS天线电压驻波比一般要求调到1.5左右。
  2. 功率:效率一般要求在40%左右
  3. 平均增益:平均增益要求在-0.5dB
  4. OTA:一般天线厂大多不具备GPS 天线OTA测试环境,天线调试好后可以以实际测试数据做标准来衡量,一般我们GPS实测时要求是:可用于定位卫星颗数大于6颗以上,最强的信号在45 dB/Hz左右,要有3颗卫星信号大于40 dB/Hz。

8.4.3 星系切换问题

有很多客户遇到过,模组默认固件,只打开GNSS电源,35S左右就能定位到了,但是切换成单北斗,就需要2分钟多甚至更长时间才能定位成功。

首先明确一点,合宙的大多数模组,均使用的单频(L1)GNSS芯片,所以内部能搜到的北斗卫星,只有B1C或者B1I,这两个频段的北斗卫星,上一讲中我也提到过,北斗卫星为高轨卫星,在同一片区域内,卫星数可能不会很多,实测在我家附近的广场上,单频(L1)GNSS芯片,只能搜到这几颗北斗卫星

所以,在明确自己是真正需要单北斗/单GPS或者其他星系前,尽量不要将模块切换为单星系状态,如果客户对单北斗需求非常明确,建议选择真正的单北斗芯片,杜绝后患,因为很多单北斗应用是需要进实验室过多项认证的,使用多星系GNSS芯片,有极大概率过不去单北斗的认证。

8.4.4 外部干扰源问题

此种情况不能说常见,但是确实客观存在,之前有部分客户就遇到了,在他们公司附近一直定位不到,但是客户放在自己小区前面广场上就能定位成功,查看地图得知,客户的公司附近,有类似"中国军工"等涉密单位,不只是GNSS定位不到,偶尔自己的手机5G/4G信号也没有,此种情况定位不到的原因不言而喻了。

不过还有少量客户遇到的干扰源还是比较明显,例如只针对GPS频段发射的干扰源,此时切换为单北斗模式,即使是单频模组,在部分情况下,还是能够正常定位成功的。

以上四点是最为常见的四种无法定位的情况,如果你使用合宙的GNSS模组排除了这四点,依旧无法定位,欢迎你来找合宙,我们将会竭力为您排查您所遇到的问题

结束语

项目价值总结

技术创新点

  1. 🎯 智能四状态机:完美平衡精度与功耗
  2. 🛣️ 自适应轨迹补偿:减少80%GPS漂移
  3. 🔋 三层功耗管理:续航从一两天提升到一周左右
  4. 🌐 多服务器架构:一次开发,多处部署
  5. 🛡️ 工业级可靠性:完整JT808协议+多重保障

商业价值

  • 低成本:软件开源,硬件上用户只需Air8000A核心板+电池即可实现主要功能
  • 易部署:模块化设计,快速定制
  • 可扩展:支持百万级设备接入
  • 标准化:符合行业协议,易于集成

学习收获

通过这个项目,你可以学到:

  1. 嵌入式开发全流程:从硬件选型到软件部署
  2. 物联网架构设计:设备端+服务器端+平台端
  3. 算法优化实践:轨迹补偿、功耗优化、网络传输
  4. 工程化思维:模块化、配置化、可测试性
  5. 产品化思维:用户体验、成本控制、可维护性
  6. 天线设计建议:有源无源设计及其干扰源排查

下一步建议

初学者

  1. 下载代码,编译运行
  2. 修改配置,体验不同模式
  3. 添加一个简单功能(如LED控制)

进阶者

  1. 优化轨迹算法(尝试卡尔曼滤波)
  2. 实现Web管理平台
  3. 设计硬件PCB,降低成本

企业用户

  1. 基于此架构开发产品
  2. 部署到云平台,服务客户
  3. 根据反馈持续优化

资源获取

项目代码:[Gitee仓库链接]

文档资料:[LuatOS课程-011 GNSS应用相关知识]

最后感谢

感谢大家的耐心观看! 希望这个项目能给你带来启发和帮助。

记住:技术最终要服务于业务,解决实际问题。这个项目的核心不是复杂的算法,而是用合适的技术解决真实的需求

互动提醒:有任何问题欢迎在合宙的企业微信群里提问,我或者我们的同事都会尽量去回复。也欢迎关注合宙公众号,后续会有更多物联网项目分享。

结束语:物联网的世界很大,我们的探索才刚刚开始。让我们一起,用代码连接万物,创造更智能的世界!

谢谢大家!我们下次直播再见! 👋