在物联网项目开发中,智能定位系统是一类常见且实用的应用场景,本文将基于 LuatOS,分享一款智能定位系统的开发思路与相关实现要点。
在实际开发过程中,类似学生卡定位器的需求十分普遍,这类需求通常对定位精度、设备续航能力、轨迹显示效果以及多平台适配性均有明确要求,而基于 LuatOS 的智能定位系统,可针对性解决这类开发需求中的核心痛点。
项目特点:
- 🛰️ 三合一定位:GNSS + 基站 + WiFi
- 🔋 续航:智能功耗管理,运动才定位
- 🛣️ 轨迹优化:减少80%GPS静态漂移以及运动漂移,路线更符合真实路径
- 🌐 多平台兼容:一次开发,同时上报多个服务器
- 🎯 工业标准:完整JT808协议实现
- 🚩特点:exgnss扩展库和exvib扩展库
适合场景:
- 👨🎓 学生安全卡、老人定位器
- 🐕 宠物追踪、牲畜管理
- 🚗 车辆监控、物流追踪
- 📦 资产定位、贵重物品跟踪
前言
当前项目为演示项目,使用到的web平台为合宙自建的内部服务器,暂时不对外开放,此项目仅仅是为了给大家演示一下exgnss扩展库和exvib库如何融入到定位器相关项目内,也会重点讲解gnss应用场景有关的代码和业务逻辑,至于后期如何开放该平台等事宜,等待内部讨论后给出。
项目功能:
- 经纬度数据、电量、卫星信号质量、速度、电池状态(充电中/未充电)等数据上报到合宙定位测试服务器
- 根据服务器下发的指令 进入/退出快速定位模式
- 功耗优化,区分运动和静止状态
项目需求:
- 由Air8000A内部加速度传感器判断是否开启/关闭GNSS
- 按照JT808协议和服务器进行通讯
- 可以上传多个服务器(为后期做拓展)
- 尽可能的减少静态/动态漂移带来的轨迹失真
- 与服务器直接保持长连接状态,并且尽可能的做到功耗最低
使用到的最为重要的两个扩展库介绍:
exgnss:
exgnss库实际上是把底层提供的libgnss接口封装了一层,让配置过程更快速一点,更方便用户快速上手,exgnss库最重要的三个接口为:
lua
exgnss.setup(gnssotps) --配置GNGSS输出相关
exgnss.open(mode,para)--硬件层面打开GNSS
exgnss.close(mode,para)--硬件层面关闭GNSS
至于这三个接口的具体使用方法,以及传参,这里有接口/参数说明以及demo示例,后面项目中也用到了,这里带大家简单过一次。就不做赘述了
常量定义:
exgnss.DEFAULT
lua
常量含义: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定位成功与否]
lua
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库的三种模式主要用于以下场景:
- 微小震动检测:用于检测轻微震动的场景,例如用手敲击桌面,加速度量程2g;
- 运动检测:用于电动车或汽车行驶时的检测和人行走和跑步时的检测,加速度量程4g
- 跌倒检测:用于人或物体瞬间跌倒时的检测,加速度量程8g
咱们这个项目 使用的就是运动检测,exvib库就三个接口,分别为:
lua
exvib.read_xyz()--读三轴(G-Sensor)传过来的XYZ轴数据
exvib.open(mode)--打开三轴(G-Sensor)
exvib.close()--关闭三轴(G-Sensor)
exvib库完整的说明可以看这里,后面项目具体代码中也有,这三个接口不难理解,唯一注意的一点是exvib.open这个接口
exvib.open(mode)
lua
参数含义:加速度传感器的应用模式;
数据类型:number;
取值范围:
1 - 微小震动检测,用于检测轻微震动的场景,例如用手敲击桌面,加速度量程2g;
2 - 运动检测,用于电动车或汽车行驶时的检测和人行走和跑步时的检测,加速度量程4g;
3 - 跌倒检测,用于人或物体瞬间跌倒时的检测,加速度量程8g;
是否必选:是;
第一部分:项目整体架构
1.1 模块化架构设计

