随着物联网(IoT)的快速发展,LoRa技术凭借其独特的扩频调制机制和超远传输距离,在智能城市、农业监测、工业自动化等领域取得了显著的应用成果。

LoRa全称(Long Range Radio)是基于Semtech公司开发的一种低功耗局域网无线标准,解决了在同样的功耗条件下比其他无线方式传播的距离更远的技术难题,实现了低功耗和远距离两种兼顾的效果。
LoRa技术特点:长距离通信、功耗(电池寿命长)、广域覆盖、抗干扰能力、低成本、开放标准。
项目简介
项目基于树莓派RP2350开发板与LoRa通信技术,构建分布式气象数据系统,实现了多节点数据分布式采集与无线传输。

"Beetle 树莓派RP2350:LoRa气象钟"由LoRa网关、LoRa节点组成。
1、LoRa网关
基于Python构建,接收来自LoRa节点的数据,并将数据发送至物联网云平台,定时向LoRa节点广播时钟、气象等数据更新。
2、LoRa数据节点
由Beetle RP2350、LoRa模块、OLED组成,实时采集现场节点数据,发送至LoRa网关,接收来自LoRa网关广播的时钟、气象等数据,并实时更显在OLED上。
硬件介绍
1、Beetle RP2350 开发板
Beetle RP2350 是一款基于RP2350芯片设计的高性能迷你体积的开发板,有2路ADC,分别是A0对应IO26,A1对应IO27。
2、0.96 OLED屏

0.96寸OLED显示模块采用SSD1306驱动芯片,有128x64个自发光的白色像素点,采用I2C通信。
3、LoRa模块

LORA模块,最大通信距离为3KM(S1速率下),拥有50个信道,8种速率,UART通信。
连接电路

程序编写
1、 安装Beetle RP2350 开发板
步骤 1:添加开发板管理器网址
打开 Arduino IDE 后,点击菜单栏中的 文件 -> 首选项。
在弹出的 首选项 窗口中,找到 附加开发板管理器网址 输入框。
输入 Beetle RP2350 开发板的支持包链接。通常可以在开发板的官方文档或者社区中找到对应的链接。对于 RP2350 开发板,一般使用的链接是 https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json。
点击 确定 保存设置。

步骤 2:安装 Beetle RP2350 开发板支持
点击菜单栏中的 工具 -> 开发板 -> 开发板管理器,在开发板管理器窗口中,等待索引更新完成。
在搜索框中输入 RP2350。
在搜索结果中找到 Raspberry Pi Pico/RP2040/RP2350 并点击 安装 按钮。

等待安装过程完成,这可能需要一些时间,具体取决于你的网络速度。
步骤 3:选择 Generic RP2350 开发板
安装完成后,点击菜单栏中的 工具 -> 开发板,在开发板列表中选择 Generic RP2350。

步骤 4:选择端口
将 Beetle RP2350 开发板通过 USB 线连接到计算机。点击菜单栏中的 工具 -> 端口,选择与开发板对应的端口。
完成以上步骤后,你就可以在 Arduino IDE 中使用 Beetle RP2350 开发板进行编程和开发了。你可以编写代码并上传到开发板上运行。
2、安装U8g2库
在库管理器的搜索框中输入 U8g2,然后在搜索结果中找到对应的库。点击 安装 按钮,等待安装完成。U8g2 是一个用于单色 OLED/LCD 显示屏的图形库,支持超过 50 种不同类型的显示屏(如 SSD1306、SH1106 等),提供了简洁的 API 来绘制文本、图形和图像。

3、安装ArduinoJson库
在库管理器的搜索框中输入ArduinoJson,然后在搜索结果中找到对应的库。点击 安装 按钮,等待安装完成。ArduinoJson 是一个用于解析和生成 JSON(JavaScript Object Notation)数据的库,简化了 Arduino 与 JSON 格式数据的交互。

