【逗老师的无线电】骚活,GPS热点盒子自动上报APRS位置

文章讲述了如何使用树莓派Zero2W和SIM76004G网卡构建一个具备GPS和APRS功能的热点盒子。通过AT指令控制GPS和AGPS进行定位,并使用Python的aprs库上报APRS数据帧。项目适用于HAM无线电爱好者,可实现基于GPS位置的自动上报。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
在这里插入图片描述

逗老师最近整了个有意思的小活,组装了一个有4G网卡带GPS功能的热点盒子,让盒子基于GPS位置信息,自动上报APRS位置帧
全篇亮点

  • 基于GPS和AGPS共同定位
  • 基于TCP直接上报APRS数据帧

别说,这小活整完之后,还是有点意思的,于是写篇文章分享一下。可能受限于不同的硬件本篇文章分享的代码不一定能直接跑起来,但是分享一下思路,各位HAM们也可以试试看。

一、硬件情况

在这里插入图片描述
这次使用的是树莓派Zero 2W,加上了一个SIMCOM 7600的4G网卡,这个4G网卡自身带GPS功能(其实绝大部分的网卡都带GPS功能)
热点板用的是BH3BBU老师的版本,BBU老师的板子做工真的好,手动鼓掌

二、Git项目传送门

代码部分https://github.com/ytlzq0228/RPI_APRS

三、NMEA协议读取GPS信息

1、NMEA协议简介

NMEA协议(National Marine Electronics Association,国家海洋电子协会)是一个用于船舶和其他移动平台的通信协议,广泛应用于全球定位系统(GPS)设备。它定义了不同的电子设备之间传输信息的标准格式,特别是海洋和导航领域的设备,如GPS接收器、自动驾驶仪、船用雷达等。

NMEA消息结构

NMEA消息是以特定格式发送的纯文本数据,通常称为“句子”(sentence)。每个句子包含一行文本,以$开头,以回车和换行结束。句子的内容由逗号分隔的字段构成。

常见的NMEA消息类型

  1. $GPGGA(Global Positioning System Fix Data):提供GPS定位数据,包括时间、位置和固定质量等信息。
    • 示例:$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
  2. $GPRMC(Recommended Minimum Specific GPS/Transit Data):提供最少的GPS数据,包括时间、位置、速度、航向等。
    • 示例:$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
  3. $GPGSV(Satellites in View):列出当前可见的卫星信息,包括卫星编号、仰角、方位角和信号强度。
    • 示例:$GPGSV,2,1,08,01,40,083,41,02,50,128,42,03,60,173,43,04,70,218,44*74
  4. $GPGLL(Geographic Position – Latitude/Longitude):提供当前位置的经纬度和UTC时间。
    • 示例:$GPGLL,4916.45,N,12311.12,W,225444,A*1D
  5. $GPVTG(Track Made Good and Ground Speed):提供地面速度和航向数据。
    • 示例:$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48

2、直接从串口读取NMEA消息

对于串口GPS设备,直接使用cat即可预览NMEA消息,大家伙平日里应该也见过。大概长这样。
在这里插入图片描述
注意,上述示例为纯GPS模块的消息。如果是支持北斗等其他定位信息的模块,其他GNSS星座系统的代码前缀如下:

  1. GPS(全球定位系统):
    • 代码前缀:GP
    • 解释:表示数据来自 GPS(美国的全球定位系统)。
    • 示例句子:$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
  2. GLONASS(全球导航卫星系统,俄罗斯):
    • 代码前缀:GL
    • 解释:表示数据来自 GLONASS。
    • 示例句子:$GLGSV,2,1,08,66,15,032,42,67,27,092,43,68,48,180,44,69,68,270,45*7A
  3. Galileo(伽利略系统,欧洲):
    • 代码前缀:GA
    • 解释:表示数据来自 Galileo。
    • 示例句子:$GAGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
  4. BeiDou(北斗导航卫星系统,中国):
    • 代码前缀:BD
    • 解释:表示数据来自 北斗 系统。
    • 示例句子:$BDGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
  5. QZSS(准天顶卫星系统,日本):
    • 代码前缀:QZ
    • 解释:表示数据来自 QZSS 系统。
    • 示例句子:$QZGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
  6. NavIC(印度区域导航卫星系统,印度):
    • 代码前缀:IN
    • 解释:表示数据来自 NavIC 系统。
    • 示例句子:$INGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