架构特点:
- 分层设计:
-
定位核心层:commom(管理了exgnss和trackCompensate)
-
传感器层:vibration(三轴)
-
电源管理层:charge、manage
-
通讯协议层:api、jt808
-
服务器层:srvs、auxServer
-
具体后面讲代码的时候会一个个lua文件打开,仔细说明,这里就不做过多赘述了
- 事件驱动:
lua
-- 发布订阅模式,完全解耦
sys.publish("SYS_STATUS_RUN") -- 运动事件
sys.subscribe("GNSS_STATE", fn) -- 定位状态变化
sys.waitUntil("IP_READY", 10000) -- 等待网络就绪
- 配置驱动:
lua
-- cfg.lua 统一管理所有配置
_G.GNSS_LOWPOWER_ENABLE = true -- 低功耗开关
_G.GNSS_LOWPOWER_INTERVAL = 60 -- 定位间隔(单位:S)
-- 轨迹补偿参数
trackCompensate.setConfig({
scene = "person", -- 人员模式
autoAdaptive = true, -- 自动适应
distanceThreshold = 50 -- 漂移过滤阈值
})
- gnss应用场景****管理:
lua
-- 避免资源冲突,智能管理GNSS
exgnss.open(exgnss.DEFAULT, {tag = "common_app"})
exgnss.open(exgnss.TIMERORSUC, {tag = "lowpower"})
-- 两个tag都关闭时,GNSS才真正关闭
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模式 - 定位捕获
lua
-- 代码位置: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模式 - 运动追踪
lua
-- 关键特性:
-- 1. 1Hz定位频率,实时性高
-- 2. 自动轨迹补偿,减少漂移
-- 3. 双模式上传:正常5分钟/快速3秒
-- 4. 智能休眠检测:20秒无震动
-- 上传频率控制
if fastUpload then -- 快速模式:3秒/次(紧急情况)
srvs.dataSend() -- 立即上传
else -- 正常模式:缓存+批量上传
if waitUploadTimes >= 30 then -- 约5分钟
uploadCache() -- 批量上传缓存数据
end
end
STATIC模式 - 静止优化
lua
-- STATIC_GNSS:使用最后有效位置
-- STATIC_LBS:使用基站定位(GNSS失败时)
-- 共同特点:
-- 1. 关闭GNSS,功耗降至1mA
-- 2. 定期心跳保持连接
-- 3. 运动立即唤醒,无感知切换
2.3 状态切换条件表
第三部分:轨迹补偿算法详解
3.1 GPS为什么需要补偿?
现场演示:请大家看这张图,红色框为原始GPS轨迹:

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

3.3 核心算法代码解析
算法1:距离阈值过滤
lua
-- 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°跳变)
lua
-- 关键问题: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:速度突变过滤
lua
-- 原理: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:拐点补偿
bash
-- 解决急转弯被拉直的问题
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 双场景自适应系统
lua
-- 人员模式 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 优化效果对比
测试数据:
lua
原始轨迹数据:
点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 ← 航向修正
量化提升:
第四部分:功耗优化策略
4.1 三层功耗管理架构
lua
-- 第一层:硬件级控制
-- 引用计数管理,避免资源冲突
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 震动检测优化算法
lua
-- vibration.lua 核心逻辑
local function vibrationOptimization()
-- 静止→运动:快速响应(5秒内2次震动)
-- 避免误判:防止偶尔震动误触发
-- 运动→静止:延迟确认(20秒内0次震动)
-- 避免误判:防止行走时停顿误判为静止
-- 有效震动检测:10秒内5次震动
-- 防止误触:过滤无效震动(如放口袋里的晃动)
-- 冷却机制:有效震动后30分钟内不再重复触发
-- 避免频繁:如车辆持续震动场景
end
优化空间:
- 进一步降低GNSS功耗:按需定位
- 优化网络连接:心跳间隔调整
- 深度休眠:夜间完全关闭
第五部分:数据通信架构及代码完整实现
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 多服务器管理架构
lua
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 数据上传频率控制
lua
-- 双模式上传策略
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 演示准备
硬件准备:
- Air8000A开发板(已刷本项目固件)
- SIM卡(已开通数据业务)
- 锂电池(3000mAh)
- 电脑(串口调试+Web平台)
软件准备:
- LuaTools(串口调试/代码下载)
- Web监控平台(显示轨迹)
注:目前定位监控平台不对外开放,仅在直播过程中,演示使用一下

