Compare commits
2 Commits
b80a04bfc1
...
c117759ac3
| Author | SHA1 | Date | |
|---|---|---|---|
| c117759ac3 | |||
| 5519d43253 |
173
DEVELOPMENT.md
173
DEVELOPMENT.md
@@ -1,173 +0,0 @@
|
|||||||
# 开发环境搭建指南
|
|
||||||
|
|
||||||
## 系统要求
|
|
||||||
|
|
||||||
- Python 3.7+
|
|
||||||
- Raspberry Pi 或其他兼容的嵌入式Linux系统
|
|
||||||
- LoRa通信模块(如SX1278)
|
|
||||||
- RS485通信接口
|
|
||||||
|
|
||||||
## 协议栈架构
|
|
||||||
|
|
||||||
本系统采用标准物联网协议栈:
|
|
||||||
|
|
||||||
```
|
|
||||||
应用层: LwM2M
|
|
||||||
传输层: CoAP (Constrained Application Protocol)
|
|
||||||
网络层: LoRaWAN
|
|
||||||
数据链路层: LoRa
|
|
||||||
物理层: LoRa
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装步骤
|
|
||||||
|
|
||||||
### 1. 克隆项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <项目地址>
|
|
||||||
cd pig-house-controller
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 创建虚拟环境
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate # Linux/Mac
|
|
||||||
# 或
|
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. LwM2M支持的特殊处理
|
|
||||||
|
|
||||||
由于 `lwm2m-client` 包在 PyPI 上不可用,我们需要手动安装或使用替代方案:
|
|
||||||
|
|
||||||
#### 方案一:使用 wakaama-python (推荐)
|
|
||||||
```bash
|
|
||||||
# 克隆 wakaama-python 仓库
|
|
||||||
git clone https://github.com/djarek/wakaama-python.git
|
|
||||||
cd wakaama-python
|
|
||||||
# 按照项目说明进行安装
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 方案二:使用 Eclipse Wakaama
|
|
||||||
Eclipse Wakaama 是一个成熟的 LwM2M 实现,可以作为 C 扩展集成到 Python 项目中。
|
|
||||||
|
|
||||||
#### 方案三:自行实现 LwM2M 客户端
|
|
||||||
根据 LwM2M 规范自行实现必要的功能。
|
|
||||||
|
|
||||||
### 5. 配置系统
|
|
||||||
|
|
||||||
复制示例配置文件并根据实际环境进行修改:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp config.json.example config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
修改 [config.json](file:///C:/Users/divano/Desktop/work/AA-Pig/pig-house-controller/config.json) 文件中的参数,包括:
|
|
||||||
- LoRa通信参数
|
|
||||||
- 总线配置
|
|
||||||
- 设备配置
|
|
||||||
- 日志配置
|
|
||||||
|
|
||||||
### 6. 硬件连接
|
|
||||||
|
|
||||||
1. 连接LoRa模块到树莓派的SPI接口
|
|
||||||
2. 连接传感器总线RS485模块到树莓派的UART接口
|
|
||||||
3. 连接执行器总线RS485模块到树莓派的另一个UART接口
|
|
||||||
|
|
||||||
### 7. 运行系统
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 开发工具推荐
|
|
||||||
|
|
||||||
- **IDE**: PyCharm 或 VS Code
|
|
||||||
- **版本控制**: Git
|
|
||||||
- **调试工具**: Python内置pdb或IDE调试器
|
|
||||||
- **日志查看**: tail命令或专业日志查看工具
|
|
||||||
|
|
||||||
## 测试环境
|
|
||||||
|
|
||||||
建议在实际部署前进行充分测试:
|
|
||||||
|
|
||||||
1. 单元测试: 验证各模块功能
|
|
||||||
2. 集成测试: 验证模块间协作
|
|
||||||
3. 系统测试: 验证完整功能流程
|
|
||||||
4. 硬件测试: 验证实际硬件连接和通信
|
|
||||||
|
|
||||||
## 协议栈实现说明
|
|
||||||
|
|
||||||
### LoRaWAN层
|
|
||||||
使用SX1278等LoRa芯片,通过SPI接口与树莓派通信。实现基本的LoRaWAN功能:
|
|
||||||
- OTAA/ABP入网
|
|
||||||
- 数据加解密
|
|
||||||
- 数据包重传机制
|
|
||||||
|
|
||||||
### CoAP层
|
|
||||||
实现CoAP协议栈,支持:
|
|
||||||
- GET/POST/PUT/DELETE方法
|
|
||||||
- 资源发现
|
|
||||||
- 块传输
|
|
||||||
- 观察者模式
|
|
||||||
|
|
||||||
### LwM2M层
|
|
||||||
实现LwM2M客户端功能:
|
|
||||||
- 设备注册与管理
|
|
||||||
- 固件更新
|
|
||||||
- 参数配置
|
|
||||||
- 数据上报
|
|
||||||
|
|
||||||
注意:由于 `lwm2m-client` 包不可用,需要使用替代方案实现 LwM2M 功能。
|
|
||||||
|
|
||||||
### SenML数据格式
|
|
||||||
所有传感器数据使用SenML格式进行编码和传输,确保数据标准化和互操作性。
|
|
||||||
|
|
||||||
## 抽象接口开发说明
|
|
||||||
|
|
||||||
系统定义了以下抽象接口,开发者在实现具体功能时需要继承这些基类:
|
|
||||||
|
|
||||||
### 通信接口 (BaseComm)
|
|
||||||
位于 [comms/base_comm.py](file:///C:/Users/divano/Desktop/work/AA-Pig/pig-house-controller/comms/base_comm.py),定义了通信模块的基本操作:
|
|
||||||
- `connect()`: 建立通信连接
|
|
||||||
- `disconnect()`: 断开通信连接
|
|
||||||
- `send()`: 发送数据
|
|
||||||
- `receive()`: 接收数据
|
|
||||||
- `is_connected()`: 检查连接状态
|
|
||||||
|
|
||||||
### 设备接口 (BaseDevice)
|
|
||||||
位于 [devices/base_device.py](file:///C:/Users/divano/Desktop/work/AA-Pig/pig-house-controller/devices/base_device.py),定义了设备的基本操作:
|
|
||||||
- `connect()`: 连接设备
|
|
||||||
- `disconnect()`: 断开设备连接
|
|
||||||
- `read_data()`: 读取设备数据
|
|
||||||
- `write_data()`: 向设备写入数据
|
|
||||||
- `get_status()`: 获取设备状态
|
|
||||||
|
|
||||||
### 存储接口 (BaseStorage)
|
|
||||||
位于 [storage/base_storage.py](file:///C:/Users/divano/Desktop/work/AA-Pig/pig-house-controller/storage/base_storage.py),定义了存储模块的基本操作:
|
|
||||||
- `save()`: 保存数据
|
|
||||||
- `load()`: 加载数据
|
|
||||||
- `delete()`: 删除数据
|
|
||||||
- `exists()`: 检查键是否存在
|
|
||||||
- `list_keys()`: 列出所有键
|
|
||||||
|
|
||||||
### 命令处理器接口 (BaseHandler)
|
|
||||||
位于 [core/base_handler.py](file:///C:/Users/divano/Desktop/work/AA-Pig/pig-house-controller/core/base_handler.py),定义了命令处理的基本操作:
|
|
||||||
- `handle_command()`: 处理命令
|
|
||||||
- `register_command()`: 注册命令处理函数
|
|
||||||
- `unregister_command()`: 注销命令处理函数
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 确保硬件连接正确,特别是UART接口不要接反
|
|
||||||
2. 根据实际硬件调整配置文件中的串口设备路径
|
|
||||||
3. 注意LoRa频段的合法性,遵守当地无线电管理规定
|
|
||||||
4. 建议在开发阶段使用DEBUG日志级别,生产环境使用INFO或更高
|
|
||||||
5. LwM2M 功能需要特殊处理,因为标准包不可用
|
|
||||||
2
Makefile
Normal file
2
Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
swag:
|
||||||
|
python -m grpc_tools.protoc -I./proto --python_out=./proto ./proto/client.proto
|
||||||
306
client_pb.py
Normal file
306
client_pb.py
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
根据client.proto生成的解析代码
|
||||||
|
适用于ESP32 MicroPython环境
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# MethodType枚举
|
||||||
|
METHOD_TYPE_SWITCH = 0
|
||||||
|
METHOD_TYPE_COLLECT = 1
|
||||||
|
|
||||||
|
def encode_varint(value):
|
||||||
|
"""编码varint值"""
|
||||||
|
buf = bytearray()
|
||||||
|
while value >= 0x80:
|
||||||
|
buf.append((value & 0x7F) | 0x80)
|
||||||
|
value >>= 7
|
||||||
|
buf.append(value & 0x7F)
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def decode_varint(buf, pos=0):
|
||||||
|
"""解码varint值"""
|
||||||
|
result = 0
|
||||||
|
shift = 0
|
||||||
|
while pos < len(buf):
|
||||||
|
byte = buf[pos]
|
||||||
|
pos += 1
|
||||||
|
result |= (byte & 0x7F) << shift
|
||||||
|
if not (byte & 0x80):
|
||||||
|
break
|
||||||
|
shift += 7
|
||||||
|
return result, pos
|
||||||
|
|
||||||
|
def encode_string(value):
|
||||||
|
"""编码字符串"""
|
||||||
|
value_bytes = value.encode('utf-8')
|
||||||
|
length = encode_varint(len(value_bytes))
|
||||||
|
return length + value_bytes
|
||||||
|
|
||||||
|
def decode_string(buf, pos=0):
|
||||||
|
"""解码字符串"""
|
||||||
|
length, pos = decode_varint(buf, pos)
|
||||||
|
value = buf[pos:pos+length].decode('utf-8')
|
||||||
|
pos += length
|
||||||
|
return value, pos
|
||||||
|
|
||||||
|
def encode_instruction(method, data):
|
||||||
|
"""
|
||||||
|
编码Instruction消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: 方法类型 (int)
|
||||||
|
data: 数据 (bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytearray: 编码后的数据
|
||||||
|
"""
|
||||||
|
result = bytearray()
|
||||||
|
|
||||||
|
# 编码method字段 (field_number=1, wire_type=0)
|
||||||
|
result.extend(encode_varint((1 << 3) | 0)) # tag
|
||||||
|
result.extend(encode_varint(method)) # value
|
||||||
|
|
||||||
|
# 编码data字段 (field_number=2, wire_type=2)
|
||||||
|
result.extend(encode_varint((2 << 3) | 2)) # tag
|
||||||
|
result.extend(encode_varint(len(data))) # length
|
||||||
|
result.extend(data) # value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def decode_instruction(buf):
|
||||||
|
"""
|
||||||
|
解码Instruction消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buf: 编码后的数据 (bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 解码后的消息
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
while pos < len(buf):
|
||||||
|
# 读取标签
|
||||||
|
tag, pos = decode_varint(buf, pos)
|
||||||
|
field_number = tag >> 3
|
||||||
|
wire_type = tag & 0x07
|
||||||
|
|
||||||
|
if field_number == 1: # method字段
|
||||||
|
if wire_type == 0: # varint类型
|
||||||
|
value, pos = decode_varint(buf, pos)
|
||||||
|
result['method'] = value
|
||||||
|
elif field_number == 2: # data字段
|
||||||
|
if wire_type == 2: # 长度分隔类型
|
||||||
|
length, pos = decode_varint(buf, pos)
|
||||||
|
value = buf[pos:pos+length]
|
||||||
|
pos += length
|
||||||
|
result['data'] = value
|
||||||
|
else:
|
||||||
|
# 跳过未知字段
|
||||||
|
if wire_type == 0: # varint
|
||||||
|
_, pos = decode_varint(buf, pos)
|
||||||
|
elif wire_type == 2: # 长度分隔
|
||||||
|
length, pos = decode_varint(buf, pos)
|
||||||
|
pos += length
|
||||||
|
else:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def encode_switch(device_action, bus_number, bus_address, relay_channel):
|
||||||
|
"""
|
||||||
|
编码Switch消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_action: 设备动作指令 (str)
|
||||||
|
bus_number: 总线号 (int)
|
||||||
|
bus_address: 总线地址 (int)
|
||||||
|
relay_channel: 继电器通道号 (int)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytearray: 编码后的数据
|
||||||
|
"""
|
||||||
|
result = bytearray()
|
||||||
|
|
||||||
|
# 编码device_action字段 (field_number=1, wire_type=2)
|
||||||
|
result.extend(encode_varint((1 << 3) | 2)) # tag
|
||||||
|
action_bytes = encode_string(device_action) # value (length + string)
|
||||||
|
result.extend(action_bytes)
|
||||||
|
|
||||||
|
# 编码bus_number字段 (field_number=2, wire_type=0)
|
||||||
|
result.extend(encode_varint((2 << 3) | 0)) # tag
|
||||||
|
result.extend(encode_varint(bus_number)) # value
|
||||||
|
|
||||||
|
# 编码bus_address字段 (field_number=3, wire_type=0)
|
||||||
|
result.extend(encode_varint((3 << 3) | 0)) # tag
|
||||||
|
result.extend(encode_varint(bus_address)) # value
|
||||||
|
|
||||||
|
# 编码relay_channel字段 (field_number=4, wire_type=0)
|
||||||
|
result.extend(encode_varint((4 << 3) | 0)) # tag
|
||||||
|
result.extend(encode_varint(relay_channel)) # value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def decode_switch(buf):
|
||||||
|
"""
|
||||||
|
解码Switch消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buf: 编码后的数据 (bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 解码后的消息
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
while pos < len(buf):
|
||||||
|
# 读取标签
|
||||||
|
tag, pos = decode_varint(buf, pos)
|
||||||
|
field_number = tag >> 3
|
||||||
|
wire_type = tag & 0x07
|
||||||
|
|
||||||
|
if field_number == 1: # device_action字段
|
||||||
|
if wire_type == 2: # 字符串类型
|
||||||
|
value, pos = decode_string(buf, pos)
|
||||||
|
result['device_action'] = value
|
||||||
|
elif field_number == 2: # bus_number字段
|
||||||
|
if wire_type == 0: # varint类型
|
||||||
|
value, pos = decode_varint(buf, pos)
|
||||||
|
result['bus_number'] = value
|
||||||
|
elif field_number == 3: # bus_address字段
|
||||||
|
if wire_type == 0: # varint类型
|
||||||
|
value, pos = decode_varint(buf, pos)
|
||||||
|
result['bus_address'] = value
|
||||||
|
elif field_number == 4: # relay_channel字段
|
||||||
|
if wire_type == 0: # varint类型
|
||||||
|
value, pos = decode_varint(buf, pos)
|
||||||
|
result['relay_channel'] = value
|
||||||
|
else:
|
||||||
|
# 跳过未知字段
|
||||||
|
if wire_type == 0: # varint
|
||||||
|
_, pos = decode_varint(buf, pos)
|
||||||
|
elif wire_type == 2: # 长度分隔
|
||||||
|
length, pos = decode_varint(buf, pos)
|
||||||
|
pos += length
|
||||||
|
else:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def encode_collect(bus_number, bus_address, value):
|
||||||
|
"""
|
||||||
|
编码Collect消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bus_number: 总线号 (int)
|
||||||
|
bus_address: 总线地址 (int)
|
||||||
|
value: 采集值 (float)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytearray: 编码后的数据
|
||||||
|
"""
|
||||||
|
result = bytearray()
|
||||||
|
|
||||||
|
# 编码bus_number字段 (field_number=1, wire_type=0)
|
||||||
|
result.extend(encode_varint((1 << 3) | 0)) # tag
|
||||||
|
result.extend(encode_varint(bus_number)) # value
|
||||||
|
|
||||||
|
# 编码bus_address字段 (field_number=2, wire_type=0)
|
||||||
|
result.extend(encode_varint((2 << 3) | 0)) # tag
|
||||||
|
result.extend(encode_varint(bus_address)) # value
|
||||||
|
|
||||||
|
# 编码value字段 (field_number=3, wire_type=5)
|
||||||
|
result.extend(encode_varint((3 << 3) | 5)) # tag
|
||||||
|
# 将float转换为little-endian的4字节
|
||||||
|
result.extend(struct.pack('<f', value)) # value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def decode_collect(buf):
|
||||||
|
"""
|
||||||
|
解码Collect消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buf: 编码后的数据 (bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 解码后的消息
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
while pos < len(buf):
|
||||||
|
# 读取标签
|
||||||
|
tag, pos = decode_varint(buf, pos)
|
||||||
|
field_number = tag >> 3
|
||||||
|
wire_type = tag & 0x07
|
||||||
|
|
||||||
|
if field_number == 1: # bus_number字段
|
||||||
|
if wire_type == 0: # varint类型
|
||||||
|
value, pos = decode_varint(buf, pos)
|
||||||
|
result['bus_number'] = value
|
||||||
|
elif field_number == 2: # bus_address字段
|
||||||
|
if wire_type == 0: # varint类型
|
||||||
|
value, pos = decode_varint(buf, pos)
|
||||||
|
result['bus_address'] = value
|
||||||
|
elif field_number == 3: # value字段
|
||||||
|
if wire_type == 5: # 32位浮点类型
|
||||||
|
# 从little-endian的4字节解析float
|
||||||
|
value = struct.unpack('<f', buf[pos:pos+4])[0]
|
||||||
|
pos += 4
|
||||||
|
result['value'] = value
|
||||||
|
else:
|
||||||
|
# 跳过未知字段
|
||||||
|
if wire_type == 0: # varint
|
||||||
|
_, pos = decode_varint(buf, pos)
|
||||||
|
elif wire_type == 5: # 32位固定长度
|
||||||
|
pos += 4
|
||||||
|
elif wire_type == 2: # 长度分隔
|
||||||
|
length, pos = decode_varint(buf, pos)
|
||||||
|
pos += length
|
||||||
|
else:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建一个Switch消息
|
||||||
|
switch_data = encode_switch("ON", 1, 10, 2)
|
||||||
|
print(f"编码后的Switch消息: {switch_data.hex()}")
|
||||||
|
|
||||||
|
# 创建一个Instruction消息,包含Switch数据
|
||||||
|
instruction_data = encode_instruction(METHOD_TYPE_SWITCH, switch_data)
|
||||||
|
print(f"编码后的Instruction消息: {instruction_data.hex()}")
|
||||||
|
|
||||||
|
# 解码Instruction消息
|
||||||
|
decoded_instruction = decode_instruction(instruction_data)
|
||||||
|
print(f"解码后的Instruction消息: {decoded_instruction}")
|
||||||
|
|
||||||
|
# 解码Switch消息
|
||||||
|
if 'data' in decoded_instruction:
|
||||||
|
decoded_switch = decode_switch(decoded_instruction['data'])
|
||||||
|
print(f"解码后的Switch消息: {decoded_switch}")
|
||||||
|
|
||||||
|
# 创建一个Collect消息
|
||||||
|
collect_data = encode_collect(1, 20, 25.6)
|
||||||
|
print(f"编码后的Collect消息: {collect_data.hex()}")
|
||||||
|
|
||||||
|
# 创建一个Instruction消息,包含Collect数据
|
||||||
|
instruction_data2 = encode_instruction(METHOD_TYPE_COLLECT, collect_data)
|
||||||
|
print(f"编码后的Instruction消息(Collect): {instruction_data2.hex()}")
|
||||||
|
|
||||||
|
# 解码Instruction消息
|
||||||
|
decoded_instruction2 = decode_instruction(instruction_data2)
|
||||||
|
print(f"解码后的Instruction消息: {decoded_instruction2}")
|
||||||
|
|
||||||
|
# 解码Collect消息
|
||||||
|
if 'data' in decoded_instruction2:
|
||||||
|
decoded_collect = decode_collect(decoded_instruction2['data'])
|
||||||
|
print(f"解码后的Collect消息: {decoded_collect}")
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 通信层
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Callable, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class BaseComm(ABC):
|
|
||||||
"""
|
|
||||||
通信接口抽象基类
|
|
||||||
定义所有通信模块需要实现的基本方法
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def connect(self) -> bool:
|
|
||||||
"""
|
|
||||||
建立通信连接
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 连接是否成功
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def disconnect(self) -> None:
|
|
||||||
"""
|
|
||||||
断开通信连接
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def send(self, data: bytes, address: Optional[str] = None) -> bool:
|
|
||||||
"""
|
|
||||||
发送数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 要发送的数据
|
|
||||||
address: 目标地址(可选)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 发送是否成功
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def receive(self, timeout: Optional[float] = None) -> Optional[bytes]:
|
|
||||||
"""
|
|
||||||
接收数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
timeout: 超时时间(秒)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: 接收到的数据,如果没有数据则返回None
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""
|
|
||||||
检查通信连接状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否已连接
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_callback(self, callback: Callable[[bytes], None]) -> None:
|
|
||||||
"""
|
|
||||||
设置数据接收回调函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
callback: 接收数据时调用的回调函数
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# lora实现
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"lora": {
|
|
||||||
"address": "0x1234",
|
|
||||||
"frequency": 433,
|
|
||||||
"bandwidth": 125,
|
|
||||||
"spreading_factor": 7,
|
|
||||||
"coding_rate": 5,
|
|
||||||
"encryption_key": "your_encryption_key_here"
|
|
||||||
},
|
|
||||||
"master": {
|
|
||||||
"lora_address": "0xABCD",
|
|
||||||
"protocol": "modbus"
|
|
||||||
},
|
|
||||||
"bus": {
|
|
||||||
"sensor": {
|
|
||||||
"port": "/dev/ttyUSB0",
|
|
||||||
"baudrate": 9600,
|
|
||||||
"devices": []
|
|
||||||
},
|
|
||||||
"actuator": {
|
|
||||||
"port": "/dev/ttyUSB1",
|
|
||||||
"baudrate": 9600,
|
|
||||||
"devices": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": {
|
|
||||||
"level": "INFO",
|
|
||||||
"file_path": "logs/pig_house.log",
|
|
||||||
"max_size": "1MB",
|
|
||||||
"backup_count": 1,
|
|
||||||
"report_errors": true,
|
|
||||||
"terminate_on_report_failure": true
|
|
||||||
},
|
|
||||||
"system": {
|
|
||||||
"heartbeat_interval": 60,
|
|
||||||
"data_collection_interval": 30,
|
|
||||||
"command_timeout": 10,
|
|
||||||
"retry_count": 3,
|
|
||||||
"error_handling": "retry"
|
|
||||||
},
|
|
||||||
"devices": [
|
|
||||||
{
|
|
||||||
"id": "temp_01",
|
|
||||||
"type": "temperature",
|
|
||||||
"address": "0x01",
|
|
||||||
"bus": "sensor",
|
|
||||||
"unit": "celsius",
|
|
||||||
"location": "main_hall"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "humidity_01",
|
|
||||||
"type": "humidity",
|
|
||||||
"address": "0x02",
|
|
||||||
"bus": "sensor",
|
|
||||||
"unit": "%",
|
|
||||||
"location": "main_hall"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "feed_01",
|
|
||||||
"type": "feed_port",
|
|
||||||
"address": "0x10",
|
|
||||||
"bus": "actuator",
|
|
||||||
"location": "feeding_area_1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "water_01",
|
|
||||||
"type": "water_valve",
|
|
||||||
"address": "0x11",
|
|
||||||
"bus": "actuator",
|
|
||||||
"location": "watering_area_1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
165
config.py
165
config.py
@@ -1,165 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
def __init__(self, config_file="config.json"):
|
|
||||||
self.config_file = config_file
|
|
||||||
self.default_config = {
|
|
||||||
# LoRa通信配置
|
|
||||||
"lora": {
|
|
||||||
"address": "0x1234",
|
|
||||||
"frequency": 433,
|
|
||||||
"bandwidth": 125,
|
|
||||||
"spreading_factor": 7,
|
|
||||||
"coding_rate": 5,
|
|
||||||
"encryption_key": "default_key"
|
|
||||||
},
|
|
||||||
|
|
||||||
# 上位机配置
|
|
||||||
"master": {
|
|
||||||
"lora_address": "0x5678",
|
|
||||||
"protocol": "modbus"
|
|
||||||
},
|
|
||||||
|
|
||||||
# 总线配置
|
|
||||||
"bus": {
|
|
||||||
"sensor": {
|
|
||||||
"port": "/dev/ttyUSB0",
|
|
||||||
"baudrate": 9600,
|
|
||||||
"devices": []
|
|
||||||
},
|
|
||||||
"actuator": {
|
|
||||||
"port": "/dev/ttyUSB1",
|
|
||||||
"baudrate": 9600,
|
|
||||||
"devices": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
"log": {
|
|
||||||
"level": "INFO",
|
|
||||||
"file_path": "logs/pig_house.log",
|
|
||||||
"max_size": "1KB", # 减小日志文件大小
|
|
||||||
"backup_count": 1, # 只保留一个备份文件
|
|
||||||
"report_errors": True, # 是否上报错误
|
|
||||||
"terminate_on_report_failure": True # 上报失败时是否终止程序
|
|
||||||
},
|
|
||||||
|
|
||||||
# 系统参数
|
|
||||||
"system": {
|
|
||||||
"heartbeat_interval": 60, # 心跳包间隔(秒)
|
|
||||||
"data_collection_interval": 300, # 数据采集间隔(秒)
|
|
||||||
"command_timeout": 10, # 命令超时时间(秒)
|
|
||||||
"retry_count": 3, # 重试次数
|
|
||||||
"error_handling": "retry"
|
|
||||||
},
|
|
||||||
|
|
||||||
# 设备配置
|
|
||||||
"devices": [
|
|
||||||
{
|
|
||||||
"id": "temp_01",
|
|
||||||
"type": "temperature",
|
|
||||||
"address": "0x01",
|
|
||||||
"bus": "sensor",
|
|
||||||
"unit": "celsius"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "humidity_01",
|
|
||||||
"type": "humidity",
|
|
||||||
"address": "0x02",
|
|
||||||
"bus": "sensor",
|
|
||||||
"unit": "%"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "feed_01",
|
|
||||||
"type": "feed_port",
|
|
||||||
"address": "0x10",
|
|
||||||
"bus": "actuator"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "water_01",
|
|
||||||
"type": "water_valve",
|
|
||||||
"address": "0x11",
|
|
||||||
"bus": "actuator"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
self.config = {}
|
|
||||||
self.load_config()
|
|
||||||
|
|
||||||
def load_config(self):
|
|
||||||
"""加载配置文件"""
|
|
||||||
if os.path.exists(self.config_file):
|
|
||||||
try:
|
|
||||||
with open(self.config_file, "r", encoding="utf-8") as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
# 合并默认配置和文件配置
|
|
||||||
self.config = self._merge_dict(self.default_config, config_data)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"加载配置文件出错: {e},使用默认配置")
|
|
||||||
self.config = self.default_config
|
|
||||||
else:
|
|
||||||
print("配置文件不存在,使用默认配置")
|
|
||||||
self.config = self.default_config
|
|
||||||
self.save_config()
|
|
||||||
|
|
||||||
def save_config(self):
|
|
||||||
"""保存配置到文件"""
|
|
||||||
try:
|
|
||||||
# 确保目录存在
|
|
||||||
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
with open(self.config_file, "w", encoding="utf-8") as file:
|
|
||||||
json.dump(self.config, file, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
def get(self, key_path, default=None):
|
|
||||||
"""根据路径获取配置项,例如: get('lora.address')"""
|
|
||||||
keys = key_path.split('.')
|
|
||||||
value = self.config
|
|
||||||
try:
|
|
||||||
for key in keys:
|
|
||||||
value = value[key]
|
|
||||||
return value
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
return default
|
|
||||||
|
|
||||||
def set(self, key_path, value):
|
|
||||||
"""根据路径设置配置项"""
|
|
||||||
keys = key_path.split('.')
|
|
||||||
config_ref = self.config
|
|
||||||
for key in keys[:-1]:
|
|
||||||
if key not in config_ref:
|
|
||||||
config_ref[key] = {}
|
|
||||||
config_ref = config_ref[key]
|
|
||||||
config_ref[keys[-1]] = value
|
|
||||||
|
|
||||||
def _merge_dict(self, default, override):
|
|
||||||
"""合并两个字典,保留默认值"""
|
|
||||||
merged = default.copy()
|
|
||||||
for key, value in override.items():
|
|
||||||
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
||||||
merged[key] = self._merge_dict(merged[key], value)
|
|
||||||
else:
|
|
||||||
merged[key] = value
|
|
||||||
return merged
|
|
||||||
|
|
||||||
# 便捷访问方法
|
|
||||||
def lora_config(self):
|
|
||||||
return self.config.get('lora', {})
|
|
||||||
|
|
||||||
def master_config(self):
|
|
||||||
return self.config.get('master', {})
|
|
||||||
|
|
||||||
def bus_config(self):
|
|
||||||
return self.config.get('bus', {})
|
|
||||||
|
|
||||||
def log_config(self):
|
|
||||||
return self.config.get('log', {})
|
|
||||||
|
|
||||||
def system_config(self):
|
|
||||||
return self.config.get('system', {})
|
|
||||||
|
|
||||||
def devices_config(self):
|
|
||||||
return self.config.get('devices', [])
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(ABC):
|
|
||||||
"""
|
|
||||||
命令处理器抽象基类
|
|
||||||
定义所有命令处理器需要实现的基本方法
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def handle_command(self, command: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
处理命令
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: 命令类型
|
|
||||||
data: 命令数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: 处理结果
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def register_command(self, command: str, handler_func) -> bool:
|
|
||||||
"""
|
|
||||||
注册命令处理函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: 命令类型
|
|
||||||
handler_func: 处理函数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 注册是否成功
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def unregister_command(self, command: str) -> bool:
|
|
||||||
"""
|
|
||||||
注销命令处理函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: 命令类型
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 注销是否成功
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 处理接收的指令
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class LogLevel(Enum):
|
|
||||||
"""日志等级枚举"""
|
|
||||||
DEBUG = "DEBUG"
|
|
||||||
INFO = "INFO"
|
|
||||||
WARNING = "WARNING"
|
|
||||||
ERROR = "ERROR"
|
|
||||||
CRITICAL = "CRITICAL"
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(Enum):
|
|
||||||
"""设备类型枚举"""
|
|
||||||
# 传感器类型
|
|
||||||
TEMPERATURE = "temperature"
|
|
||||||
HUMIDITY = "humidity"
|
|
||||||
PRESSURE = "pressure"
|
|
||||||
LIGHT = "light"
|
|
||||||
CO2 = "co2"
|
|
||||||
NH3 = "nh3" # 氨气
|
|
||||||
H2S = "h2s" # 硫化氢
|
|
||||||
|
|
||||||
# 执行器类型
|
|
||||||
FEED_PORT = "feed_port"
|
|
||||||
WATER_VALVE = "water_valve"
|
|
||||||
FAN = "fan"
|
|
||||||
HEATER = "heater"
|
|
||||||
COOLER = "cooler"
|
|
||||||
LIGHT_CONTROLLER = "light_controller"
|
|
||||||
|
|
||||||
|
|
||||||
class BusType(Enum):
|
|
||||||
"""总线类型枚举"""
|
|
||||||
SENSOR = "sensor"
|
|
||||||
ACTUATOR = "actuator"
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorHandlingStrategy(Enum):
|
|
||||||
"""错误处理策略枚举"""
|
|
||||||
RETRY = "retry"
|
|
||||||
SKIP = "skip"
|
|
||||||
ALERT = "alert"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 心跳包
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 定时任务
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 设备驱动
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceStatus(Enum):
|
|
||||||
"""设备状态枚举"""
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
ONLINE = "online"
|
|
||||||
OFFLINE = "offline"
|
|
||||||
ERROR = "error"
|
|
||||||
BUSY = "busy"
|
|
||||||
|
|
||||||
|
|
||||||
class BaseDevice(ABC):
|
|
||||||
"""
|
|
||||||
设备接口抽象基类
|
|
||||||
定义所有设备需要实现的基本方法
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, device_id: str, device_type: str, address: str, bus: str):
|
|
||||||
"""
|
|
||||||
初始化设备
|
|
||||||
|
|
||||||
Args:
|
|
||||||
device_id: 设备唯一标识
|
|
||||||
device_type: 设备类型
|
|
||||||
address: 设备地址
|
|
||||||
bus: 所在总线
|
|
||||||
"""
|
|
||||||
self.device_id = device_id
|
|
||||||
self.device_type = device_type
|
|
||||||
self.address = address
|
|
||||||
self.bus = bus
|
|
||||||
self.status = DeviceStatus.UNKNOWN
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def connect(self) -> bool:
|
|
||||||
"""
|
|
||||||
连接设备
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 连接是否成功
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def disconnect(self) -> None:
|
|
||||||
"""
|
|
||||||
断开设备连接
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def read_data(self) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
读取设备数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 设备数据,格式为 {数据名: 数据值},失败时返回None
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def write_data(self, data: Dict[str, Any]) -> bool:
|
|
||||||
"""
|
|
||||||
向设备写入数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 要写入的数据,格式为 {数据名: 数据值}
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 写入是否成功
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_status(self) -> DeviceStatus:
|
|
||||||
"""
|
|
||||||
获取设备状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DeviceStatus: 设备状态
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取设备信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 设备信息
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"device_id": self.device_id,
|
|
||||||
"device_type": self.device_type,
|
|
||||||
"address": self.address,
|
|
||||||
"bus": self.bus,
|
|
||||||
"status": self.status.value
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 下料口
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 温度传感器
|
|
||||||
311
main.py
311
main.py
@@ -4,3 +4,314 @@
|
|||||||
"""
|
"""
|
||||||
猪舍主控系统主程序入口
|
猪舍主控系统主程序入口
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import machine
|
||||||
|
import time
|
||||||
|
import struct
|
||||||
|
import client_pb
|
||||||
|
|
||||||
|
# 初始化RS485串口
|
||||||
|
# 使用UART2,连接到ESP32的GPIO16(RX)和GPIO17(TX)
|
||||||
|
rs485_uart = machine.UART(2, baudrate=9600, bits=8, parity=None, stop=1, rx=16, tx=17)
|
||||||
|
rs485_uart.init()
|
||||||
|
|
||||||
|
# RS485收发控制引脚
|
||||||
|
rs485_re_de_pin = machine.Pin(5, machine.Pin.OUT)
|
||||||
|
rs485_re_de_pin.value(0) # 默认接收模式
|
||||||
|
|
||||||
|
# ESP32设备地址(应该唯一标识这个ESP32设备)
|
||||||
|
ESP32_ADDRESS = 1
|
||||||
|
|
||||||
|
# LoRaWAN模块地址
|
||||||
|
LORA_MODULE_ADDRESS = 254
|
||||||
|
|
||||||
|
def receive_lora_message():
|
||||||
|
"""
|
||||||
|
接收来自LoRaWAN模块的消息
|
||||||
|
返回: 字节数据
|
||||||
|
"""
|
||||||
|
# 在共享的RS485总线上监听来自LoRaWAN模块的消息
|
||||||
|
# 需要检查消息是否是发给本设备的
|
||||||
|
buffer = bytearray()
|
||||||
|
|
||||||
|
# 持续监听,不设置超时
|
||||||
|
while rs485_uart.any():
|
||||||
|
data = rs485_uart.read()
|
||||||
|
if data:
|
||||||
|
buffer.extend(data)
|
||||||
|
# 简单的帧检测逻辑
|
||||||
|
if len(buffer) >= 3 and buffer[0] == 0xAA and buffer[-1] == 0x55:
|
||||||
|
# 检查地址是否匹配
|
||||||
|
if len(buffer) >= 3 and buffer[2] == ESP32_ADDRESS:
|
||||||
|
# 提取有效数据(去掉帧头、地址、校验和帧尾)
|
||||||
|
return buffer[3:-2]
|
||||||
|
else:
|
||||||
|
break # 没有更多数据可读
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_lora_message(data):
|
||||||
|
"""
|
||||||
|
通过LoRaWAN模块发送消息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 要发送的字节数据
|
||||||
|
"""
|
||||||
|
# 切换到发送模式
|
||||||
|
rs485_re_de_pin.value(1)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 构造发送给LoRaWAN模块的RS485帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.append(0xAA) # 帧头
|
||||||
|
frame.append(LORA_MODULE_ADDRESS & 0xFF) # LoRaWAN模块地址
|
||||||
|
frame.append(ESP32_ADDRESS & 0xFF) # 本设备地址(作为源地址)
|
||||||
|
|
||||||
|
# 添加数据
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验和
|
||||||
|
checksum = sum(frame[1:]) & 0xFF
|
||||||
|
frame.append(checksum)
|
||||||
|
frame.append(0x55) # 帧尾
|
||||||
|
|
||||||
|
# 发送命令
|
||||||
|
rs485_uart.write(frame)
|
||||||
|
print(f"通过LoRa发送数据: {frame.hex()}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 切换回接收模式
|
||||||
|
time.sleep_ms(10)
|
||||||
|
rs485_re_de_pin.value(0)
|
||||||
|
|
||||||
|
def send_rs485_command(bus_number, bus_address, command, channel=None):
|
||||||
|
"""
|
||||||
|
发送命令到RS485总线上的设备
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bus_number: 总线号
|
||||||
|
bus_address: 设备地址
|
||||||
|
command: 命令内容
|
||||||
|
channel: 通道号(可选)
|
||||||
|
"""
|
||||||
|
# 切换到发送模式
|
||||||
|
rs485_re_de_pin.value(1)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 构造RS485命令帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.append(0xAA) # 帧头
|
||||||
|
frame.append(bus_number & 0xFF) # 总线号
|
||||||
|
frame.append(bus_address & 0xFF) # 设备地址
|
||||||
|
|
||||||
|
# 添加命令数据
|
||||||
|
if isinstance(command, str):
|
||||||
|
frame.extend(command.encode('utf-8'))
|
||||||
|
elif isinstance(command, bytes):
|
||||||
|
frame.extend(command)
|
||||||
|
elif isinstance(command, int):
|
||||||
|
frame.append(command & 0xFF)
|
||||||
|
|
||||||
|
# 如果有通道号,则添加
|
||||||
|
if channel is not None:
|
||||||
|
frame.append(channel & 0xFF)
|
||||||
|
|
||||||
|
# 计算校验和
|
||||||
|
checksum = sum(frame[1:]) & 0xFF
|
||||||
|
frame.append(checksum)
|
||||||
|
frame.append(0x55) # 帧尾
|
||||||
|
|
||||||
|
# 发送命令
|
||||||
|
rs485_uart.write(frame)
|
||||||
|
print(f"已发送RS485命令: {frame.hex()}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 切换回接收模式
|
||||||
|
time.sleep_ms(10)
|
||||||
|
rs485_re_de_pin.value(0)
|
||||||
|
|
||||||
|
def collect_sensor_data(bus_number, bus_address):
|
||||||
|
"""
|
||||||
|
从传感器收集数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bus_number: 总线号
|
||||||
|
bus_address: 传感器地址
|
||||||
|
|
||||||
|
返回:
|
||||||
|
传感器数据
|
||||||
|
"""
|
||||||
|
# 切换到发送模式
|
||||||
|
rs485_re_de_pin.value(1)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 构造读取传感器数据的命令
|
||||||
|
frame = bytearray([0xAA, bus_number & 0xFF, bus_address & 0xFF, 0x01, 0x00, 0x55])
|
||||||
|
rs485_uart.write(frame)
|
||||||
|
print(f"已发送传感器读取命令: {frame.hex()}")
|
||||||
|
|
||||||
|
# 等待响应
|
||||||
|
time.sleep_ms(50) # 短暂等待响应
|
||||||
|
|
||||||
|
# 读取传感器返回的数据
|
||||||
|
buffer = bytearray()
|
||||||
|
|
||||||
|
while rs485_uart.any():
|
||||||
|
data = rs485_uart.read()
|
||||||
|
if data:
|
||||||
|
buffer.extend(data)
|
||||||
|
# 简单的帧检测逻辑
|
||||||
|
if len(buffer) >= 3 and buffer[0] == 0xAA and buffer[-1] == 0x55:
|
||||||
|
# 检查地址是否匹配
|
||||||
|
if len(buffer) >= 3 and buffer[2] == ESP32_ADDRESS:
|
||||||
|
# 提取有效数据
|
||||||
|
if len(buffer) >= 5:
|
||||||
|
# 模拟一个浮点数值
|
||||||
|
value = float(buffer[3] + (buffer[4] << 8)) / 100.0
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(buffer) > 0:
|
||||||
|
print(f"传感器响应不完整: {buffer.hex()}")
|
||||||
|
else:
|
||||||
|
print("传感器无响应")
|
||||||
|
return None
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 切换回接收模式
|
||||||
|
time.sleep_ms(10)
|
||||||
|
rs485_re_de_pin.value(0)
|
||||||
|
|
||||||
|
def parse_instruction(data):
|
||||||
|
"""
|
||||||
|
解析来自LoRaWAN的指令
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: protobuf编码的指令数据
|
||||||
|
|
||||||
|
返回:
|
||||||
|
解析后的指令对象,失败时返回None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instruction = client_pb.decode_instruction(data)
|
||||||
|
return instruction
|
||||||
|
except Exception as e:
|
||||||
|
print(f"解析指令失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_switch_instruction(switch_msg):
|
||||||
|
"""
|
||||||
|
处理开关指令
|
||||||
|
|
||||||
|
参数:
|
||||||
|
switch_msg: Switch消息字典
|
||||||
|
"""
|
||||||
|
action = switch_msg.get('device_action', '')
|
||||||
|
bus_number = switch_msg.get('bus_number', 0)
|
||||||
|
bus_address = switch_msg.get('bus_address', 0)
|
||||||
|
channel = switch_msg.get('relay_channel', 0)
|
||||||
|
|
||||||
|
print(f"处理开关指令: 动作={action}, 总线={bus_number}, 地址={bus_address}, 通道={channel}")
|
||||||
|
|
||||||
|
if action.upper() == "ON":
|
||||||
|
# 发送开启设备命令
|
||||||
|
send_rs485_command(bus_number, bus_address, 0x01, channel)
|
||||||
|
elif action.upper() == "OFF":
|
||||||
|
# 发送关闭设备命令
|
||||||
|
send_rs485_command(bus_number, bus_address, 0x00, channel)
|
||||||
|
else:
|
||||||
|
# 其他自定义命令
|
||||||
|
send_rs485_command(bus_number, bus_address, action, channel)
|
||||||
|
|
||||||
|
def handle_collect_instruction(collect_msg):
|
||||||
|
"""
|
||||||
|
处理采集指令
|
||||||
|
|
||||||
|
参数:
|
||||||
|
collect_msg: Collect消息字典
|
||||||
|
"""
|
||||||
|
bus_number = collect_msg.get('bus_number', 0)
|
||||||
|
bus_address = collect_msg.get('bus_address', 0)
|
||||||
|
|
||||||
|
print(f"处理采集指令: 总线={bus_number}, 地址={bus_address}")
|
||||||
|
|
||||||
|
# 从传感器采集数据
|
||||||
|
value = collect_sensor_data(bus_number, bus_address)
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
# 构造Collect响应消息
|
||||||
|
collect_data = client_pb.encode_collect(bus_number, bus_address, value)
|
||||||
|
|
||||||
|
# 构造Instruction消息包装Collect数据
|
||||||
|
instruction_data = client_pb.encode_instruction(client_pb.METHOD_TYPE_COLLECT, collect_data)
|
||||||
|
|
||||||
|
# 发送回上位机
|
||||||
|
send_lora_message(instruction_data)
|
||||||
|
else:
|
||||||
|
print("采集数据失败")
|
||||||
|
|
||||||
|
def process_instruction(instruction):
|
||||||
|
"""
|
||||||
|
处理解析后的指令
|
||||||
|
|
||||||
|
参数:
|
||||||
|
instruction: 解析后的指令字典
|
||||||
|
"""
|
||||||
|
method = instruction.get('method', -1)
|
||||||
|
|
||||||
|
if method == client_pb.METHOD_TYPE_SWITCH:
|
||||||
|
# 处理开关指令
|
||||||
|
if 'data' in instruction:
|
||||||
|
switch_msg = client_pb.decode_switch(instruction['data'])
|
||||||
|
handle_switch_instruction(switch_msg)
|
||||||
|
else:
|
||||||
|
print("开关指令缺少data字段")
|
||||||
|
elif method == client_pb.METHOD_TYPE_COLLECT:
|
||||||
|
# 处理采集指令
|
||||||
|
if 'data' in instruction:
|
||||||
|
collect_msg = client_pb.decode_collect(instruction['data'])
|
||||||
|
handle_collect_instruction(collect_msg)
|
||||||
|
else:
|
||||||
|
print("采集指令缺少data字段")
|
||||||
|
else:
|
||||||
|
print(f"不支持的指令类型: {method}")
|
||||||
|
|
||||||
|
def main_loop():
|
||||||
|
"""
|
||||||
|
主循环
|
||||||
|
"""
|
||||||
|
print("猪舍控制系统启动...")
|
||||||
|
print(f"设备地址: {ESP32_ADDRESS}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 接收LoRaWAN消息
|
||||||
|
lora_data = receive_lora_message()
|
||||||
|
|
||||||
|
if lora_data:
|
||||||
|
print(f"收到LoRaWAN消息: {lora_data.hex()}")
|
||||||
|
|
||||||
|
# 解析指令
|
||||||
|
instruction = parse_instruction(lora_data)
|
||||||
|
|
||||||
|
if instruction:
|
||||||
|
# 处理指令
|
||||||
|
process_instruction(instruction)
|
||||||
|
else:
|
||||||
|
print("无效的指令数据")
|
||||||
|
|
||||||
|
# 其他周期性任务可以放在这里
|
||||||
|
# 例如定时采集传感器数据等
|
||||||
|
time.sleep(0.01) # 短暂休眠避免过度占用CPU
|
||||||
|
|
||||||
|
# 程序入口
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main_loop()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("程序被中断")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"程序异常: {e}")
|
||||||
32
proto/client.proto
Normal file
32
proto/client.proto
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package device;
|
||||||
|
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
|
||||||
|
option go_package = "internal/app/service/device/proto";
|
||||||
|
|
||||||
|
// 指令类型
|
||||||
|
enum MethodType{
|
||||||
|
SWITCH = 0; // 启停
|
||||||
|
COLLECT = 1; // 采集
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指令
|
||||||
|
message Instruction{
|
||||||
|
MethodType method = 1;
|
||||||
|
google.protobuf.Any data = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Switch{
|
||||||
|
string device_action = 1; // 指令
|
||||||
|
int32 bus_number = 2; // 总线号
|
||||||
|
int32 bus_address = 3; // 总线地址
|
||||||
|
int32 relay_channel = 4; // 继电器通道号
|
||||||
|
}
|
||||||
|
|
||||||
|
message Collect{
|
||||||
|
int32 bus_number = 1; // 总线号
|
||||||
|
int32 bus_address = 2; // 总线地址
|
||||||
|
float value = 3; // 采集值
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# 项目依赖包
|
|
||||||
|
|
||||||
# LoRa通信相关
|
|
||||||
pyserial>=3.5
|
|
||||||
paho-mqtt>=1.6.1
|
|
||||||
|
|
||||||
# CoAP协议支持
|
|
||||||
aiocoap>=0.4.5
|
|
||||||
|
|
||||||
# LwM2M支持 (使用wakaama-python作为替代)
|
|
||||||
# 注意:lwm2m-client在PyPI上不可用,需要手动安装或使用其他实现
|
|
||||||
# wakaama-python>=0.1.0
|
|
||||||
|
|
||||||
# 数据格式(SenML)
|
|
||||||
senml>=1.0.0
|
|
||||||
|
|
||||||
# 其他工具
|
|
||||||
jsonschema>=4.0.0
|
|
||||||
|
|
||||||
# 通用依赖
|
|
||||||
typing-extensions>=3.10.0; python_version < "3.10"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 持久化存储
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class BaseStorage(ABC):
|
|
||||||
"""
|
|
||||||
存储接口抽象基类
|
|
||||||
定义所有存储模块需要实现的基本方法
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def save(self, key: str, value: Any) -> bool:
|
|
||||||
"""
|
|
||||||
保存数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: 键
|
|
||||||
value: 值
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 保存是否成功
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def load(self, key: str, default: Any = None) -> Any:
|
|
||||||
"""
|
|
||||||
加载数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: 键
|
|
||||||
default: 默认值
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Any: 加载的数据
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def delete(self, key: str) -> bool:
|
|
||||||
"""
|
|
||||||
删除数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: 要删除的键
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 删除是否成功
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def exists(self, key: str) -> bool:
|
|
||||||
"""
|
|
||||||
检查键是否存在
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: 键
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 键是否存在
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def list_keys(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
列出所有键
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[str]: 所有键的列表
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def clear(self) -> bool:
|
|
||||||
"""
|
|
||||||
清空所有数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 清空是否成功
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def save_batch(self, data: Dict[str, Any]) -> bool:
|
|
||||||
"""
|
|
||||||
批量保存数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 要保存的数据字典
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 保存是否成功
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def load_batch(self, keys: List[str]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
批量加载数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keys: 要加载的键列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: 加载的数据字典
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# json 文件实现
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from storage.base_storage import BaseStorage
|
|
||||||
|
|
||||||
|
|
||||||
class JSONStorage(BaseStorage):
|
|
||||||
def __init__(self, filename="data.json"):
|
|
||||||
self.filename = filename
|
|
||||||
# 如果文件不存在,先创建空字典
|
|
||||||
try:
|
|
||||||
with open(self.filename, "r") as f:
|
|
||||||
pass
|
|
||||||
except OSError:
|
|
||||||
with open(self.filename, "w") as f:
|
|
||||||
f.write("{}")
|
|
||||||
|
|
||||||
def _read_all(self):
|
|
||||||
with open(self.filename, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def _write_all(self, data):
|
|
||||||
with open(self.filename, "w") as f:
|
|
||||||
json.dump(data, f)
|
|
||||||
|
|
||||||
def save(self, key, value):
|
|
||||||
data = self._read_all()
|
|
||||||
data[key] = value
|
|
||||||
self._write_all(data)
|
|
||||||
|
|
||||||
def load(self, key, default=None):
|
|
||||||
data = self._read_all()
|
|
||||||
return data.get(key, default)
|
|
||||||
|
|
||||||
def delete(self, key):
|
|
||||||
data = self._read_all()
|
|
||||||
if key in data:
|
|
||||||
del data[key]
|
|
||||||
self._write_all(data)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 内存实现
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 核心逻辑测试
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 校验工具
|
|
||||||
57
utils/fs.py
57
utils/fs.py
@@ -1,57 +0,0 @@
|
|||||||
# 兼容PC和MicroPython的文件操作
|
|
||||||
|
|
||||||
# compat_fs.py
|
|
||||||
try:
|
|
||||||
import uos as os # MicroPython
|
|
||||||
MICROPYTHON = True
|
|
||||||
except ImportError:
|
|
||||||
import os # CPython
|
|
||||||
MICROPYTHON = False
|
|
||||||
|
|
||||||
def list_dir(path="."):
|
|
||||||
"""列出目录内容"""
|
|
||||||
return os.listdir(path)
|
|
||||||
|
|
||||||
def make_dir(path):
|
|
||||||
"""创建目录"""
|
|
||||||
if MICROPYTHON:
|
|
||||||
os.mkdir(path)
|
|
||||||
else:
|
|
||||||
os.makedirs(path, exist_ok=True)
|
|
||||||
|
|
||||||
def remove_file(path):
|
|
||||||
"""删除文件"""
|
|
||||||
os.remove(path)
|
|
||||||
|
|
||||||
def read_file(path, mode="r"):
|
|
||||||
"""读取文件内容"""
|
|
||||||
with open(path, mode) as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
def write_file(path, data, mode="w"):
|
|
||||||
"""写入文件内容"""
|
|
||||||
with open(path, mode) as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
def is_file(path):
|
|
||||||
"""判断是否是文件"""
|
|
||||||
try:
|
|
||||||
st = os.stat(path)
|
|
||||||
# MicroPython: stat()[0] >> 14 & 0xF == 8 表示普通文件
|
|
||||||
if MICROPYTHON:
|
|
||||||
return (st[0] >> 14) & 0xF == 8
|
|
||||||
else:
|
|
||||||
return os.path.isfile(path)
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_dir(path):
|
|
||||||
"""判断是否是目录"""
|
|
||||||
try:
|
|
||||||
st = os.stat(path)
|
|
||||||
if MICROPYTHON:
|
|
||||||
return (st[0] >> 14) & 0xF == 2
|
|
||||||
else:
|
|
||||||
return os.path.isdir(path)
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 日志
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 数据解析工具
|
|
||||||
Reference in New Issue
Block a user