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