主要程序代码及说明
1、LoRa数据节点
#include <Wire.h>
#include <U8g2lib.h>
#include <ArduinoJson.h>
#define LORA_RX_PIN 9
#define LORA_TX_PIN 8
#define LED_PIN 25
#define OLED_SDA 4
#define OLED_SCL 5
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R0,
U8X8_PIN_NONE,
OLED_SCL,
OLED_SDA
);
// 初始数据
String date = "2025-05-27";
String displayTime = "08:15:00";
String temperature = "25.1";
String weather = "yin";
String weekday;
unsigned long lastUpdate = 0;
const int updateInterval = 1000;
// 天气映射表
const char* weatherJson = R"(
{
"xue": "Snow",
"lei": "Thunder",
"shachen": "Sandstorm",
"wu": "Fog",
"bingbao": "Hail",
"yun": "Cloudy",
"yu": "Rain",
"yin": "Overcast",
"qing": "Sunny"
})";
// 星期计算函数
void calculateWeekday(String dateStr) {
int y = dateStr.substring(0, 4).toInt();
int m = dateStr.substring(5, 7).toInt();
int d = dateStr.substring(8, 10).toInt();
if (m < 3) {
m += 12;
y--;
}
int J = y / 100;
int K = y % 100;
int h = (d + 13*(m+1)/5 + K + K/4 + J/4 + 5*J) % 7;
h = (h + 7) % 7;
const char* weekdays[] = {"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"};
weekday = weekdays[h];
}
// 天气解析函数
const char* getFullWeather(const char* chineseCode) {
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, weatherJson);
if (error) return "Error";
if (doc.containsKey(chineseCode)) {
return doc[chineseCode];
}
return "Unknown";
}
// 输入验证函数
bool isValidTime(String t) {
if (t.length() != 8) return false;
if (t[2] != ':' || t[5] != ':') return false;
for (int i=0; i<8; i++) {
if (i == 2 || i == 5) continue;
if (!isdigit(t[i])) return false;
}
int h = t.substring(0,2).toInt();
int m = t.substring(3,5).toInt();
int s = t.substring(6,8).toInt();
return (h >=0 && h <24 && m >=0 && m <60 && s >=0 && s <60);
}
bool isValidDate(String d) {
if (d.length() != 10) return false;
if (d[4] != '-' || d[7] != '-') return false;
int y = d.substring(0,4).toInt();
int m = d.substring(5,7).toInt();
int day = d.substring(8,10).toInt();
if (y < 2000 || y > 9999) return false;
if (m <1 || m >12) return false;
int daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31};
if (m ==2 && (y%4 ==0 && (y%100 !=0 || y%400 ==0))) daysInMonth[1] =29;
return (day >0 && day <= daysInMonth[m-1]);
}
void setup() {
Serial.begin(9600);
Serial2.begin(9600);
pinMode(LED_PIN, OUTPUT);
u8g2.begin();
u8g2.enableUTF8Print(); // 启用 UTF-8 支持
calculateWeekday(date);
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_unifont_t_chinese2); // 选择支持中文的字体
u8g2.setCursor(20, 20);
u8g2.print("准备就绪!"); // 直接打印 UTF-8 编码的中文字符
u8g2.sendBuffer();
delay(1000);
}
void processCommand(String cmd) {
cmd.trim();
if (cmd.startsWith("!D")) {
String newDate = cmd.substring(2);
if (isValidDate(newDate)) {
date = newDate;
calculateWeekday(date);
} else {
Serial.println("Invalid Date Format");
}
}
else if (cmd.startsWith("!T")) {
String newTime = cmd.substring(2);
if (isValidTime(newTime)) {
displayTime = newTime;
} else {
Serial.println("Invalid Time Format");
}
}
else if (cmd.startsWith("!C")) {
temperature = cmd.substring(2);
}
else if (cmd.startsWith("!W")) {
weather = cmd.substring(2);
}
else if (cmd == "!help") {
printHelp();
}
else {
Serial.println("Unknown Command");
}
updateDisplay();
}
void updateDisplay() {
u8g2.firstPage();
do {
// 日期和星期显示
u8g2.setFont(u8g2_font_7x13B_tf);
String dateStr = date + " " + weekday;
int dateWidth = u8g2.getUTF8Width(dateStr.c_str());
u8g2.setCursor((128 - dateWidth)/2, 15);
u8g2.print(dateStr);
// 时间显示
u8g2.setFont(u8g2_font_logisoso24_tf);
int timeWidth = u8g2.getUTF8Width(displayTime.c_str());
u8g2.setCursor((128 - timeWidth)/2, 45);
u8g2.print(displayTime);
// 温度显示
u8g2.setFont(u8g2_font_7x13B_tf);
String tempStr = temperature + "C";
u8g2.setCursor(10, 62);
u8g2.print(tempStr);
// 天气显示
const char* weatherStr = getFullWeather(weather.c_str());
int weatherWidth = u8g2.getUTF8Width(weatherStr);
u8g2.setCursor(128 - weatherWidth - 10, 62);
u8g2.print(weatherStr);
} while (u8g2.nextPage());
}
void loop() {
if (Serial2.available()) {
String cmd = Serial2.readStringUntil('\n');
processCommand(cmd);
}
if (millis() - lastUpdate >= updateInterval) {
updateTime();
updateDisplay();
lastUpdate = millis();
}
}
void updateTime() {
int h = displayTime.substring(0,2).toInt();
int m = displayTime.substring(3,5).toInt();
int s = displayTime.substring(6,8).toInt();
s++;
if (s >= 60) { s = 0; m++; }
if (m >= 60) { m = 0; h++; }
if (h >= 24) {
h = 0;
incrementDate();
calculateWeekday(date);
}
auto formatNum = [](int n){ return n < 10 ? "0"+String(n) : String(n); };
displayTime = formatNum(h) + ":" + formatNum(m) + ":" + formatNum(s);
}
void incrementDate() {
int y = date.substring(0,4).toInt();
int m = date.substring(5,7).toInt();
int d = date.substring(8,10).toInt() + 1;
int daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31};
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {
daysInMonth[1] = 29;
}
if (d > daysInMonth[m-1]) {
d = 1;
if (++m > 12) {
m = 1;
y++;
}
}
auto formatNum = [](int n){ return n < 10 ? "0"+String(n) : String(n); };
date = String(y) + "-" + formatNum(m) + "-" + formatNum(d);
}
void printHelp() {
Serial.println("Available Commands:");
Serial.println("!DYYYY-MM-DD Set date");
Serial.println("!THH:MM:SS Set time");
Serial.println("!CXX.X Set temperature");
Serial.println("!Wxxxx Set weather");
Serial.println("!help Show this help");
}
2、LoRa网关
import serial
import datetime
import time
import sys
import requests
# 可配置参数
SERIAL_PORT = 'COM5' # 串口号
BAUDRATE = 9600 # 波特率
UPDATE_INTERVAL = 60 # 默认5分钟(单位:秒)
DATA_PREFIX = '!' # 数据帧起始标志
def get_timestamp():
"""获取带时间戳的日期时间"""
now = datetime.datetime.now()
return (
now.strftime("%Y-%m-%d"),
now.strftime("%H:%M:%S"),
now.strftime("[%Y-%m-%d %H:%M:%S]") # 日志时间戳
)
def send_to_serial(data, max_retries=3):
"""增强型串口发送(已移除校验和)"""
for attempt in range(max_retries):
try:
with serial.Serial(SERIAL_PORT, BAUDRATE, timeout=1) as ser:
time.sleep(1.5)
# 构造纯净数据帧(无校验和)
frame = f"{DATA_PREFIX}{data}\r\n" # 直接拼接数据
ser.write(frame.encode('utf-8'))
print(f"{get_timestamp()[2]} 发送: {frame.strip()}")
# 监听响应(可选)
if ser.in_waiting > 0:
response = ser.read_all().decode().strip()
print(f"收到响应: {response}")
return True
except serial.SerialException as e:
print(f"尝试 {attempt+1}/{max_retries} 失败: {str(e)}")
time.sleep(2)
except Exception as e:
print(f"意外错误: {str(e)}")
return False
return False
def fetch_weather_data():
url = "http://v1.yiketianqi.com/free/day?appid=appid&appsecret=appsecret&unescape=1"
try:
# 发送GET请求
response = requests.get(url)
response.raise_for_status() # 检查HTTP错误
# 解析JSON数据
data = response.json()
# 提取并返回数据
return {
"nums": data.get("nums"),
"cityid": data.get("cityid"),
"city": data.get("city"),
"date": data.get("date"),
"week": data.get("week"),
"update_time": data.get("update_time"),
"weather": data.get("wea"),
"weather_img": data.get("wea_img"),
"temperature": data.get("tem"),
"day_temperature": data.get("tem_day"),
"night_temperature": data.get("tem_night"),
"wind": data.get("win"),
"wind_speed": data.get("win_speed"),
"wind_meter": data.get("win_meter"),
"air_quality": data.get("air"),
"pressure": data.get("pressure"),
"humidity": data.get("humidity")
}
except requests.exceptions.RequestException as e:
print(f"请求错误: {e}")
except ValueError as e:
print(f"JSON解析错误: {e}")
except Exception as e:
print(f"发生未知错误: {e}")
return None
def main_loop():
"""主循环逻辑"""
print(f"\n=== LoRa数据发送器已启动 ===")
print(f"串口: {SERIAL_PORT}, 波特率: {BAUDRATE}")
print(f"更新间隔: {UPDATE_INTERVAL//60} 分钟")
print("按下 Ctrl+C 停止运行...\n")
try:
while True:
date, time_str, timestamp = get_timestamp()
# 构造数据包(已删除校验和字段)
date_packet = f"D{date}" # 格式示例: D2025-05-27
time_packet = f"T{time_str}"# 格式示例: T11:16:15
weather_data = fetch_weather_data()
if weather_data:
weather_packet = f"W{weather_data['weather_img']}"
temperature_packet = f"C{weather_data['temperature']}"
# 发送数据
success = send_to_serial(date_packet) and send_to_serial(time_packet) and send_to_serial(weather_packet) and send_to_serial(temperature_packet)
# 失败处理
if not success:
print(f"{timestamp} 严重错误: 无法恢复连接,等待人工干预")
break
time.sleep(UPDATE_INTERVAL)
except KeyboardInterrupt:
print("\n=== 用户手动终止 ===")
if __name__ == "__main__":
# 通过命令行参数修改间隔(示例:python script.py 60)
if len(sys.argv) > 1:
try:
UPDATE_INTERVAL = int(sys.argv[1])
except ValueError:
print("警告: 无效的时间参数,使用默认值")
main_loop()
蠍蠍蠍蠍蠍2025.06.05
大佬,这种电路图怎么画
豆爸2025.06.07
简单点,word、ppt都可以;或者画图、PS也都可以
匿名
该评论已删除
蠍蠍蠍蠍蠍2025.06.11
感谢大佬