当然,也有支持开启GNSS混合定位的GNSS模块,在配置为GNSS混合输出后,会直接输出GN开头的语句。表示为混合定位。
具体请参阅GNSS模块手册。

3、处理并格式化NMEA消息中的定位信息

APRS主要需要使用【经度,纬度,海拔,速度,航向】这几个信息。这几个信息分别存在于NMEA协议的GGA和RMC语句中。其中仅能从GGA中获取海拔,仅能从RMC中获取航向和速度。所以如果需要输出完整的APRS报文,需要同时读取GGA和RMC语句。
代码部分https://github.com/ytlzq0228/RPI_APRS/blob/main/GPS_NMEA.py

3.1、循环获取GGA和RMC信息

在这里插入图片描述

3.2、GGA语句处理

在这里插入图片描述

3.3、RMC语句处理

在这里插入图片描述

3.4、构建APRS报文

在这里插入图片描述

三、AT指令获取AGPS

SIM7600-H的GPS需要外接天线,然后通过ttyUSB1串口发送NMEA数据,或者通过ttyUSB2串口通过AT指令获取。
同时,关键的来了,SIM7600-H的AGPS,也就是基站辅助定位,同样可以通过AT指令获取坐标。
本文,优先通过GPS获取定位,但是如果在室内,或者没有外接GPS天线的时候,会通过AGPS基站辅助定位获得稍微不那么精准的坐标。

1、开启GPS功能

1.1、单次开启关闭GPS功能

AT+CGPS=1

该指令用于一次性开启GPS功能
同样,AT+CGPS=0用于关闭GPS功能

1.2、开机自启GPS

AT+CGPSAUTO=1

该指令用于配置模块自动启动GPS,加电后GPS功能即运行

2、AT指令获取GPS坐标

AT+CGPSINFO

该指令用于获取当前GPS的定位信息,GPS获取位置后,正常回显GPS坐标
在这里插入图片描述
如果当前GPS定位未成功,回显空数据
在这里插入图片描述

3、AT指令获取AGPS坐标

如果当前GPS信号弱,可以通过AGPS来通过基站辅助定位获取坐标
使用AGPS前,需要确保数据链路已经建立

AT+CNETSTART

通过该指令建立数据链路

AT+CLBS=1

通过该指令,获取AGPS坐标
在这里插入图片描述
正常情况下,只要插着SIM卡,能正常联网,兜底方案AGPS都能算出一个大概的坐标出来。

三、上报APRS数据

1、APRS基本上报方式

HTTP方式连接:
服务器地址:china.aprs2.net
服务器端口:14580

telnet上去之后输入
user XXXXXX pass YYYYY(换行回车符)

XXXXXX为你的呼号,YYYYY为你呼号的passcode
passcode的生成方式google一下,就有好多在线工具可以帮忙生成。
下面的网站就是一个可以生成passcode的站点

https://apps.magicbug.co.uk/passcode/index.php
在这里插入图片描述
在这里插入图片描述
哎,这玩意就是这样,明文生成密码,还没有鉴权,所以,大家自觉遵守道德规范就好。

输入user和pass之后,等待几秒(我设置是等待5秒),收到验证通过的反馈后
在这里插入图片描述

之后再发送符合APRS的数据帧字符串即可。
例如:

BI1FQO-13>APDG03,TCPIP*,qAC,BI1FQO-CS:!4008.22ND11632.89E&/A=000000440 HelloWorld!

在这里插入图片描述
之后再去APRS网站上查一下,诶嘿,这不就出来啦
在这里插入图片描述

2、脚本上报

Python中有一个包,名字就叫aprs,导入此包之后方便了很多,无需构建HTTP Request报文,只需调用时候传递拼好的字符串即可。

pip install aprs

然后,直接附上脚本,各位HAM们自己研究一下,简单的很

#!/usr/bin/python

import serial
import time
ser = serial.Serial("/dev/ttyUSB2",115200)
import aprs

rec_buff = ''