第七部分:扩展与定制
7.1 如何适配新硬件?
lua
-- 步骤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 添加新服务器
lua
-- 新建一个服务器模块: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 自定义轨迹算法
lua
-- 继承或替换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 添加新功能模块
lua
-- 示例:添加温度监控模块
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)通过两个步骤加速定位:
- 基站定位:通过附近的基站(单基站单独定位,多基站的话进行三角定位),确定大致位置(精度50-1500米)
- 星历下载:从服务器获取当前可用的卫星信息,减少搜索时间
在我们的项目中,exgnss库已经内置了完整的AGPS逻辑,包括:
- 自动获取基站信息
- 从互联网下载星历数据
- 时间同步校准
- 无需额外的客户自建服务器支持,使用合宙的AGPS服务
Q2:轨迹补偿算法会增加延迟吗?
A:几乎零延迟。原因:
- 轻量计算:所有算法都是简单的数学运算,在毫秒级完成
- 本地处理:在设备端实时处理,不依赖网络
- 历史缓存:最多保存5个点,内存占用极小
- 异步执行:在数据上报周期(5分钟)内完成,不影响实时性
实际测试中算法执行时间 < 10ms,完全可以忽略。
Q3:如何保证数据不丢失?
A:四级数据保障机制:
lua
-- 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个同时连接
- 网络带宽:每个连接需要独立的数据流
优化建议:
- 主备模式:主服务器故障时切换备用
- 分级上报:关键数据报所有服务器,普通数据只报主服务器
- 按需连接:非实时服务器可以间歇性连接
8.2 业务问题
Q5:这个项目适合学生卡场景吗?有什么特殊考虑?
A:适合!这个示例demo就是根据定制的学生卡项目进行了脱敏和算法优化而来,
学生场景:
- 作息规律:上学、放学时间固定
- 活动范围:家→学校→辅导班
- 运动模式:步行+公共交通
- 安全需求:实时追踪+电子围栏
可以额外加的优化:
bash
-- 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:四步部署法:
第一步:硬件准备
lua
1. 采购硬件清单所有部件
(本项目只用到了Air8000A核心板、一块电池、以及一张物联网卡[可以拿手机副卡零时顶替一下])
2. 烧录固件(本项目代码)
4. 插入SIM卡(开通数据业务)
第二步:服务器准备(这里就不做过多介绍了,每个客户的服务器都不一样,本示例中的服务器为合宙临时测试服务器,即使客户烧录了代码,没有账号也看不到设备,本web平台仅作展示使用,具体后期怎么开放出来,内部还在商量中)
lua
1. 准备云服务器(1核2G足够)
2. 部署JT808协议解析服务
3. 配置数据库(MySQL/PostgreSQL)
4. 部署Web管理平台
第三步:配置对接
lua
1. 在cfg.lua中配置服务器地址
2. 在平台注册设备ID
3. 测试数据收发
4. 配置报警规则和电子围栏
第四步:批量部署
lua
1. 使用量产工具批量烧录
2. 自动化测试每台设备
3. 包装和发货
4. 提供用户使用文档
8.3 进阶问题
Q7:如何进一步降低功耗?
A:五级功耗优化策略:
已实现(当前项目):
- GNSS按需开关
- 状态机智能切换
- 心跳间隔优化
可进一步优化:
lua
-- 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
具体实现:
lua
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 天线相关问题
天线相关问题又分了很多种,有如下几个大类
- 天线使用问题
- 天线设计问题
- 星系切换问题
- 干扰问题
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,降低成本
企业用户:
- 基于此架构开发产品
- 部署到云平台,服务客户
- 根据反馈持续优化