在物联网项目开发中,智能定位系统是一类常见且实用的应用场景,本文将基于 LuatOS,分享一款智能定位系统的开发思路与相关实现要点。
在实际开发过程中,类似学生卡定位器的需求十分普遍,这类需求通常对定位精度、设备续航能力、轨迹显示效果以及多平台适配性均有明确要求,而基于 LuatOS 的智能定位系统,可针对性解决这类开发需求中的核心痛点。
项目特点:
- 🛰️ 三合一定位:GNSS + 基站 + WiFi
- 🔋 续航:智能功耗管理,运动才定位
- 🛣️ 轨迹优化:减少80%GPS静态漂移以及运动漂移,路线更符合真实路径
- 🌐 多平台兼容:一次开发,同时上报多个服务器
- 🎯 工业标准:完整JT808协议实现
- 🚩特点:exgnss扩展库和exvib扩展库
适合场景:
- 👨🎓 学生安全卡、老人定位器
- 🐕 宠物追踪、牲畜管理
- 🚗 车辆监控、物流追踪
- 📦 资产定位、贵重物品跟踪
前言
当前项目为演示项目,使用到的web平台为合宙自建的内部服务器,暂时不对外开放,此项目仅仅是为了给大家演示一下exgnss扩展库和exvib库如何融入到定位器相关项目内,也会重点讲解gnss应用场景有关的代码和业务逻辑,至于后期如何开放该平台等事宜,等待内部讨论后给出。
项目功能:
- 经纬度数据、电量、卫星信号质量、速度、电池状态(充电中/未充电)等数据上报到合宙定位测试服务器
- 根据服务器下发的指令 进入/退出快速定位模式
- 功耗优化,区分运动和静止状态
项目需求:
- 由Air8000A内部加速度传感器判断是否开启/关闭GNSS
- 按照JT808协议和服务器进行通讯
- 可以上传多个服务器(为后期做拓展)
- 尽可能的减少静态/动态漂移带来的轨迹失真
- 与服务器直接保持长连接状态,并且尽可能的做到功耗最低
使用到的最为重要的两个扩展库介绍:
exgnss:
exgnss库实际上是把底层提供的libgnss接口封装了一层,让配置过程更快速一点,更方便用户快速上手,exgnss库最重要的三个接口为:
1exgnss.setup(gnssotps) --配置GNGSS输出相关 2exgnss.open(mode,para)--硬件层面打开GNSS 3exgnss.close(mode,para)--硬件层面关闭GNSS 4
至于这三个接口的具体使用方法,以及传参,这里有接口/参数说明以及demo示例,后面项目中也用到了,这里带大家简单过一次。就不做赘述了
常量定义:
exgnss.DEFAULT
1常量含义:exgnss应用模式1,常开模式; 2数据类型:number; 3示例代码:--- exgnss应用模式1. 4 -- 打开gnss后,gnss定位成功时,如果有回调函数,会调用回调函数 5 -- 使用此应用模式调用exgnss.open打开的“gnss应用”,必须主动调用exgnss.close 6 -- 或者exgnss.close_all才能关闭此“gnss应用”,主动关闭时,即使有回调函数,也不会调用回调函数 7 -- 通俗点说就是一直打开,除非自己手动关闭掉 8 local function mode_cb(tag) 9 log.info("TAGmode_cb+++++++++",tag) 10 log.info("nmea", "rmc", json.encode(exgnss.rmc(2))) 11 end 12 exgnss.open(exgnss.DEFAULT,{tag="MODE",cb=mode_cb}) 13
本简易定位器demo中用到的就是这个常量
还有**exgnss.TIMERORSUC(**到规定时间或者GNSS定位成功后[开拓地带约为35S]关闭GNSS)和
exgnss.TIMER[到规定时间就关,不管GNSS定位成功与否]
1exgnss=require("exgnss") --因为是扩展库不是核心库 所以需要require(类似C语言中的#incloud) 2 3local function mode_cb(tag) 4 log.info("定位应用的回调函数",tag) 5 log.info("让我看看当前经纬度", json.encode(exgnss.rmc(2))) 6end 7 8local function gnss_fnc() 9 local gnssotps={ 10 gnssmode=1, --1为卫星全定位,2为单北斗 11 agps_enable=true, --是否使用AGPS,开启AGPS后定位速度更快,会访问服务器下载星历,对Air8000来说,星历时效性:北斗1小时,GPS2小时,默认下载星历的时间为1小时,即一小时内只会下载一次 12 debug=true, --是否输出调试信息(GNSS芯片输出的nmea数据) 13 14 --其他参数用户可以看接口说明,本demo中用不到其他参数 15 } 16 --设置gnss参数 17 exgnss.setup(gnssotps) 18 --开启gnss应用,到规定的60S就关闭,不管定位成功与否 19 exgnss.open(exgnss.TIMER,{tag="用户自定义应用名",val=60,cb=mode_cb}) --使用TIMER模式,开启60s后关闭 20 21 sys.wait(40000) 22 log.info("关闭一个gnss应用") 23 --关闭一个gnss应用 24 exgnss.close(exgnss.TIMER,{tag="用户自定义应用名"})--关闭tag为“用户自定义应用名”应用 25 --查询gnss应用状态 26 log.info("gnss应用状态",exgnss.is_active(exgnss.TIMER,{tag="用户自定义应用名"})) 27 sys.wait(10000) 28 --关闭所有gnss应用 29 exgnss.close_all() 30 --查询最后一次定位结果 31 local loc= exgnss.last_loc() 32 if loc then 33 log.info("lastloc", loc.lat,loc.lng) 34 end 35end 36 37sys.taskInit(gnss_fnc) 38 39 40--GNSS定位状态的消息处理函数: 41local function gnss_state(event, ticks) 42 -- event取值有 43 -- "FIXED":string类型 定位成功 44 -- "LOSE": string类型 定位丢失 45 -- "CLOSE": string类型 GNSS关闭,仅配合使用exgnss.lua有效 46 -- ticks number类型 是事件发生的时间,一般可以忽略 47 log.info("exgnss", "state", event) 48 if event=="FIXED" then 49 --获取rmc数据 50 --json.encode默认输出"7f"格式保留7位小数,可以根据自己需要的格式调整小数位,本示例保留5位小数 51 log.info("nmea", "rmc0", json.encode(exgnss.rmc(0),"5f")) 52 end 53end 54sys.subscribe("GNSS_STATE",gnss_state) 55
exvib:
exvib库实际上是操作了Air780EGP/Air780EGG/Air780EGH/Air8000A/Air8000AB/Air8000N/Air8000U/Air8000D/Air8000DB模组内的一颗G-Sensor加速度传感器,模组内部硬件具体怎么接的,咱们这个项目中无需关心,exvib库的三种模式主要用于以下场景:
- 微小震动检测:用于检测轻微震动的场景,例如用手敲击桌面,加速度量程2g;
- 运动检测:用于电动车或汽车行驶时的检测和人行走和跑步时的检测,加速度量程4g
- 跌倒检测:用于人或物体瞬间跌倒时的检测,加速度量程8g
咱们这个项目 使用的就是运动检测,exvib库就三个接口,分别为:
1exvib.read_xyz()--读三轴(G-Sensor)传过来的XYZ轴数据 2exvib.open(mode)--打开三轴(G-Sensor) 3exvib.close()--关闭三轴(G-Sensor) 4
exvib库完整的说明可以看这里,后面项目具体代码中也有,这三个接口不难理解,唯一注意的一点是exvib.open这个接口
exvib.open(mode)
1参数含义:加速度传感器的应用模式; 2数据类型:number; 3取值范围: 41 - 微小震动检测,用于检测轻微震动的场景,例如用手敲击桌面,加速度量程2g; 52 - 运动检测,用于电动车或汽车行驶时的检测和人行走和跑步时的检测,加速度量程4g; 63 - 跌倒检测,用于人或物体瞬间跌倒时的检测,加速度量程8g; 7是否必选:是; 8
第一部分:项目整体架构
1.1 模块化架构设计

架构特点:
1. 分层设计:
- 定位核心层:commom(管理了exgnss和trackCompensate)
- 传感器层:vibration(三轴)
- 电源管理层:charge、manage
- 通讯协议层:api、jt808
- 服务器层:srvs、auxServer
- 具体后面讲代码的时候会一个个lua文件打开,仔细说明,这里就不做过多赘述了
2. 事件驱动:
1-- 发布订阅模式,完全解耦 2sys.publish("SYS_STATUS_RUN") -- 运动事件 3sys.subscribe("GNSS_STATE", fn) -- 定位状态变化 4sys.waitUntil("IP_READY", 10000) -- 等待网络就绪 5
3. 配置驱动:
1-- cfg.lua 统一管理所有配置 2_G.GNSS_LOWPOWER_ENABLE = true -- 低功耗开关 3_G.GNSS_LOWPOWER_INTERVAL = 60 -- 定位间隔(单位:S) 4-- 轨迹补偿参数 5trackCompensate.setConfig({ 6 scene = "person", -- 人员模式 7 autoAdaptive = true, -- 自动适应 8 distanceThreshold = 50 -- 漂移过滤阈值 9}) 10
4. gnss应用场景****管理:
1-- 避免资源冲突,智能管理GNSS 2exgnss.open(exgnss.DEFAULT, {tag = "common_app"}) 3exgnss.open(exgnss.TIMERORSUC, {tag = "lowpower"}) 4-- 两个tag都关闭时,GNSS才真正关闭 5
1.2 核心模块介绍
第二部分:智能定位状态机
一、什么是状态机
状态机的全称叫做有限状态机,是计算机的运行的基本的元素。
状态机由三个要素组成:
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模式 - 定位捕获
1-- 代码位置:common.lua 第200-250行 2function captureMode() 3 logF("========== 进入CAPTURE模式 ==========") 4 5 -- 1. 打开GNSS,启用AGPS 6 exgnss.open(exgnss.DEFAULT, { 7 tag = "common_app" 8 }) 9 10 -- 2. WiFi扫描辅助定位 11 wlan.scan() 12 13 -- 3. 最多尝试10次(首次)或4次(后续) 14 for i = 1, maxAttempts do 15 srvs.dataSend() -- 触发定位数据采集-- 等待定位结果 16 local result = sys.waitUntil("GNSS_STATE", 3000) 17 if result == "FIXED" or exgnss.is_fix() then 18 logF("定位成功!耗时", i * 3, "秒") 19 return "TRACKING" -- 进入追踪模式 20 end 21 logF("定位尝试", i, "/", maxAttempts, "失败") 22 end 23 -- 4. 定位失败处理 24 if not manage.isRun() then -- 静止状态 25 logF("静止状态,降级到基站定位") 26 return "STATIC_LBS" 27 else 28 logF("运动状态,继续尝试") 29 return "CAPTURE" -- 继续尝试 30 end 31end 32
TRACKING模式 - 运动追踪
1-- 关键特性: 2-- 1. 1Hz定位频率,实时性高 3-- 2. 自动轨迹补偿,减少漂移 4-- 3. 双模式上传:正常5分钟/快速3秒 5-- 4. 智能休眠检测:20秒无震动 6-- 上传频率控制 7if fastUpload then -- 快速模式:3秒/次(紧急情况) 8 srvs.dataSend() -- 立即上传 9else -- 正常模式:缓存+批量上传 10 if waitUploadTimes >= 30 then -- 约5分钟 11 uploadCache() -- 批量上传缓存数据 12 end 13end 14
STATIC模式 - 静止优化
1-- STATIC_GNSS:使用最后有效位置 2-- STATIC_LBS:使用基站定位(GNSS失败时) 3-- 共同特点: 4-- 1. 关闭GNSS,功耗降至1mA 5-- 2. 定期心跳保持连接 6-- 3. 运动立即唤醒,无感知切换 7
2.3 状态切换条件表
第三部分:轨迹补偿算法详解
3.1 GPS为什么需要补偿?
现场演示:请大家看这张图,红色框为原始GPS轨迹:

问题根源(上一节也讲过了):
- 多路径效应:信号反射,误差10-50米
- 卫星几何:仰角低时误差大
- 大气延迟:电离层影响
- 时钟误差:接收机时钟不准
3.2 四层补偿算法

3.3 核心算法代码解析
算法1:距离阈值过滤
1-- trackCompensate.lua 第80-120行 2local function distanceFilter(lat, lng, params) 3 if #historyBuffer == 0 then 4 return lat, lng -- 无历史数据,直接使用 5 end 6 7 local last = historyBuffer[#historyBuffer] 8 local distance = calculateDistance(last.lat, last.lng, lat, lng) 9 -- 自适应阈值:人员50米,车辆200米 10 local threshold = params.distanceThreshold 11 if distance > threshold then 12 logF("漂移点过滤:距离", distance, "米 > 阈值", threshold, "米") 13 return last.lat, last.lng -- 返回历史位置 14 end 15 return lat, lng -- 正常点 16end 17
算法2:航向平滑(解决360°跳变)
1-- 关键问题:359° → 1° 实际只变了2°,但差值算成358° 2-- 解决方案:使用三角函数处理角度循环 3local function smoothCourse(currentCourse, historyCourses) 4 local sumSin = 0 5 local sumCos = 0 6 -- 加权平均,最近的数据权重高 7 for i, course in ipairs(historyCourses) do 8 local weight = 1.0 / (i + 1) -- 权重递减 9 sumSin = sumSin + math.sin(math.rad(course)) * weight 10 sumCos = sumCos + math.cos(math.rad(course)) * weight 11 end 12 -- 计算平均航向 13 local avgCourse = math.deg(math.atan2(sumSin, sumCos)) 14 avgCourse = (avgCourse + 360) % 360 -- 确保0-360范围 15 -- 检查是否跳变 16 local diff = math.abs(currentCourse - avgCourse) 17 if diff > 180 then 18 diff = 360 - diff 19 -- 处理循环 20 end 21 if diff > params.courseChangeThreshold then 22 logF("航向跳变补偿:", currentCourse, "→", avgCourse) 23 return avgCourse 24 end 25 return currentCourse 26end 27
算法3:速度突变过滤
1-- 原理:GNSS速度有时会从0突然跳到30km/h 2-- 验证:用实际位移计算的速度更可靠 3local function smoothSpeed(gnssSpeed, distance, timeDiff) 4 -- 计算基于位移的实际速度 5 local displacementSpeed = 0 6 if timeDiff > 0 then 7 displacementSpeed = (distance / timeDiff) * 3.6 -- m/s → km/h 8 end 9 -- 差异过大时,使用位移速度 10 local speedDiff = math.abs(gnssSpeed - displacementSpeed) 11 if speedDiff > params.speedChangeThreshold then 12 logF("速度突变:GNSS=", gnssSpeed, "位移=", displacementSpeed, "差值=", speedDiff) 13 return displacementSpeed 14 end 15 return gnssSpeed 16end 17
算法4:拐点补偿
1-- 解决急转弯被拉直的问题 2local function cornerCompensation(lat, lng, bearing, speed) 3 -- 1. 检测转弯角度 4 local angleDiff = calculateAngleDifference() 5 6 -- 2. 急转弯判定(人员120°,车辆90°) 7 if angleDiff > params.cornerAngleThreshold then 8 -- 3. 沿当前航向预测位置 9 local distance = (speed / 3.6) * 3 10 -- 假设3秒间隔 11 local estimatedLat = lat + (distance / 6371000) * math.cos(math.rad(bearing)) 12 local estimatedLng = lng + (distance / 6371000) * math.sin(math.rad(bearing)) / math.cos(math.rad(lat)) 13 14 -- 4. 检查预测位置是否合理 15 local predictionError = calculateDistance(lat, lng, estimatedLat, estimatedLng) 16 if predictionError < params.distanceThreshold then 17 logF("拐点补偿:角度", angleDiff, "°") 18 return estimatedLat, estimatedLng 19 end 20 end 21 return lat, lng 22end 23
3.4 双场景自适应系统
1-- 人员模式 vs 车辆模式 2local config = { 3 scene = "person", -- 默认人员模式 4 autoAdaptive = true, -- 自动切换 5 personParams = { 6 distanceThreshold = 50, -- 50米漂移过滤 7 speedChangeThreshold = 10, -- 10km/h速度突变 8 courseChangeThreshold = 60, -- 60°航向跳变 9 cornerAngleThreshold = 120 -- 120°急转弯 10 }, 11 vehicleParams = { 12 distanceThreshold = 200, -- 200米漂移过滤 13 speedChangeThreshold = 50, -- 50km/h速度突变 14 courseChangeThreshold = 30, -- 30°航向跳变 15 cornerAngleThreshold = 90 -- 90°急转弯 16 }, 17 18 adaptiveSpeedThreshold = 15 -- 15km/h切换阈值 19} 20 21-- 自动场景判断 22local function getCurrentParams() 23 if config.autoAdaptive and #historyBuffer >= 3 then 24 -- 计算最近3个点的平均速度 25 local avgSpeed = 0 26 for i = #historyBuffer - 2, #historyBuffer do 27 avgSpeed = avgSpeed + historyBuffer[i].speed 28 end 29 avgSpeed = avgSpeed / 3 30 if avgSpeed >= config.adaptiveSpeedThreshold then 31 logF("自动切换:车辆模式(速度", avgSpeed, "km/h)") 32 return config.vehicleParams 33 else 34 logF("自动切换:人员模式(速度", avgSpeed, "km/h)") 35 return config.personParams 36 end 37 end 38 -- 固定模式 39 return config.scene == "vehicle" and config.vehicleParams or config.personParams 40end 41
3.5 优化效果对比
测试数据:
1原始轨迹数据: 2 点1: 31.123456, 121.654321, 速度5, 航向180 3 点2: 31.123500, 121.654300, 速度25, 航向185 ← 速度突变! 4 点3: 31.123800, 121.654000, 速度6, 航向359 ← 航向跳变! 5 6优化后数据: 7 点1: 31.123456, 121.654321, 速度5, 航向180 8 点2: 31.123480, 121.654310, 速度5, 航向182 ← 速度修正 9 点3: 31.123520, 121.654280, 速度5, 航向181 ← 航向修正 10
量化提升:
第四部分:功耗优化策略
4.1 三层功耗管理架构
1-- 第一层:硬件级控制 2-- 引用计数管理,避免资源冲突 3local function hardwarePowerControl() -- 不同模块使用独立tag 4 exgnss.open(exgnss.DEFAULT, { 5 tag = "common_app" 6 }) -- 主定位 7 exgnss.open(exgnss.TIMERORSUC, { 8 tag = "lowpower" 9 }) -- 低功耗模式 10 -- 只有所有tag都关闭,硬件才真正断电 11 -- 避免:A模块关闭影响B模块使用 12end 13-- 第二层:应用级控制 14-- 状态机自动切换 15local function applicationPowerControl() 16 -- TRACKING模式:GNSS常开 17 -- STATIC模式:GNSS关闭 18 -- 根据运动状态自动调整 19 if manage.isRun() then 20 logF("运动状态,保持GNSS开启") 21 else 22 logF("静止状态,关闭GNSS节省功耗") 23 exgnss.close(exgnss.DEFAULT, { 24 tag = "common_app" 25 }) 26 end 27end 28-- 第三层:系统级控制 29-- 统一休眠管理 30local function systemPowerControl() -- manage模块引用计数 31 manage.wake("READ_GNSS_DATA") -- 读取数据时唤醒 32 manage.sleep("READ_GNSS_DATA") -- 读取完成后休眠 33 34 manage.wake("charge") -- 充电时唤醒 35 manage.sleep("charge") -- 充电结束休眠 36 -- 检查所有tag,全部休眠时进入低功耗模式 37 local allSleeping = true 38 for tag, state in pairs(manage.tags) do 39 if state > 0 then 40 allSleeping = false 41 break 42 end 43 end 44 if allSleeping then 45 pm.power(pm.WORK_MODE, 1) -- 进入长连接低功耗模式休眠 46 logF("所有模块休眠,进入LIGHT模式") 47 end 48end 49
4.2 震动检测优化算法
1-- vibration.lua 核心逻辑 2local function vibrationOptimization() 3-- 静止→运动:快速响应(5秒内2次震动) 4-- 避免误判:防止偶尔震动误触发 5-- 运动→静止:延迟确认(20秒内0次震动) 6-- 避免误判:防止行走时停顿误判为静止 7-- 有效震动检测:10秒内5次震动 8-- 防止误触:过滤无效震动(如放口袋里的晃动) 9-- 冷却机制:有效震动后30分钟内不再重复触发 10-- 避免频繁:如车辆持续震动场景 11end 12
优化空间:
- 进一步降低GNSS功耗:按需定位
- 优化网络连接:心跳间隔调整
- 深度休眠:夜间完全关闭
第五部分:数据通信架构及代码完整实现
5.1 JT808协议完整实现

5.2 数据包结构详解
1 2-- JT808 0x0200 位置信息报文结构 3local function buildPositionPacket() 4 -- 1. 消息头(12字节) 5 local header = api.NumToBigBin(0x0200, 2) -- 消息ID 6 .. api.NumToBigBin(bodyLen, 2) -- 消息体属性 7 .. simID -- 终端手机号(6字节BCD) 8 .. api.NumToBigBin(msgSn, 2) -- 消息流水号 9 10 -- 2. 消息体基础部分(28字节) 11 local body = api.NumToBigBin(0, 4) -- 报警标志 12 .. api.NumToBigBin(status, 4) -- 状态位 13 .. api.NumToBigBin(lat, 4) -- 纬度(度*1000000) 14 .. api.NumToBigBin(lng, 4) -- 经度(度*1000000) 15 .. api.NumToBigBin(altitude, 2) -- 海拔(米) 16 .. api.NumToBigBin(speed, 2) -- 速度(0.1km/h) 17 .. api.NumToBigBin(course, 2) -- 方向(度) 18 .. timeBCD -- 时间(6字节BCD) 19 20 -- 3. 扩展信息(附加项) 21 -- 里程信息 22 body = body .. api.NumToBigBin(0x01, 1) -- 附加项ID 23 .. api.NumToBigBin(4, 1) -- 长度4字节 24 .. api.NumToBigBin(mileage, 4) -- 里程值 25 26 -- 电量信息 27 body = body .. api.NumToBigBin(0x04, 1) -- 附加项ID 28 .. api.NumToBigBin(2, 1) -- 长度2字节 29 .. api.NumToBigBin(chargeState, 1) -- 充电状态 30 .. api.NumToBigBin(batteryPercent, 1) -- 电量百分比 31 32 -- WiFi信息(如有) 33 if wifiList and #wifiList > 0 then 34 body = body .. api.NumToBigBin(0x54, 1) -- WiFi附加项 35 .. buildWifiInfo(wifiList) 36 end 37 38 -- 4. 组装完整报文 39 local packet = header .. body 40 packet = packet .. string.char(api.XorCheck(packet)) -- 校验码 41 packet = msgEncode(packet) -- 转义处理 42 packet = "\x7E" .. packet .. "\x7E" -- 添加起始结束符 43 44 return packet 45end 46
5.3 多服务器管理架构
1local srvs = { 2 servers = {}, -- 服务器实例列表-- 添加服务器 3 add = function(self, server) 4 table.insert(self.servers, server) 5 logF("添加服务器,当前数量:", #self.servers) 6 end, 7 8 -- 发送数据到所有服务器 9 dataSend = function(self, data) 10 logF("========== 开始发送数据 ==========") 11 12 -- 数据预处理和日志 13 if data then 14 self:_logDataInfo(data) 15 else 16 logF("数据为空,自动调用common.monitorRecord()") 17 data = common.monitorRecord() 18 end -- 分发到所有服务器 19 local successCount = 0 20 for i, srv in ipairs(self.servers) do 21 if srv.dataSend then 22 local ok, err = pcall(srv.dataSend, data) 23 if ok then 24 successCount = successCount + 1 25 logF("服务器", i, "发送成功") 26 else 27 logF("服务器", i, "发送失败:", err) 28 end 29 end 30 end 31 32 logF("发送完成,成功:", successCount, "/", #self.servers) 33 return successCount 34 end, 35 36 -- 检查连接状态(任意一个连接成功即返回true) 37 isConnected = function(self) 38 for _, srv in ipairs(self.servers) do 39 if srv.isConnected and srv.isConnected() then 40 return true 41 end 42 end 43 return false 44 end 45} 46 47-- 使用示例 48local auxServer = require "auxServer" 49local productionServer = require "productionServer" 50 51srvs:add(auxServer) -- 测试服务器 52srvs:add(productionServer) -- 生产服务器 53-- 一次调用,多处发送 54srvs:dataSend(positionData) 55
5.4 数据上传频率控制
1-- 双模式上传策略 2local uploadConfig = { 3 normal = { 4 interval = 5 * 60, -- 5分钟 5 cacheSize = 30, -- 缓存30次后上传 6 description = "正常模式:平衡功耗与实时性" 7 }, 8 9 fast = { 10 interval = 3, -- 3秒 11 immediate = true, -- 立即上传 12 description = "快速模式:紧急追踪场景" 13 } 14} 15 16-- 模式切换接口 17function common.setfastUpload(duration) 18 if duration > 0 then -- 进入快速上传模式 19 logF("进入快速上传模式,持续", duration, "分钟") 20 fastUpload = true -- 立即触发一次上传 21 srvs.dataSend() 22 23 -- 定时自动退出 24 sys.timerStart(function() 25 fastUpload = false 26 logF("快速上传模式结束") 27 end, duration * 60 * 1000) 28 else -- 退出快速上传模式 29 fastUpload = false 30 logF("退出快速上传模式") 31 end 32end 33-- 触发方式 34-- 1. 服务器指令:0x8202消息设置上传间隔 35-- 2. 本地逻辑:检测到特殊场景(如放学时间) 36-- 3. 手动调用: 37common.setfastUpload(10) 38
第五部分:项目代码逐行过
项目代码:[Gitee仓库链接]
打开代码一行行过就行,不用写出来了
第六部分:实际应用演示
6.1 演示准备
硬件准备:
- Air8000A开发板(已刷本项目固件)
- SIM卡(已开通数据业务)
- 锂电池(3000mAh)
- 电脑(串口调试+Web平台)
软件准备:
- LuaTools(串口调试/代码下载)
- Web监控平台(显示轨迹)
注:目前定位监控平台不对外开放,仅在直播过程中,演示使用一下

第七部分:扩展与定制
7.1 如何适配新硬件?
1-- 步骤1:修改硬件配置 2-- 在bootup.lua中调整GPIO和参数 3local hardwareConfig = { 4 gnssUart = 2, -- GNSS串口号 5 gnssBaudrate = 115200, -- 波特率 6 powerKeyPin = 46, -- 电源键GPIO 7 chargeDetectPin = 40, -- 充电检测GPIO 8 vibrationPin = gpio.WAKEUP2, -- 震动传感器 9 leds = { -- LED指示灯 10 network = 1, 11 gnss = 17, 12 charge = 21 13 } 14} 15 16-- 步骤2:调整功耗参数 17if _G.NEW_HARDWARE then 18 _G.GNSS_LOWPOWER_ENABLE = true 19 _G.GNSS_LOWPOWER_INTERVAL = 120 -- 新硬件续航更长 20end 21-- 步骤3:校准电池曲线 22local newBatteryCurve = {4200, 4180, 4160, 4140, 4120 -- 100%-96%-- ... 根据实际硬件调整 23} 24
7.2 添加新服务器
1-- 新建一个服务器模块:myServer.lualocal 2myServer = {} 3 4function myServer.dataSend(data) -- 1. 建立TCP连接 5 local socket = socket.create(nil, "myServer") 6 socket.config(socket, myConfig.port, false) 7 socket.connect(socket, myConfig.host, myConfig.port) 8 9 -- 2. 数据格式转换(如果需要) 10 local myFormatData = convertToMyFormat(data) 11 12 -- 3. 发送数据 13 local result = socket.tx(socket, myFormatData) 14 15 -- 4. 返回结果 16 return result 17end 18function myServer.isConnected() -- 检查连接状态 19 return connectStatus 20end 21-- 在bootup.lua中添加 22local myServer = require "myServer" 23srvs.add(myServer) 24
7.3 自定义轨迹算法
1-- 继承或替换trackCompensate模块 2local myTrack = require "trackCompensate" -- 方法1:修改配置 3myTrack.setConfig({ 4 scene = "vehicle", 5 personParams = { 6 distanceThreshold = 30, -- 更严格的过滤 7 cornerAngleThreshold = 90 -- 更早的拐点检测 8 } 9}) 10 11-- 方法2:完全自定义算法 12local function myCompensateAlgorithm(lat, lng, course, speed) 13 -- 添加卡尔曼滤波 14 local filtered = kalmanFilter(lat, lng) 15 16 -- 添加地图匹配(如果有地图数据) 17 if hasMapData then 18 return mapMatching(filtered.lat, filtered.lng) 19 end 20 return filtered.lat, filtered.lng, course, speed 21end 22-- 在common.lua中替换调用 23-- 原调用:trackCompensate.compensate() 24-- 新调用:myCompensateAlgorithm() 25
7.4 添加新功能模块
1-- 示例:添加温度监控模块 2local temperature = {} 3 4function temperature.init() -- 初始化温度传感器 5 sensor.init() 6 7 -- 定时读取温度 8 sys.timerLoopStart(function() 9 local temp = sensor.read() 10 logF("当前温度:", temp, "°C") 11 12 -- 高温报警 13 if temp > 50 then 14 sys.publish("HIGH_TEMP_ALARM", temp) 15 end 16 end, 60000) -- 每分钟检查一次 17end 18function temperature.getCurrent() 19 return currentTemperature 20end 21-- 在bootup.lua中加载 22_G.temperature = require "temperature" 23temperature.init() 24
第八部分:Q&A环节
8.1 技术问题
Q1:AGPS如何工作的?需要服务器支持吗?
A:这个在上一讲中有提到,AGPS(辅助GPS)通过两个步骤加速定位:
- 基站定位:通过附近的基站(单基站单独定位,多基站的话进行三角定位),确定大致位置(精度50-1500米)
- 星历下载:从服务器获取当前可用的卫星信息,减少搜索时间
在我们的项目中,exgnss库已经内置了完整的AGPS逻辑,包括:
- 自动获取基站信息
- 从互联网下载星历数据
- 时间同步校准
- 无需额外的客户自建服务器支持,使用合宙的AGPS服务
Q2:轨迹补偿算法会增加延迟吗?
A:几乎零延迟。原因:
- 轻量计算:所有算法都是简单的数学运算,在毫秒级完成
- 本地处理:在设备端实时处理,不依赖网络
- 历史缓存:最多保存5个点,内存占用极小
- 异步执行:在数据上报周期(5分钟)内完成,不影响实时性
实际测试中算法执行时间 < 10ms,完全可以忽略。
Q3:如何保证数据不丢失?
A:四级数据保障机制:
1-- 1. 本地缓存 2local dataCache = {} -- 最大200条 3if #dataCache > 200 then -- FIFO淘汰,但记录已删除数量 4 fskv.set("lost_data_count", lostCount + 1) 5end 6-- 2. 失败重试 7local retryCount = 0 8while not sendSuccess and retryCount < 3 do 9 sendSuccess = srvs.dataSend(data) 10 retryCount = retryCount + 1 11 if not sendSuccess then 12 sys.wait(1000) -- 1秒后重试 13 end 14end 15-- 3. 确认机制 16-- JT808协议要求服务器回复0x8001确认包 17-- 未收到确认会触发重传 18 19-- 4. 持久化存储 20-- 关键数据(如最后位置)写入fskv 21-- 断电后仍可恢复 22
Q4:最多支持多少个服务器同时连接?
A:理论上64个socket通道限制,实际受硬件内存限制:
- 内存限制:每个TCP连接约10-20KB内存
- Air8000A:建议不超过8个同时连接
- 网络带宽:每个连接需要独立的数据流
优化建议:
- 主备模式:主服务器故障时切换备用
- 分级上报:关键数据报所有服务器,普通数据只报主服务器
- 按需连接:非实时服务器可以间歇性连接
8.2 业务问题
Q5:这个项目适合学生卡场景吗?有什么特殊考虑?
A:适合!这个示例demo就是根据定制的学生卡项目进行了脱敏和算法优化而来,
学生场景:
- 作息规律:上学、放学时间固定
- 活动范围:家→学校→辅导班
- 运动模式:步行+公共交通
- 安全需求:实时追踪+电子围栏
可以额外加的优化:
1-- 1. 上下学时间自动快速上传 2local schoolTimeConfig = { 3 goToSchool = "07:00-08:30", 4 goHome = "15:30-18:00" 5} 6 7-- 2. 电子围栏自动预警 8function checkGeoFence(lat, lng) 9 if not isInSchool(lat, lng) and isSchoolTime() then 10 sys.publish("OUT_OF_SCHOOL_ALARM") 11 end 12end 13-- 3. 低电量家长提醒 14if batteryPercent < 20 then 15 sendSmsToParent("设备电量低,请及时充电") 16end 17
Q6:项目部署需要哪些准备工作?
A:四步部署法:
第一步:硬件准备
11. 采购硬件清单所有部件 2(本项目只用到了Air8000A核心板、一块电池、以及一张物联网卡[可以拿手机副卡零时顶替一下]) 32. 烧录固件(本项目代码) 44. 插入SIM卡(开通数据业务) 5
第二步:服务器准备(这里就不做过多介绍了,每个客户的服务器都不一样,本示例中的服务器为合宙临时测试服务器,即使客户烧录了代码,没有账号也看不到设备,本web平台仅作展示使用,具体后期怎么开放出来,内部还在商量中)
11. 准备云服务器(1核2G足够) 22. 部署JT808协议解析服务 33. 配置数据库(MySQL/PostgreSQL) 44. 部署Web管理平台 5
第三步:配置对接
11. 在cfg.lua中配置服务器地址 22. 在平台注册设备ID 33. 测试数据收发 44. 配置报警规则和电子围栏 5
第四步:批量部署
11. 使用量产工具批量烧录 22. 自动化测试每台设备 33. 包装和发货 44. 提供用户使用文档 5
8.3 进阶问题
Q7:如何进一步降低功耗?
A:五级功耗优化策略:
已实现(当前项目):
- GNSS按需开关
- 状态机智能切换
- 心跳间隔优化
可进一步优化:
1-- 1. 深度睡眠模式 2 3pm.power(pm.WORK_MODE, 3) -- 深度睡眠,功耗<12uA 4-- 2. 事件唤醒机制 5gpio.setup(wakeupPin, function() 6 pm.power(pm.WORK_MODE, 0) -- 震动唤醒,进入工作模式 7end, gpio.PULLUP, gpio.RISING) 8 9-- 3. 自适应心跳 10local function adaptiveHeartbeat() 11 if isMovingFast then 12 return 10 -- 快速移动,10秒心跳 13 elseif isNight then 14 return 300 -- 夜间,5分钟心跳 15 else 16 return 60 -- 默认60秒 17 end 18end 19
具体实现:
1function handleSignalLoss()-- 1. 检测信号丢失 2if not exgnss.is_fix() and lastFixTime then 3local lossDuration = os.time() - lastFixTime 4 5 -- 2. 短期丢失:惯性导航 6 if lossDuration < 120 then -- 2分钟内 7 local estimated = inertialNavigation( 8 lastPosition, 9 lastSpeed, 10 lastCourse, 11 lossDuration 12 ) 13 return estimated 14 end 15 -- 3. 长期丢失:切换LBSelse 16 logF("GNSS信号丢失超过2分钟,切换基站定位") 17 return lbsLocation() 18 endendend-- 信号恢复后的轨迹修正 19 function correctAfterRecovery(estimatedPoints, actualPoints) 20 -- 使用B样条曲线平滑过渡 21 local smoothed = bsplineSmooth(estimatedPoints, actualPoints) 22 return smoothed 23end 24
8.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接头。 其中低噪声放大电路是将信号进行放大和滤波的部分。
- PCB尺寸对天线性能的影响 承载陶瓷天线的PCB形状及面积。由于GPS有触地反弹的特性,当背景是7cm×7cm无间断大地时,patch天线的效能可以发挥到极致。虽然受外观结构等因素制约,但尽量保持相当的面积且形状均匀。另外放大电路增益的选择必须配合后端LNA增益;一般不建议有源天线增益超过29dBm,否则信号过饱和可能会导致自激。
- 内外置天线兼容和供电处理; 参考电路如下,R5和R6是为了兼容陶瓷PATCH天线和有源天线做的共PAD兼容设计;L6和C38是有源天线供电电源滤波电路。

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

8.4.2.3 GPS天线选型建议
- 在终端结构空间容许,能够统一保证GPS天线面朝上的安装使用状态;并且周边没有大的金属物件遮挡的情况下,建议使用GPS陶瓷天线,在空间容许的情况下尽量选择大尺寸的陶瓷天线。
- 在不能保证终端使用状态,且空间受限:比如手机,带定位功能的胸牌;建议使用FPC天线
- 在明确终端安装环境恶劣,并且对GPS性能有较高要求的;建议使用GPS有源天线
- 在不能保证产品安装使用状态,但是空间不受限制,也可以选择类似于GSM的外置棒状天线。
8.4.2.4 对天线厂家的要求
- VSWR(电压驻波比):GPS天线电压驻波比一般要求调到1.5左右。
- 功率:效率一般要求在40%左右
- 平均增益:平均增益要求在-0.5dB
- 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模组排除了这四点,依旧无法定位,欢迎你来找合宙,我们将会竭力为您排查您所遇到的问题
结束语
项目价值总结
技术创新:
- 🎯 智能四状态机:平衡精度与功耗
- 🛣️ 自适应轨迹补偿:减少80%GPS漂移
- 🔋 三层功耗管理:续航从一两天提升到一周左右
- 🌐 多服务器架构:一次开发,多处部署
- 🛡️ 工业级可靠性:完整JT808协议+多重保障
实用价值:
- 低成本:软件开源,硬件上用户只需Air8000A核心板+电池即可实现主要功能
- 易部署:模块化设计,快速定制
- 可扩展:支持百万级设备接入
- 标准化:符合行业协议,易于集成
学习收获
通过这个项目,你可以学到:
- 嵌入式开发全流程:从硬件选型到软件部署
- 物联网架构设计:设备端+服务器端+平台端
- 算法优化实践:轨迹补偿、功耗优化、网络传输
- 工程化思维:模块化、配置化、可测试性
- 产品化思维:用户体验、成本控制、可维护性
- 天线设计建议:有源无源设计及其干扰源排查
下一步建议
初学者:
- 下载代码,编译运行
- 修改配置,体验不同模式
- 添加一个简单功能(如LED控制)
进阶者:
- 优化轨迹算法(尝试卡尔曼滤波)
- 实现Web管理平台
- 设计硬件PCB,降低成本
企业用户:
- 基于此架构开发产品
- 部署到云平台,服务客户
- 根据反馈持续优化
《LuatOS 课程-011 讲:GNSS应用开发》 是转载文章,点击查看原文。