def send_at(command,back,timeout):
	rec_buff = ''
	ser.write((command+'\r\n').encode())
	time.sleep(timeout)
	if ser.inWaiting():
		time.sleep(0.01 )
		rec_buff = ser.read(ser.inWaiting())
	if back!='' and back not in rec_buff.decode():
		print(command + ' ERROR')
		print(command + ' back:\t' + rec_buff.decode())
		return 0,0
	else:
		run_resule=rec_buff.decode()
		#print(run_resule)
		return (1,run_resule)

def get_gps_position():
	rec_null = True
	answer = 0
	print('Start GPS session...')
	rec_buff = ''
	send_at('AT+CNETSTART','',1)
	time.sleep(1)
	GPS_Info=send_at('AT+CGPSINFO','+CGPSINFO: ',1)[1]
	AGPS_Info=send_at('AT+CLBS=1','OK',5)[1]
	#print(GPS_Info)
	#print(AGPS_Info)
	if ',,,,,,' in GPS_Info:
		print('GPS is not ready')
		#print(AGPS_Info.split())
		if len(AGPS_Info.split())>3:
			lat=float(AGPS_Info.split()[3].split(',')[1])*100
			lng=float(AGPS_Info.split()[3].split(',')[2])*100
		else:
			lat,lng=0,0
	else:
		lat=float(GPS_Info.split()[2].split(',')[0])*1
		lng=float(GPS_Info.split()[2].split(',')[2])*1
	#print(lat,lng)
	lat=round(lat,2)
	lng=round(lng,2)
	return lat,lng

if __name__ == '__main__':
	while True:
		try:
			lat,lng=get_gps_position()
			if lat==0 and lng==0:
				raise
			#print(lat,lng)
			frame_text=('BI1FQO-P>APDG03,TCPIP*,qAC,BI1FQO-RS:!%sND%sE&/A=000000 Auto Report by RPI with GPS module.逗老师的带GPS的盒子自动上报'%(lat,lng)).encode()
			a = aprs.TCP(b'BI1FQO', b'12345')#12345替换成你的passcode
			a.start()
		
			a.send(frame_text)
		except Exception as err:
			print(err)
		time.sleep(300)

然后,写个sheel脚本,开机自动运行,就OKK啦
这个小项目基本就这样了,对于开发者来说,这个项目非常简单。但是对于HAM们来说,如果理解起来费劲的话,也可以私信联系我帮忙处理。
这里是BI1FQO,DMR ID:4606666,希望各位HAM通联愉快!

