Compare commits

...

2 Commits

Author SHA1 Message Date
97a9c778e8 实现 lora 2025-10-09 20:10:23 +08:00
dfb50f5c74 readme 2025-10-09 17:39:04 +08:00
3 changed files with 171 additions and 210 deletions

214
README.md
View File

@@ -1,194 +1,42 @@
# 猪舍主控 # pig-house-controller
## 简介 ## LoRa通信协议约定
猪舍主控系统根据上位机的指令控制当前猪舍内所有设备(传感器,阀门,电机等),并汇聚当前猪舍传感器数据统一上报。本系统作为猪场智能化管理的重要组成部分,实现了猪舍环境的自动化监控与调节 本项目中的LoRa通信采用自定义的帧格式以支持精确寻址和大数据包的自动分片与重组。所有数据包均由主控节点主动发起
## 功能概述 ### 1. 物理帧结构
### 与上位机交互 每个通过LoRa UART模块发送的物理数据包都遵循以下结构
1. 根据上位机指令定期采集栏内所有传感器的数据,并统一上报
2. 根据上位机指令启动或关闭栏内设备,上位机发送的启动指令分两种:
- 常开指令:收到后启动设备,直到收到关闭指令后关闭设备
- 短暂开启指令:收到后启动设备,但需要上位机每过两秒发送一次指令,超过五秒没收到下一个开启指令或受到关闭指令将会关闭设备
3. 定期检查栏内设备状态,发现异常立即上报上位机
4. 定期向上位机发送心跳包
5. 接收上位机发送的总线上各机器的位置和类型
6. 根据上位机指令调整设备功率大小
7. 接收上位机批量控制指令并执行
8. 接收上位机发送的配置信息
### 与设备交互 | 字段 | 长度 (字节) | 值 (Hex) | 描述 |
1. 控制栏内设备启停 | :-------------------- | :------------ | :---------------- | :--------------------------------------------- |
2. 调整风机等功率可调设备的功率 | **帧头 (Header)** | 1 | `0xED` | 固定值,表示一个数据包的开始。 |
3. 定时检查栏内设备状态 | **后续长度 (Length)** | 1 | `0x00`-`0xFF` | 从`目标地址`字段到`数据块`末尾的总字节数。 |
4. 定时采集栏内数据 | **目标地址 (DestAddr)** | 2 | `0x0000`-`0xFFFF` | 接收该数据包的设备地址。 |
| **总包数 (TotalChunks)**| 1 | `0x01`-`0xFF` | 表示当前消息被分成了几个包。`0x01`代表这是唯一的包。 |
| **当前包序号 (CurrentChunk)**| 1 | `0x00`-`0xFE` | 当前是第几个数据包从0开始计数。 |
| **数据块 (ChunkData)** | N | - | 实际传输的数据片段。 |
### 数据管理 **示例:**
1. 保存总线上各机器的位置和类型
2. 临时保存上位机发送的指令
3. 保存上位机发送的配置信息
4. 汇总栏内所有传感器数据
5. 临时保存栏内设备故障信息,直到上报成功后清除
6. 根据批量指令控制对应设备工作
# 猪舍控制器 发送一个数据为 `[0x01, 0x02, 0x03]` 的单包消息到地址 `0x1234`
`ED 05 12 34 01 00 01 02 03`
- `ED`: 帧头
- `05`: 后续长度 (1+1+3 = 5)
- `12 34`: 目标地址
- `01`: 总包数 (共1包)
- `00`: 当前包序号 (第0包)
- `01 02 03`: 数据块
猪舍控制器是一个用于监控和控制猪舍环境的系统。它可以通过LoRa与上位机通信并通过RS485总线控制传感器和执行器设备。 ### 2. 数据分片 (Fragmentation)
## 协议栈和技术选型 - LoRa模块的物理层限制单次发送的数据部分**最大为240字节**。
- 根据项目约定为自定义协议头总包数、当前包序号预留2字节地址由模块处理。
- 因此,每个物理包中 **`数据块 (ChunkData)` 的最大长度为 `238` 字节**。
- `send_packet` 方法会自动处理分片逻辑。
本系统采用以下物联网标准协议栈: ### 3. 数据重组 (Reassembly)
### 物理层 - `receive_packet` 方法会缓存收到的分片。
- **LoRa**:低功耗广域网物理层技术,提供远距离无线传输能力 - 当一个设备的所有分片都接收完毕后,`receive_packet` 会将它们自动重组成一个完整的消息,并向上层返回。
- 由于通信是单向的(仅主控发送),接收端无需管理多个源地址的重组缓冲区。
### 数据链路层和网络层
- **LoRaWAN**基于LoRa物理层的广域网协议提供设备认证、加密和网络管理
### 传输层
- **CoAP**受限应用协议轻量级的RESTful协议适用于资源受限设备
### 应用层
- **LwM2M**:轻量级机器到机器协议,提供设备管理、固件更新等功能
### 数据格式
- **SenML**:传感器标记语言,标准化的传感器数据表示格式
这种协议栈选择具有以下优势:
1. **低功耗**:适合电池供电或节能要求高的场景
2. **远距离传输**LoRa技术可实现数公里覆盖
3. **标准化**:采用业界标准协议,便于系统集成和扩展
4. **安全性**LoRaWAN和CoAP均提供安全机制
5. **互操作性**:基于标准协议,便于与不同厂商设备集成
## 系统架构
```
猪舍控制器
├── 通信层 (LoRa)
├── 控制层 (核心逻辑)
├── 设备层 (传感器和执行器)
└── 存储层 (数据存储)
```
## 抽象接口设计
为了提高系统的可扩展性和可维护性,我们定义了以下抽象接口:
### 通信接口 (BaseComm)
定义了通信模块的基本操作,包括连接、断开连接、发送和接收数据等方法。
### 设备接口 (BaseDevice)
定义了设备的基本操作,包括连接、断开连接、读取数据、写入数据和获取状态等方法。
### 存储接口 (BaseStorage)
定义了存储模块的基本操作,包括保存、加载、删除数据等方法。
### 命令处理器接口 (BaseHandler)
定义了命令处理的基本操作,包括处理命令、注册和注销命令处理函数等方法。
## 设计理念
按照功能区分将传感器和执行器分别连接到不同的RS485总线上可以带来以下优势
1. **减少总线负载**:传感器通常需要频繁读取数据,而执行器可能需要较大的电流,分离可以避免相互干扰。
2. **提高响应速度**:控制命令可以直接发送到执行器总线,无需等待传感器数据采集完成。
3. **增强系统稳定性**:一条总线故障不会影响另一条总线上的设备。
4. **便于维护**:可以根据需要单独重启或维护某一总线。
## 配置说明
系统配置文件为 `config.json`,如果不存在,系统会根据默认配置创建。配置项包括:
### LoRa通信配置
- `lora.address`: 本机LoRa地址
- `lora.frequency`: 工作频率(MHz)
- `lora.bandwidth`: 带宽(kHz)
- `lora.spreading_factor`: 扩频因子
- `lora.coding_rate`: 编码率
- `lora.encryption_key`: 加密密钥
### 上位机配置
- `master.lora_address`: 上位机LoRa地址
- `master.protocol`: 与上位机通信协议
### 总线配置
- `bus.sensor.port`: 传感器总线串口
- `bus.sensor.baudrate`: 传感器总线波特率
- `bus.actuator.port`: 执行器总线串口
- `bus.actuator.baudrate`: 执行器总线波特率
### 日志配置
- `log.level`: 日志级别 (DEBUG, INFO, WARNING, ERROR)
- `log.file_path`: 日志文件路径
- `log.max_size`: 日志文件最大大小
- `log.backup_count`: 保留的日志文件数量
- `log.report_errors`: 是否上报错误信息
- `log.terminate_on_report_failure`: 错误上报失败时是否终止程序
### 系统参数
- `system.heartbeat_interval`: 心跳包发送间隔(秒)
- `system.data_collection_interval`: 数据采集间隔(秒)
- `system.command_timeout`: 命令超时时间(秒)
- `system.retry_count`: 命令重试次数
- `system.error_handling`: 错误处理策略
### 设备配置
- `devices`: 设备列表(包括传感器和执行器)
每个设备包含以下属性:
- `id`: 设备唯一标识
- `type`: 设备类型
- `address`: 设备地址
- `bus`: 所在总线(sensor/actuator)
- `location`: 设备位置(可选)
- `unit`: 单位(仅传感器需要,如温度单位、湿度单位等)
参考示例配置文件 `config.json.example` 创建您的配置文件。
## 枚举类型定义
为了提高代码的可读性和维护性,系统定义了以下枚举类型:
1. `LogLevel`: 日志等级枚举 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
2. `DeviceType`: 设备类型枚举 (包括温度、湿度等传感器类型和喂料口、阀门等执行器类型)
3. `BusType`: 总线类型枚举 (SENSOR, ACTUATOR)
4. `ErrorHandlingStrategy`: 错误处理策略枚举 (RETRY, SKIP, ALERT)
## 日志和错误处理机制
考虑到树莓派等嵌入式设备的存储空间限制,系统采用以下策略:
1. **限制日志文件大小**默认将日志文件大小限制为1MB仅保留一个备份文件
2. **错误上报机制**当发生错误时系统会尝试通过LoRa将错误信息上报给上位机
3. **上报成功处理**:错误信息上报成功后,系统会删除本地日志中的该条目
4. **上报失败处理**:如果错误信息上报失败,说明与上位机之间的通信不稳定,系统将根据配置决定是否终止程序运行
这种机制既节省了本地存储空间,又能确保关键错误信息能够及时传递给上位机。
## 开发顺序建议
当然,从对其他模块依赖最小的模块开始开发是一个明智的策略,以便逐步构建项目的基础。以下是建议的开发顺序:
1. **配置模块config.py**:首先实现配置模块,以定义应用程序所需的基本配置。这将为其他模块提供必要的设置。
2. **实用程序模块utils/**:开发实用程序函数,这些函数可以在整个项目中被重复使用。这样可以为其他模块提供基本的辅助功能。
3. **数据存储模块storage/**:实现数据存储逻辑,包括存储传感器数据、设备状态和配置信息。这一模块可以在独立于其他模块的情况下开发。
4. **设备交互模块devices/**:实现与设备交互的模块,包括传感器和执行器。这将为之后的通信和核心逻辑提供基础。
5. **通信模块comms/**:开发通信模块,以处理与上位机和设备的通信协议。此模块可能需要依赖设备交互模块。
6. **核心逻辑模块core/**:实现核心逻辑,包括处理命令、控制设备和管理传感器数据。这一模块将利用之前开发的模块。
7. **测试模块tests/**:在开发过程中,逐步添加测试用例以验证每个模块的功能。
8. **主程序main.py**:最后实现主程序,作为应用程序的入口点,将所有模块整合在一起。
这种顺序将帮助你逐步构建项目,并确保每个模块在开发过程中得到充分的测试和验证。
## 许可证
禁止未经授权使用本项目代码,否则后果自负。

View File

@@ -11,8 +11,8 @@
# --- LoRa 模块配置 --- # --- LoRa 模块配置 ---
# 假设LoRa模块使用独立的UART进行通信 # 假设LoRa模块使用独立的UART进行通信
LORA_CONFIG = { LORA_CONFIG = {
# # 平台LoRa地址 # 平台LoRa地址
# 'master_address': 0x01, // 目前的LoRa模块主从模式下 从节点发送广播包=点对点发送给主节点, 所有从节点都会忽略其他从节点发的广播 'master_address': 0x01,
# LoRa模块连接的UART总线ID (0, 1, or 2 on ESP32) # LoRa模块连接的UART总线ID (0, 1, or 2 on ESP32)
'uart_id': 2, 'uart_id': 2,
@@ -37,6 +37,9 @@ LORA_CONFIG = {
# (ED + 05(消息长度) + 0201(地址) + "Hello"(消息本体)) # (ED + 05(消息长度) + 0201(地址) + "Hello"(消息本体))
# 接收: ED 05 02 01 48 65 6c 6c 6f # 接收: ED 05 02 01 48 65 6c 6c 6f
'lora_mesh_mode': 'EC', 'lora_mesh_mode': 'EC',
# 单包最大用户数据数据长度, 模块限制240, 去掉两位自定义包头, 还剩238
'max_chunk_size': 238
} }
# --- 总线配置 --- # --- 总线配置 ---

View File

@@ -10,11 +10,14 @@ LoRa模块的具体实现 (UART Passthrough for LoRa Mesh)
from .lora_interface import ILoraManager from .lora_interface import ILoraManager
from main.logs.logger import log from main.logs.logger import log
from machine import UART
import time
class LoRaMeshUartPassthroughManager(ILoraManager): class LoRaMeshUartPassthroughManager(ILoraManager):
""" """
通过UART与LoRa Mesh模块通信的处理器实现 (透传模式)。 通过UART与LoRa Mesh模块通信的处理器实现 (ED模式)。
实现了自动分片与重组逻辑。
""" """
def __init__(self, lora_config: dict): def __init__(self, lora_config: dict):
@@ -24,41 +27,148 @@ class LoRaMeshUartPassthroughManager(ILoraManager):
Args: Args:
lora_config (dict): 来自全局配置文件的LoRa配置字典。 lora_config (dict): 来自全局配置文件的LoRa配置字典。
""" """
log("LoRaMeshUartPassthroughHandler: 初始化...") log("LoRaMeshUartPassthroughManager: 初始化...")
# --- 配置注入到实例变量 --- # --- 配置注入 ---
self.master_address = lora_config.get('master_address') self.master_address = lora_config.get('master_address')
self.uart_id = lora_config.get('uart_id') self.uart_id = lora_config.get('uart_id')
self.baudrate = lora_config.get('baudrate') self.baudrate = lora_config.get('baudrate')
self.pins = lora_config.get('pins') self.pins = lora_config.get('pins')
self.lora_mesh_mode = lora_config.get('lora_mesh_mode') self.max_chunk_size = lora_config.get('max_chunk_size')
self.lora_mesh_mode = b'\xed'
# TODO 目前这个配置没用, 完全按ED处理的
if lora_config.get('lora_mesh_mode') == 'EC':
self.lora_mesh_mode = b'\xec'
# 在这里可以添加真实的硬件初始化代码例如初始化UART # --- 硬件初始化 ---
# self.uart = UART(self.uart_id, self.baudrate, tx=self.pins['tx'], rx=self.pins['rx']) self.uart = UART(self.uart_id, self.baudrate, tx=self.pins['tx'], rx=self.pins['rx'])
log(f"LoRaMeshUartPassthroughHandler: 配置加载完成. UART ID: {self.uart_id}, Baudrate: {self.baudrate}") # --- 内部状态变量 ---
self._rx_buffer = bytearray() # UART接收缓冲区
self._reassembly_cache = {} # 分片重组缓冲区 { chunk_index: chunk_data }
self._expected_chunks = 0 # 当前会话期望的总分片数
def receive_packet(self): log(f"LoRaMeshUartPassthroughManager: 配置加载完成. UART ID: {self.uart_id}, Baudrate: {self.baudrate}")
def send_packet(self, payload: bytes) -> bool:
""" """
【实现】非阻塞地检查并接收一个数据包 【实现】发送一个数据包,自动处理分片
(当前为存根实现)
"""
# 具体的实现将在这里...
# e.g. self.uart.read()
pass
def send_packet(self, data_bytes: bytes) -> bool:
"""
【实现】发送一个数据包。
(当前为存根实现)
Args: Args:
data_bytes (bytes): 需要发送的字节数据。 payload (bytes): 需要发送的完整业务数据。
Returns: Returns:
bool: True表示发送成功False表示失败。 bool: True表示所有分片都已成功提交发送False表示失败。
""" """
# 具体的实现将在这里... max_chunk_size = self.max_chunk_size
# e.g. self.uart.write(data_bytes) if not payload:
log(f"LoRaMeshUartPassthroughHandler: 模拟发送数据 -> {data_bytes}") total_chunks = 1
return True else:
total_chunks = (len(payload) + max_chunk_size - 1) // max_chunk_size
try:
for i in range(total_chunks):
chunk_index = i
start = i * max_chunk_size
end = start + max_chunk_size
chunk_data = payload[start:end]
# --- 组装物理包 ---
header = b'\xed'
dest_addr_bytes = self.master_address.to_bytes(2, 'big')
total_chunks_bytes = total_chunks.to_bytes(1, 'big')
current_chunk_bytes = chunk_index.to_bytes(1, 'big')
# 计算后续长度(总包数和当前包序号是自定义包头, 各占一位, 标准包头算在长度内)
length_val = 2 + len(chunk_data)
length_bytes = length_val.to_bytes(1, 'big')
# 拼接成最终的数据包
packet_to_send = header + length_bytes + dest_addr_bytes + total_chunks_bytes + current_chunk_bytes + chunk_data
self.uart.write(packet_to_send)
log(f"LoRa: 发送分片 {chunk_index + 1}/{total_chunks} 到地址 {self.master_address}")
# 让出CPU, 模块将缓存区的数据发出去本身也需要时间
time.sleep_ms(10)
return True
except Exception as e:
log(f"LoRa: 发送数据包失败: {e}")
return False
def receive_packet(self) -> bytes | None:
"""
【实现】非阻塞地检查、解析并重组一个完整的数据包。
"""
# 1. 从硬件读取数据到缓冲区
if self.uart.any():
self._rx_buffer.extend(self.uart.read())
# 2. 循环尝试从缓冲区解析包
while True:
# 2.1 检查头部和长度字段是否存在
if len(self._rx_buffer) < 2:
return None # 数据不足,无法读取长度
# 2.2 检查帧头是否正确
if self._rx_buffer[0] != 0xED:
log(f"LoRa: 接收到错误帧头: {hex(self._rx_buffer[0])}正在寻找下一个ED...")
next_ed = self._rx_buffer.find(b'\xed', 1)
if next_ed == -1:
self._rx_buffer.clear()
else:
self._rx_buffer = self._rx_buffer[next_ed:]
continue
# 2.3 检查包是否完整
payload_len = self._rx_buffer[1]
total_packet_len = 1 + 1 + payload_len
if len(self._rx_buffer) < total_packet_len:
return None # "半包"情况,等待更多数据
# 3. 提取和解析一个完整的物理包
packet = self._rx_buffer[:total_packet_len]
self._rx_buffer = self._rx_buffer[total_packet_len:]
addr = int.from_bytes(packet[2:4], 'big')
total_chunks = packet[4]
current_chunk = packet[5]
chunk_data = packet[6:]
# --- 长度反向校验 ---
# 根据协议Length字段 = 2 (自定义头) + N (数据块)
expected_payload_len = 2 + len(chunk_data)
if payload_len != expected_payload_len:
log(f"LoRa: 收到损坏的数据包!声明长度 {payload_len} 与实际计算长度 {expected_payload_len} 不符。已丢弃。")
# 包已从缓冲区移除直接continue进入下一次循环尝试解析缓冲区的后续内容
continue
# --- 校验结束 ---
# 4. 重组逻辑
if total_chunks == 1:
log(f"LoRa: 收到单包消息,来自地址 {addr},长度 {len(chunk_data)}")
return chunk_data
if current_chunk == 0:
log(f"LoRa: 开始接收新的多包会话 ({total_chunks}个分片)...")
self._reassembly_cache.clear()
self._expected_chunks = total_chunks
self._reassembly_cache[current_chunk] = chunk_data
log(f"LoRa: 收到分片 {current_chunk + 1}/{self._expected_chunks},已缓存 {len(self._reassembly_cache)}")
if len(self._reassembly_cache) == self._expected_chunks:
log("LoRa: 所有分片已集齐,正在重组...")
full_payload = bytearray()
for i in range(self._expected_chunks):
if i not in self._reassembly_cache:
log(f"LoRa: 重组失败!缺少分片 {i}")
self._reassembly_cache.clear()
return None
full_payload.extend(self._reassembly_cache[i])
log(f"LoRa: 重组完成,总长度 {len(full_payload)}")
self._reassembly_cache.clear()
return bytes(full_payload)