<think>好的,用户现在想获取在Android平台上实现仿淘宝购物车功能的解决方案或示例代码,特别是UI部分的实现。我需要结合之前的对话和提供的引用内容来给出合适的回答。 首先,回顾用户之前的提问和我的回答,淘宝购物车页面涉及的技术架构包括前端动态渲染、后端微服务、数据存储以及高性能保障措施。用户现在聚焦于Android端的实现,尤其是UI部分,可能需要详细的步骤和代码示例。 接下来,查看用户提供的三个引用。引用[1]提到在uni-app中实现购物车功能,包括单选/全选、修改数量、计算总金额、店铺分类等,这提示我需要考虑Android端的类似功能模块。引用[2]讨论了移动端滑动删除的实现,这部分在淘宝购物车中常见,需要包含在解决方案中。引用[3]则涉及Android购物车的初始化、数据绑定和空布局处理,这些是基础但重要的点。 用户需要仿淘宝的UI,所以需要分析淘宝购物车的界面元素:店铺分组、商品列表、单选/全选、滑动删除、数量加减、总价计算等。同时,可能还需要考虑数据加载方式,如懒加载和分页,以处理大量数据。 我需要将这些元素拆解为具体的Android组件和实现步骤。例如,使用RecyclerView嵌套展示店铺和商品,自定义Adapter处理多级列表,ItemTouchHelper实现滑动删除,数据绑定库或手动更新UI来同步选中状态和总价计算。此外,本地数据存储和网络请求的整合也是需要考虑的。 还需要注意性能优化,比如RecyclerView的视图复用,避免内存泄漏,以及如何处理异步数据加载和UI更新。引用[1]中的懒加载和分页加载更多数据可以应用到Android的RecyclerView滚动监听中,实现动态加载。 最后,用户可能需要示例代码,所以需要提供关键的代码片段,如Adapter的实现、ViewHolder的结构、滑动删除的处理逻辑,以及总价计算的方法。同时,要确保代码符合Android最佳实践,例如使用ViewBinding或DataBinding来简化视图操作,避免在Adapter中处理业务逻辑,而是通过接口回调与Activity/Fragment通信。 可能还需要考虑不同Android版本的兼容性,以及如何测试这些功能,比如使用Espresso进行UI测试,确保交互的稳定性。此外,引用[3]提到的空布局和下拉刷新功能也需要整合到示例中,提升用户体验。 总结下来,回答需要结构清晰,分步骤讲解UI构建、功能实现和数据处理,并提供可运行的代码示例,同时引用之前的架构设计中的要点,如数据一致性和高性能保障,确保解决方案的全面性。</think>根据淘宝购物车功能特点及Android实现要点,以下为分模块实现方案及核心代码示例: ### 一、UI架构设计(仿淘宝布局) ```xml <!-- 购物车容器 --> <CoordinatorLayout> <RecyclerView app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <LinearLayout android:id="@+id/bottom_bar"> <CheckBox android:id="@+id/cb_all_select"/> <TextView android:id="@+id/tv_total_price"/> <Button android:text="结算"/> </LinearLayout> </CoordinatorLayout> ``` ### 二、核心功能实现 #### 1. 多级嵌套列表(店铺+商品) ```kotlin // 数据结构 data class CartEntity( val shopId: Long, var isShopSelected: Boolean = false, val goodsList: MutableList<CartItem> = mutableListOf() ) data class CartItem( val itemId: String, var count: Int = 1, var isSelected: Boolean = false ) // 多类型Adapter实现 class CartAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { const val TYPE_SHOP = 0 const val TYPE_ITEM = 1 } override fun getItemViewType(position: Int): Int { return when (dataList[position]) { is CartEntity -> TYPE_SHOP is CartItem -> TYPE_ITEM else -> throw IllegalArgumentException() } } } ``` #### 2. 滑动删除实现(ItemTouchHelper) ```kotlin class SwipeDeleteCallback : ItemTouchHelper.SimpleCallback( 0, ItemTouchHelper.LEFT) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val position = viewHolder.adapterPosition // 执行删除操作 (mAdapter as CartAdapter).removeItem(position) } override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { // 绘制删除背景和图标 val itemView = viewHolder.itemView val deleteIcon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_delete) val deleteMargin = 16.dpToPx() deleteIcon?.let { val iconTop = itemView.top + (itemView.height - it.intrinsicHeight) / 2 val iconLeft = itemView.right - deleteMargin - it.intrinsicWidth it.setBounds(iconLeft, iconTop, iconLeft + it.intrinsicWidth, iconTop + it.intrinsicHeight) it.draw(c) } } } ``` ### 三、业务逻辑处理 #### 1. 总价计算(响应式更新) ```kotlin private fun calculateTotalPrice() { var total = 0.0 cartData.forEach { shop -> shop.goodsList.filter { it.isSelected }.forEach { item -> total += item.price * item.count } } binding.tvTotalPrice.text = "¥${String.format("%.2f", total)}" } ``` #### 2. 联动选择逻辑 ```kotlin // 店铺选择状态变化 fun onShopSelectChanged(shopPosition: Int, isChecked: Boolean) { val shop = dataList[shopPosition] as CartEntity shop.isShopSelected = isChecked shop.goodsList.forEach { it.isSelected = isChecked } checkAllSelectState() } // 全选按钮点击 binding.cbAllSelect.setOnCheckedChangeListener { _, isChecked -> cartData.forEach { shop -> shop.isShopSelected = isChecked shop.goodsList.forEach { it.isSelected = isChecked } } adapter.notifyDataSetChanged() calculateTotalPrice() } ``` ### 四、性能优化要点 1. **视图复用**:使用`RecyclerView.RecycledViewPool`共享视图池 2. **差异更新**:实现`ListAdapter`配合`DiffUtil`进行增量更新 3. **内存优化**:商品图片加载使用Glide/Picasso的RecyclerView预加载策略[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逗老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值