玖行换电站协议
This commit is contained in:
parent
5f53e0edff
commit
68b599a660
@ -1,150 +1,20 @@
|
|||||||
# 换电站 MQTT 协议解析器
|
# Swap Station Project
|
||||||
|
A PyCharm project for connecting to a swap station via MQTT on Windows.
|
||||||
|
|
||||||
该软件包提供了一个基于 Python 的换电站 MQTT 协议实现,符合 V2.6.2 规范。
|
## Structure
|
||||||
|
- `src/common/`: Shared configurations and encryption utilities.
|
||||||
|
- `src/key_management/`: Handles key exchange and AES key storage.
|
||||||
|
- `src/state_receiver/`: Receives state information messages.
|
||||||
|
- `src/event_receiver/`: Receives event record messages.
|
||||||
|
- `data/`: Stores AES keys (aes_key.bin, aes_iv.bin).
|
||||||
|
- `logs/`: Optional log files.
|
||||||
|
|
||||||
## 主要功能
|
## Setup
|
||||||
|
1. Install EMQX and configure authentication.
|
||||||
|
2. Run `pip install -r requirements.txt` in the PyCharm terminal.
|
||||||
|
3. Replace the platform public key in `common.py`.
|
||||||
|
4. Run `key_management.py` first, then `state_receiver.py` and `event_receiver.py`.
|
||||||
|
|
||||||
- 完整实现所有消息类型(状态类、事件类、请求/响应类)
|
## Usage
|
||||||
- 支持加密功能(RSA/AES)
|
- Use PyCharm's "Run Configurations" to execute each module.
|
||||||
- 全面的消息字段验证
|
- Simulate messages with MQTTX or a test script.
|
||||||
- 简单易用的消息创建和解析 API
|
|
||||||
- 完整的测试覆盖
|
|
||||||
|
|
||||||
## 安装方法
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
下面是创建和解析站点状态消息的简单示例:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from mqtt_protocol import StationStateMessage
|
|
||||||
|
|
||||||
# 创建站点状态消息
|
|
||||||
state_msg = StationStateMessage.create(
|
|
||||||
state=1, # 运营状态
|
|
||||||
smoke="1,1,1,1", # 所有烟感正常
|
|
||||||
fire=1, # 消防系统正常
|
|
||||||
temp=25, # 温度
|
|
||||||
humid=60, # 湿度
|
|
||||||
totalElect=1000.5 # 总电量
|
|
||||||
)
|
|
||||||
|
|
||||||
# 转换为 JSON
|
|
||||||
json_data = state_msg.to_json()
|
|
||||||
|
|
||||||
# 从 JSON 解析
|
|
||||||
parsed_msg = StationStateMessage.from_json(json_data)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 消息类型
|
|
||||||
|
|
||||||
### 状态类消息
|
|
||||||
- 换电站属性信息
|
|
||||||
- 换电站状态
|
|
||||||
- 机器人状态
|
|
||||||
- 换电车辆状态
|
|
||||||
- 换电过程实时状态
|
|
||||||
- 电池状态
|
|
||||||
- 充电机状态
|
|
||||||
|
|
||||||
### 事件类消息
|
|
||||||
- 充电事件记录
|
|
||||||
- 换电事件记录
|
|
||||||
- 故障告警事件记录
|
|
||||||
|
|
||||||
### 请求/响应类消息
|
|
||||||
- 换电启动请求/响应
|
|
||||||
- 充电启动请求/响应
|
|
||||||
- 记录查询请求/响应
|
|
||||||
- 费率模型查询请求/响应
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
mqtt_protocol/
|
|
||||||
├── README.md # 项目说明文档
|
|
||||||
├── requirements.txt # 项目依赖
|
|
||||||
├── setup.py # 安装配置
|
|
||||||
├── docs/ # 文档目录
|
|
||||||
├── examples/ # 示例代码
|
|
||||||
├── tests/ # 测试用例
|
|
||||||
└── mqtt_protocol/ # 主代码目录
|
|
||||||
├── base_message.py # 基础消息类
|
|
||||||
├── state_messages.py # 状态类消息
|
|
||||||
├── event_messages.py # 事件类消息
|
|
||||||
├── request_response.py # 请求响应类消息
|
|
||||||
├── encryption_handler.py # 加密处理
|
|
||||||
└── utils/ # 工具类目录
|
|
||||||
```
|
|
||||||
|
|
||||||
## 运行测试
|
|
||||||
|
|
||||||
使用 pytest 运行测试:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 示例代码
|
|
||||||
|
|
||||||
查看 `examples/` 目录中的使用示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/state_example.py # 状态消息示例
|
|
||||||
python examples/event_example.py # 事件消息示例
|
|
||||||
python examples/request_example.py # 请求响应示例
|
|
||||||
```
|
|
||||||
|
|
||||||
## 协议文档
|
|
||||||
|
|
||||||
详细的协议规范请参见 `docs/protocol_spec.md`。
|
|
||||||
|
|
||||||
## 系统要求
|
|
||||||
|
|
||||||
- Python 3.7+
|
|
||||||
- pycryptodome>=3.15.0(加密库)
|
|
||||||
- pytest>=7.0.0(测试用)
|
|
||||||
- paho-mqtt>=1.6.1(MQTT客户端)
|
|
||||||
- python-dateutil>=2.8.2(日期处理)
|
|
||||||
|
|
||||||
## 主要功能模块说明
|
|
||||||
|
|
||||||
### 1. 基础消息模块 (base_message.py)
|
|
||||||
- 定义了所有消息的基础类
|
|
||||||
- 包含消息头部处理
|
|
||||||
- 提供 JSON 序列化和反序列化功能
|
|
||||||
|
|
||||||
### 2. 状态消息模块 (state_messages.py)
|
|
||||||
- 实现各类状态信息的处理
|
|
||||||
- 包括站点状态、设备状态等
|
|
||||||
- 支持实时状态更新
|
|
||||||
|
|
||||||
### 3. 事件消息模块 (event_messages.py)
|
|
||||||
- 处理充电、换电等事件记录
|
|
||||||
- 支持事件确认机制
|
|
||||||
- 包含事件重试逻辑
|
|
||||||
|
|
||||||
### 4. 请求响应模块 (request_response.py)
|
|
||||||
- 实现所有请求响应类消息
|
|
||||||
- 支持同步和异步请求处理
|
|
||||||
- 包含超时和重试机制
|
|
||||||
|
|
||||||
### 5. 加密模块 (encryption_handler.py)
|
|
||||||
- 实现 RSA 和 AES 加密
|
|
||||||
- 提供密钥管理功能
|
|
||||||
- 支持安全通信
|
|
||||||
|
|
||||||
## 使用注意事项
|
|
||||||
|
|
||||||
1. 在使用前确保正确配置 MQTT 服务器连接信息
|
|
||||||
2. 注意处理所有可能的异常情况
|
|
||||||
3. 建议在生产环境中启用加密功能
|
|
||||||
4. 定期检查并处理消息重试队列
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
MIT 许可证
|
|
Binary file not shown.
@ -1,79 +0,0 @@
|
|||||||
"""
|
|
||||||
Examples of using event messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from mqtt_protocol import ChargeRecord, SwapRecord
|
|
||||||
from mqtt_protocol.event_messages import RateDetail, ChargeStartType, SwapType, SwapMode
|
|
||||||
|
|
||||||
|
|
||||||
def charge_record_example():
|
|
||||||
# Create rate details
|
|
||||||
rate_details = [
|
|
||||||
RateDetail(rateType=1, startTime="2024-02-24 10:00:00",
|
|
||||||
stopTime="2024-02-24 12:00:00", elect=50.5),
|
|
||||||
RateDetail(rateType=2, startTime="2024-02-24 12:00:00",
|
|
||||||
stopTime="2024-02-24 14:00:00", elect=45.8)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create a charge record
|
|
||||||
charge_record = ChargeRecord.create(
|
|
||||||
equipNo="CHG001",
|
|
||||||
orderSn="CR202402240001",
|
|
||||||
gunNo="1",
|
|
||||||
startTime="2024-02-24 10:00:00",
|
|
||||||
stopTime="2024-02-24 14:00:00",
|
|
||||||
startSOC=20.5,
|
|
||||||
endSOC=85.3,
|
|
||||||
chgQty=96.3,
|
|
||||||
startMeter=1000.0,
|
|
||||||
endMeter=1096.3,
|
|
||||||
plateNo="京A12345",
|
|
||||||
vin="LSVAA12345G123456",
|
|
||||||
stopReason=1,
|
|
||||||
sharpElect=50.5,
|
|
||||||
peakElect=45.8,
|
|
||||||
flatElect=0.0,
|
|
||||||
valleyElect=0.0,
|
|
||||||
rateModelID="RATE001",
|
|
||||||
detailsList=rate_details,
|
|
||||||
swapSn="",
|
|
||||||
chgSwitch=0,
|
|
||||||
startType=ChargeStartType.STATION_AUTO
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Charge Record Example:")
|
|
||||||
print(charge_record.to_json())
|
|
||||||
|
|
||||||
|
|
||||||
def swap_record_example():
|
|
||||||
# Create a swap record
|
|
||||||
swap_record = SwapRecord(
|
|
||||||
equipNo="ROBOT001",
|
|
||||||
orderSn="SR202402240001",
|
|
||||||
startTime="2024-02-24 15:00:00",
|
|
||||||
stopTime="2024-02-24 15:10:00",
|
|
||||||
vin="LSVAA12345G123456",
|
|
||||||
rfidCode="123456789012345678901234",
|
|
||||||
plateNo="京A12345",
|
|
||||||
newBatID="BAT001",
|
|
||||||
newCabinetNo=1,
|
|
||||||
newBatSoc=95.5,
|
|
||||||
oldBatID="BAT002",
|
|
||||||
oldCabinetNo=2,
|
|
||||||
oldBatSoc=15.5,
|
|
||||||
swapType=SwapType.BATTERY_SWAP,
|
|
||||||
swapMode=SwapMode.FULL_AUTO,
|
|
||||||
swapStartType=1,
|
|
||||||
lane=1,
|
|
||||||
userID="USER001",
|
|
||||||
stationMode=1
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nSwap Record Example:")
|
|
||||||
print(swap_record.to_json())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
charge_record_example()
|
|
||||||
swap_record_example()
|
|
@ -1,103 +0,0 @@
|
|||||||
"""
|
|
||||||
Examples of using request and response messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
from mqtt_protocol import (
|
|
||||||
SwapStartRequest,
|
|
||||||
SwapStartResponse,
|
|
||||||
ChargeStartRequest,
|
|
||||||
ChargeStartResponse
|
|
||||||
)
|
|
||||||
from mqtt_protocol.request_response import (
|
|
||||||
RateModelQuery,
|
|
||||||
RateModelResponse,
|
|
||||||
RateInfo,
|
|
||||||
RateTimeSegment
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def swap_request_example():
|
|
||||||
# Create a swap start request
|
|
||||||
swap_req = SwapStartRequest(
|
|
||||||
vin="LSVAA12345G123456",
|
|
||||||
rfid_code="123456789012345678901234",
|
|
||||||
plate_no="京A12345",
|
|
||||||
lane=1,
|
|
||||||
user_id="USER001"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Swap Start Request Example:")
|
|
||||||
print(swap_req.to_json())
|
|
||||||
|
|
||||||
# Create a response
|
|
||||||
swap_resp = SwapStartResponse(
|
|
||||||
vin="LSVAA12345G123456",
|
|
||||||
rfid_code="123456789012345678901234",
|
|
||||||
plate_no="京A12345",
|
|
||||||
result=1, # Success
|
|
||||||
order_sn="SR202402240001"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nSwap Start Response Example:")
|
|
||||||
print(swap_resp.to_json())
|
|
||||||
|
|
||||||
|
|
||||||
def charge_request_example():
|
|
||||||
# Create a charge start request
|
|
||||||
charge_req = ChargeStartRequest(
|
|
||||||
chg_id="CHG001",
|
|
||||||
gun_no=1
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nCharge Start Request Example:")
|
|
||||||
print(charge_req.to_json())
|
|
||||||
|
|
||||||
# Create a response
|
|
||||||
charge_resp = ChargeStartResponse(
|
|
||||||
result=1, # Success
|
|
||||||
chg_id="CHG001",
|
|
||||||
gun_no=1,
|
|
||||||
order_sn="CR202402240001"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nCharge Start Response Example:")
|
|
||||||
print(charge_resp.to_json())
|
|
||||||
|
|
||||||
|
|
||||||
def rate_model_example():
|
|
||||||
# Create rate model query
|
|
||||||
rate_query = RateModelQuery("RATE001")
|
|
||||||
print("\nRate Model Query Example:")
|
|
||||||
print(rate_query.to_json())
|
|
||||||
|
|
||||||
# Create rate info list
|
|
||||||
rate_list = [
|
|
||||||
RateInfo(rateType=1, electPrice=1.2, servicePrice=0.5),
|
|
||||||
RateInfo(rateType=2, electPrice=1.0, servicePrice=0.4),
|
|
||||||
RateInfo(rateType=3, electPrice=0.8, servicePrice=0.3),
|
|
||||||
RateInfo(rateType=4, electPrice=0.6, servicePrice=0.2)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create rate time segments
|
|
||||||
time_segments = [
|
|
||||||
RateTimeSegment(rateType=1, index=1,
|
|
||||||
startTime="10:00:00", stopTime="12:00:00"),
|
|
||||||
RateTimeSegment(rateType=2, index=2,
|
|
||||||
startTime="12:00:00", stopTime="14:00:00")
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create response
|
|
||||||
rate_resp = RateModelResponse(
|
|
||||||
rate_model_id="RATE001",
|
|
||||||
rate_list=rate_list,
|
|
||||||
rate_details_list=time_segments
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nRate Model Response Example:")
|
|
||||||
print(rate_resp.to_json())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
swap_request_example()
|
|
||||||
charge_request_example()
|
|
||||||
rate_model_example()
|
|
@ -1,68 +0,0 @@
|
|||||||
"""
|
|
||||||
Constants used throughout the MQTT protocol implementation
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Protocol version
|
|
||||||
PROTOCOL_VERSION = "V2.6.2"
|
|
||||||
|
|
||||||
# Topic related constants
|
|
||||||
TOPIC_PREFIX = "HCMS" # Heavy truck Charging Management System
|
|
||||||
TOPIC_SEPARATOR = "/"
|
|
||||||
|
|
||||||
# Message directions
|
|
||||||
M2S = "M2S" # Master to Slave (Cloud Platform to Station)
|
|
||||||
S2M = "S2M" # Slave to Master (Station to Cloud Platform)
|
|
||||||
|
|
||||||
# Message types
|
|
||||||
STATE = "state"
|
|
||||||
EVENT = "event"
|
|
||||||
CONFIRM = "confirm"
|
|
||||||
REQUEST = "request"
|
|
||||||
RESPONSE = "response"
|
|
||||||
KEEPALIVE = "keepalive"
|
|
||||||
ENCRYPT_KEY_REQ = "encryptKeyReq"
|
|
||||||
ENCRYPT_KEY_RESP = "encryptKeyResp"
|
|
||||||
|
|
||||||
# Station modes
|
|
||||||
STATION_MODE_OPERATION = 1
|
|
||||||
STATION_MODE_DEBUG = 2
|
|
||||||
STATION_MODE_MAINTENANCE = 3
|
|
||||||
|
|
||||||
# Device states
|
|
||||||
DEVICE_STATE_UNKNOWN = 0
|
|
||||||
DEVICE_STATE_NORMAL = 1
|
|
||||||
DEVICE_STATE_ALARM = 2
|
|
||||||
|
|
||||||
# Charging states
|
|
||||||
CHARGING_STATE_STANDBY = 1
|
|
||||||
CHARGING_STATE_CHARGING = 2
|
|
||||||
CHARGING_STATE_COMPLETE = 3
|
|
||||||
CHARGING_STATE_FAULT = 4
|
|
||||||
CHARGING_STATE_OFFLINE = 5
|
|
||||||
|
|
||||||
# Swap states
|
|
||||||
SWAP_STATE_NOT_STARTED = 1
|
|
||||||
SWAP_STATE_STARTED = 2
|
|
||||||
SWAP_STATE_PAUSED = 3
|
|
||||||
SWAP_STATE_RESUMED = 4
|
|
||||||
SWAP_STATE_TERMINATED = 5
|
|
||||||
SWAP_STATE_COMPLETED = 6
|
|
||||||
|
|
||||||
# Rate types
|
|
||||||
RATE_TYPE_SHARP = 1
|
|
||||||
RATE_TYPE_PEAK = 2
|
|
||||||
RATE_TYPE_FLAT = 3
|
|
||||||
RATE_TYPE_VALLEY = 4
|
|
||||||
|
|
||||||
# Lane types
|
|
||||||
LANE_TYPE_SINGLE = 1
|
|
||||||
LANE_TYPE_DOUBLE = 2
|
|
||||||
|
|
||||||
# Error codes
|
|
||||||
ERROR_SUCCESS = 1
|
|
||||||
ERROR_FAILURE = 2
|
|
||||||
|
|
||||||
# Timeouts (in seconds)
|
|
||||||
KEEPALIVE_INTERVAL = 30
|
|
||||||
EVENT_RETRY_INTERVAL = 15
|
|
||||||
COMMUNICATION_TIMEOUT = 90 # 3 * KEEPALIVE_INTERVAL
|
|
@ -1,40 +0,0 @@
|
|||||||
"""
|
|
||||||
MQTT Protocol Parser for Battery Swap Station
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
|
||||||
|
|
||||||
from .base_message import BaseMessage, MessageHeader, MessageType, MessageDirection
|
|
||||||
from .state_messages import (
|
|
||||||
StationInfo,
|
|
||||||
StationStateMessage,
|
|
||||||
RobotStateMessage
|
|
||||||
)
|
|
||||||
from .event_messages import (
|
|
||||||
ChargeRecord,
|
|
||||||
SwapRecord
|
|
||||||
)
|
|
||||||
from .request_response import (
|
|
||||||
SwapStartRequest,
|
|
||||||
SwapStartResponse,
|
|
||||||
ChargeStartRequest,
|
|
||||||
ChargeStartResponse
|
|
||||||
)
|
|
||||||
from .encryption_handler import EncryptionHandler
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'BaseMessage',
|
|
||||||
'MessageHeader',
|
|
||||||
'MessageType',
|
|
||||||
'MessageDirection',
|
|
||||||
'StationInfo',
|
|
||||||
'StationStateMessage',
|
|
||||||
'RobotStateMessage',
|
|
||||||
'ChargeRecord',
|
|
||||||
'SwapRecord',
|
|
||||||
'SwapStartRequest',
|
|
||||||
'SwapStartResponse',
|
|
||||||
'ChargeStartRequest',
|
|
||||||
'ChargeStartResponse',
|
|
||||||
'EncryptionHandler'
|
|
||||||
]
|
|
@ -1,68 +0,0 @@
|
|||||||
"""
|
|
||||||
Constants used throughout the MQTT protocol implementation
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Protocol version
|
|
||||||
PROTOCOL_VERSION = "V2.6.2"
|
|
||||||
|
|
||||||
# Topic related constants
|
|
||||||
TOPIC_PREFIX = "HCMS" # Heavy truck Charging Management System
|
|
||||||
TOPIC_SEPARATOR = "/"
|
|
||||||
|
|
||||||
# Message directions
|
|
||||||
M2S = "M2S" # Master to Slave (Cloud Platform to Station)
|
|
||||||
S2M = "S2M" # Slave to Master (Station to Cloud Platform)
|
|
||||||
|
|
||||||
# Message types
|
|
||||||
STATE = "state"
|
|
||||||
EVENT = "event"
|
|
||||||
CONFIRM = "confirm"
|
|
||||||
REQUEST = "request"
|
|
||||||
RESPONSE = "response"
|
|
||||||
KEEPALIVE = "keepalive"
|
|
||||||
ENCRYPT_KEY_REQ = "encryptKeyReq"
|
|
||||||
ENCRYPT_KEY_RESP = "encryptKeyResp"
|
|
||||||
|
|
||||||
# Station modes
|
|
||||||
STATION_MODE_OPERATION = 1
|
|
||||||
STATION_MODE_DEBUG = 2
|
|
||||||
STATION_MODE_MAINTENANCE = 3
|
|
||||||
|
|
||||||
# Device states
|
|
||||||
DEVICE_STATE_UNKNOWN = 0
|
|
||||||
DEVICE_STATE_NORMAL = 1
|
|
||||||
DEVICE_STATE_ALARM = 2
|
|
||||||
|
|
||||||
# Charging states
|
|
||||||
CHARGING_STATE_STANDBY = 1
|
|
||||||
CHARGING_STATE_CHARGING = 2
|
|
||||||
CHARGING_STATE_COMPLETE = 3
|
|
||||||
CHARGING_STATE_FAULT = 4
|
|
||||||
CHARGING_STATE_OFFLINE = 5
|
|
||||||
|
|
||||||
# Swap states
|
|
||||||
SWAP_STATE_NOT_STARTED = 1
|
|
||||||
SWAP_STATE_STARTED = 2
|
|
||||||
SWAP_STATE_PAUSED = 3
|
|
||||||
SWAP_STATE_RESUMED = 4
|
|
||||||
SWAP_STATE_TERMINATED = 5
|
|
||||||
SWAP_STATE_COMPLETED = 6
|
|
||||||
|
|
||||||
# Rate types
|
|
||||||
RATE_TYPE_SHARP = 1
|
|
||||||
RATE_TYPE_PEAK = 2
|
|
||||||
RATE_TYPE_FLAT = 3
|
|
||||||
RATE_TYPE_VALLEY = 4
|
|
||||||
|
|
||||||
# Lane types
|
|
||||||
LANE_TYPE_SINGLE = 1
|
|
||||||
LANE_TYPE_DOUBLE = 2
|
|
||||||
|
|
||||||
# Error codes
|
|
||||||
ERROR_SUCCESS = 1
|
|
||||||
ERROR_FAILURE = 2
|
|
||||||
|
|
||||||
# Timeouts (in seconds)
|
|
||||||
KEEPALIVE_INTERVAL = 30
|
|
||||||
EVENT_RETRY_INTERVAL = 15
|
|
||||||
COMMUNICATION_TIMEOUT = 90 # 3 * KEEPALIVE_INTERVAL
|
|
@ -1,68 +0,0 @@
|
|||||||
"""
|
|
||||||
Constants used throughout the MQTT protocol implementation
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Protocol version
|
|
||||||
PROTOCOL_VERSION = "V2.6.2"
|
|
||||||
|
|
||||||
# Topic related constants
|
|
||||||
TOPIC_PREFIX = "HCMS" # Heavy truck Charging Management System
|
|
||||||
TOPIC_SEPARATOR = "/"
|
|
||||||
|
|
||||||
# Message directions
|
|
||||||
M2S = "M2S" # Master to Slave (Cloud Platform to Station)
|
|
||||||
S2M = "S2M" # Slave to Master (Station to Cloud Platform)
|
|
||||||
|
|
||||||
# Message types
|
|
||||||
STATE = "state"
|
|
||||||
EVENT = "event"
|
|
||||||
CONFIRM = "confirm"
|
|
||||||
REQUEST = "request"
|
|
||||||
RESPONSE = "response"
|
|
||||||
KEEPALIVE = "keepalive"
|
|
||||||
ENCRYPT_KEY_REQ = "encryptKeyReq"
|
|
||||||
ENCRYPT_KEY_RESP = "encryptKeyResp"
|
|
||||||
|
|
||||||
# Station modes
|
|
||||||
STATION_MODE_OPERATION = 1
|
|
||||||
STATION_MODE_DEBUG = 2
|
|
||||||
STATION_MODE_MAINTENANCE = 3
|
|
||||||
|
|
||||||
# Device states
|
|
||||||
DEVICE_STATE_UNKNOWN = 0
|
|
||||||
DEVICE_STATE_NORMAL = 1
|
|
||||||
DEVICE_STATE_ALARM = 2
|
|
||||||
|
|
||||||
# Charging states
|
|
||||||
CHARGING_STATE_STANDBY = 1
|
|
||||||
CHARGING_STATE_CHARGING = 2
|
|
||||||
CHARGING_STATE_COMPLETE = 3
|
|
||||||
CHARGING_STATE_FAULT = 4
|
|
||||||
CHARGING_STATE_OFFLINE = 5
|
|
||||||
|
|
||||||
# Swap states
|
|
||||||
SWAP_STATE_NOT_STARTED = 1
|
|
||||||
SWAP_STATE_STARTED = 2
|
|
||||||
SWAP_STATE_PAUSED = 3
|
|
||||||
SWAP_STATE_RESUMED = 4
|
|
||||||
SWAP_STATE_TERMINATED = 5
|
|
||||||
SWAP_STATE_COMPLETED = 6
|
|
||||||
|
|
||||||
# Rate types
|
|
||||||
RATE_TYPE_SHARP = 1
|
|
||||||
RATE_TYPE_PEAK = 2
|
|
||||||
RATE_TYPE_FLAT = 3
|
|
||||||
RATE_TYPE_VALLEY = 4
|
|
||||||
|
|
||||||
# Lane types
|
|
||||||
LANE_TYPE_SINGLE = 1
|
|
||||||
LANE_TYPE_DOUBLE = 2
|
|
||||||
|
|
||||||
# Error codes
|
|
||||||
ERROR_SUCCESS = 1
|
|
||||||
ERROR_FAILURE = 2
|
|
||||||
|
|
||||||
# Timeouts (in seconds)
|
|
||||||
KEEPALIVE_INTERVAL = 30
|
|
||||||
EVENT_RETRY_INTERVAL = 15
|
|
||||||
COMMUNICATION_TIMEOUT = 90 # 3 * KEEPALIVE_INTERVAL
|
|
@ -1,6 +1,3 @@
|
|||||||
pycryptodome>=3.15.0
|
# requirements.txt
|
||||||
pytest>=7.0.0
|
paho-mqtt==1.6.1
|
||||||
paho-mqtt>=1.6.1
|
cryptography==41.0.7
|
||||||
dataclasses>=0.6
|
|
||||||
typing-extensions>=4.0.0
|
|
||||||
python-dateutil>=2.8.2
|
|
@ -1,15 +0,0 @@
|
|||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="mqtt_protocol",
|
|
||||||
version="1.0.0",
|
|
||||||
packages=find_packages(),
|
|
||||||
install_requires=[
|
|
||||||
"pycryptodome>=3.15.0",
|
|
||||||
],
|
|
||||||
author="Your Name",
|
|
||||||
author_email="your.email@example.com",
|
|
||||||
description="MQTT Protocol Parser for Battery Swap Station",
|
|
||||||
long_description=open("README.md").read(),
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
)
|
|
79
battery_swap_station/src/event.py
Normal file
79
battery_swap_station/src/event.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# MQTT Broker 配置
|
||||||
|
BROKER = "192.168.8.139" # 替换为你的 MQTT Broker 地址
|
||||||
|
PORT = 1883
|
||||||
|
SITE_CODE = "124127"
|
||||||
|
TOPIC_EVENT = f"HCMS/{SITE_CODE}/S2M/event"
|
||||||
|
TOPIC_KEEPALIVE_IN = f"HCMS/{SITE_CODE}/S2M/keepalive"
|
||||||
|
TOPIC_KEEPALIVE_OUT = f"HCMS/{SITE_CODE}/M2S/keepalive"
|
||||||
|
TOPIC_CONFIRM = f"HCMS/{SITE_CODE}/M2S/confirm"
|
||||||
|
|
||||||
|
|
||||||
|
# 连接回调
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
print(f"Connected to MQTT Broker with result code {rc}")
|
||||||
|
client.subscribe([(TOPIC_EVENT, 0), (TOPIC_KEEPALIVE_IN, 0)])
|
||||||
|
print(f"Subscribed to {TOPIC_EVENT} and {TOPIC_KEEPALIVE_IN}")
|
||||||
|
|
||||||
|
|
||||||
|
# 消息处理回调
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
payload = msg.payload.decode()
|
||||||
|
topic = msg.topic
|
||||||
|
print(f"Received: {topic} - {payload}")
|
||||||
|
|
||||||
|
if topic == TOPIC_KEEPALIVE_IN:
|
||||||
|
# 处理心跳报文
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
client.publish(TOPIC_KEEPALIVE_OUT, current_time, qos=0)
|
||||||
|
print(f"Sent keepalive reply: {TOPIC_KEEPALIVE_OUT} - {current_time}")
|
||||||
|
|
||||||
|
elif topic == TOPIC_EVENT:
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
header = data.get("header", {})
|
||||||
|
data_body = data.get("dataBody", {})
|
||||||
|
|
||||||
|
function = header.get("function", "unknown")
|
||||||
|
index = header.get("index", 0)
|
||||||
|
order_sn = data_body.get("orderSn", "unknown")
|
||||||
|
|
||||||
|
# 构造确认报文
|
||||||
|
confirm_payload = {
|
||||||
|
"header": {
|
||||||
|
"version": "V2.0",
|
||||||
|
"timeStamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"index": index,
|
||||||
|
"function": f"{function}Conf"
|
||||||
|
},
|
||||||
|
"dataBody": {
|
||||||
|
"orderSn": order_sn,
|
||||||
|
"result": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送确认
|
||||||
|
client.publish(TOPIC_CONFIRM, json.dumps(confirm_payload), qos=0)
|
||||||
|
print(f"Sent confirmation: {TOPIC_CONFIRM} - {json.dumps(confirm_payload)}")
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Failed to parse JSON payload")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing message: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# 主程序
|
||||||
|
def main():
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
client.connect(BROKER, PORT, 60)
|
||||||
|
client.loop_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
136
battery_swap_station/src/model.py
Normal file
136
battery_swap_station/src/model.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
BROKER = "192.168.8.139"
|
||||||
|
PORT = 1883
|
||||||
|
SITE_CODE = "124127"
|
||||||
|
TOPIC_REQUEST = f"HCMS/{SITE_CODE}/M2S/request"
|
||||||
|
TOPIC_RESPONSE = f"HCMS/{SITE_CODE}/S2M/response"
|
||||||
|
|
||||||
|
# HTTP 接口配置(示例)
|
||||||
|
RATE_API_URL = "http://example.com/api/rate_model" # 替换为实际接口地址
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_rate_model_from_api():
|
||||||
|
try:
|
||||||
|
response = requests.get(RATE_API_URL)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return {
|
||||||
|
"rateModelID": data.get("rateModelID", "default_id"),
|
||||||
|
"rateList": data.get("rateList", []),
|
||||||
|
"rateDetailsList": data.get("rateDetailsList", [])
|
||||||
|
}
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Failed to fetch rate model from API: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# 连接回调
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected to MQTT Broker successfully")
|
||||||
|
client.subscribe(TOPIC_RESPONSE)
|
||||||
|
print(f"Subscribed to {TOPIC_RESPONSE}")
|
||||||
|
else:
|
||||||
|
print(f"Connection failed with code {rc}")
|
||||||
|
|
||||||
|
|
||||||
|
# 消息处理回调
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
payload = msg.payload.decode()
|
||||||
|
topic = msg.topic
|
||||||
|
print(f"Received: {topic} - {payload}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
header = data.get("header", {})
|
||||||
|
data_body = data.get("dataBody", {})
|
||||||
|
|
||||||
|
function = header.get("function", "unknown")
|
||||||
|
if function == "rateModeSyncResp":
|
||||||
|
result = data_body.get("result", 2)
|
||||||
|
reason = data_body.get("reason", "No reason provided")
|
||||||
|
print(f"Rate Mode Sync Response: Result={result}, Reason={reason}")
|
||||||
|
|
||||||
|
elif function == "checkRateModelResp":
|
||||||
|
rate_model_id = data_body.get("rateModelID", "unknown")
|
||||||
|
rate_list = data_body.get("rateList", [])
|
||||||
|
rate_details_list = data_body.get("rateDetailsList", [])
|
||||||
|
print(f"Rate Model Query Response: ID={rate_model_id}")
|
||||||
|
print(f"Rate List: {json.dumps(rate_list, indent=2)}")
|
||||||
|
print(f"Rate Details List: {json.dumps(rate_details_list, indent=2)}")
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Failed to parse JSON payload")
|
||||||
|
|
||||||
|
|
||||||
|
# 发送费率模型同步请求
|
||||||
|
def send_rate_model_sync(client, rate_model_id, rate_list, rate_details_list):
|
||||||
|
request_payload = {
|
||||||
|
"header": {
|
||||||
|
"version": "V2.0",
|
||||||
|
"timeStamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"index": 1,
|
||||||
|
"function": "rateModeSyncReq"
|
||||||
|
},
|
||||||
|
"dataBody": {
|
||||||
|
"rateModelID": rate_model_id,
|
||||||
|
"rateList": rate_list,
|
||||||
|
"rateDetailsList": rate_details_list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.publish(TOPIC_REQUEST, json.dumps(request_payload), qos=0)
|
||||||
|
print(f"Sent rate model sync request: {json.dumps(request_payload)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 发送费率模型查询请求
|
||||||
|
def send_rate_model_query(client, rate_model_id):
|
||||||
|
request_payload = {
|
||||||
|
"header": {
|
||||||
|
"version": "V2.0",
|
||||||
|
"timeStamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"index": 2,
|
||||||
|
"function": "checkRateModelReq"
|
||||||
|
},
|
||||||
|
"dataBody": {
|
||||||
|
"rateModelID": rate_model_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.publish(TOPIC_REQUEST, json.dumps(request_payload), qos=0)
|
||||||
|
print(f"Sent rate model query request: {json.dumps(request_payload)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 主程序
|
||||||
|
def main():
|
||||||
|
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.connect(BROKER, PORT, 60)
|
||||||
|
|
||||||
|
# 从 HTTP 接口获取费率数据
|
||||||
|
rate_data = fetch_rate_model_from_api()
|
||||||
|
if rate_data:
|
||||||
|
send_rate_model_sync(client, rate_data["rateModelID"],
|
||||||
|
rate_data["rateList"], rate_data["rateDetailsList"])
|
||||||
|
send_rate_model_query(client, rate_data["rateModelID"])
|
||||||
|
else:
|
||||||
|
print("Using default rate data due to API failure")
|
||||||
|
rate_model_id = "default_id"
|
||||||
|
rate_list = [{"rateType": 1, "electPrice": 1.0, "servicePrice": 0.3}]
|
||||||
|
rate_details_list = [{"rateType": 1, "startTime": "2025-03-05 00:00:00", "stopTime": "2025-03-05 23:59:59"}]
|
||||||
|
send_rate_model_sync(client, rate_model_id, rate_list, rate_details_list)
|
||||||
|
send_rate_model_query(client, rate_model_id)
|
||||||
|
|
||||||
|
client.loop_forever()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to {BROKER}:{PORT}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
76
battery_swap_station/src/state.py
Normal file
76
battery_swap_station/src/state.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# MQTT Broker 配置
|
||||||
|
BROKER = "192.168.8.139" # 替换为你的 MQTT Broker 地址
|
||||||
|
PORT = 1883
|
||||||
|
SITE_CODE = "124127"
|
||||||
|
TOPIC_STATE = f"HCMS/{SITE_CODE}/S2M/state"
|
||||||
|
TOPIC_KEEPALIVE_IN = f"HCMS/{SITE_CODE}/S2M/keepalive"
|
||||||
|
TOPIC_KEEPALIVE_OUT = f"HCMS/{SITE_CODE}/M2S/keepalive"
|
||||||
|
TOPIC_REQUEST = f"HCMS/{SITE_CODE}/M2S/request"
|
||||||
|
|
||||||
|
|
||||||
|
# 连接回调
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
print(f"Connected to MQTT Broker with result code {rc}")
|
||||||
|
client.subscribe([(TOPIC_STATE, 0), (TOPIC_KEEPALIVE_IN, 0)])
|
||||||
|
print(f"Subscribed to {TOPIC_STATE} and {TOPIC_KEEPALIVE_IN}")
|
||||||
|
|
||||||
|
|
||||||
|
# 消息处理回调
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
payload = msg.payload.decode()
|
||||||
|
topic = msg.topic
|
||||||
|
print(f"Received: {topic} - {payload}")
|
||||||
|
|
||||||
|
if topic == TOPIC_KEEPALIVE_IN:
|
||||||
|
# 处理心跳报文
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
client.publish(TOPIC_KEEPALIVE_OUT, current_time, qos=0)
|
||||||
|
print(f"Sent keepalive reply: {TOPIC_KEEPALIVE_OUT} - {current_time}")
|
||||||
|
|
||||||
|
elif topic == TOPIC_STATE:
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
header = data.get("header", {})
|
||||||
|
function = header.get("function", "unknown")
|
||||||
|
timestamp = header.get("timeStamp", "unknown")
|
||||||
|
index = header.get("index", 0)
|
||||||
|
print(f"State - Function: {function}, TimeStamp: {timestamp}, Index: {index}")
|
||||||
|
# 状态类报文无需回复,此处仅记录
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Failed to parse JSON payload")
|
||||||
|
|
||||||
|
|
||||||
|
# 发送状态召唤请求(可选)
|
||||||
|
def send_state_call(client, function):
|
||||||
|
request_payload = {
|
||||||
|
"header": {
|
||||||
|
"version": "V2.0",
|
||||||
|
"timeStamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"index": 1, # 可递增管理
|
||||||
|
"function": f"{function}Call"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.publish(TOPIC_REQUEST, json.dumps(request_payload), qos=0)
|
||||||
|
print(f"Sent state call request: {function}Call")
|
||||||
|
|
||||||
|
|
||||||
|
# 主程序
|
||||||
|
def main():
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
client.connect(BROKER, PORT, 60)
|
||||||
|
|
||||||
|
# 示例:发送 swapState 召唤请求(可选)
|
||||||
|
# send_state_call(client, "swapState")
|
||||||
|
|
||||||
|
client.loop_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,109 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for state messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from mqtt_protocol import (
|
|
||||||
StationStateMessage,
|
|
||||||
StationInfo,
|
|
||||||
RobotStateMessage
|
|
||||||
)
|
|
||||||
from mqtt_protocol.utils.constants import (
|
|
||||||
STATION_MODE_OPERATION,
|
|
||||||
DEVICE_STATE_NORMAL,
|
|
||||||
DEVICE_STATE_ALARM
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_station_state_message():
|
|
||||||
# Test creation
|
|
||||||
state_msg = StationStateMessage.create(
|
|
||||||
state=STATION_MODE_OPERATION,
|
|
||||||
smoke="1,1,1,1",
|
|
||||||
fire=DEVICE_STATE_NORMAL,
|
|
||||||
temp=25,
|
|
||||||
humid=60,
|
|
||||||
totalElect=1000.5
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test serialization
|
|
||||||
json_data = state_msg.to_json()
|
|
||||||
assert isinstance(json_data, str)
|
|
||||||
|
|
||||||
# Test deserialization
|
|
||||||
parsed_msg = StationStateMessage.from_json(json_data)
|
|
||||||
assert parsed_msg.state == STATION_MODE_OPERATION
|
|
||||||
assert parsed_msg.temp == 25
|
|
||||||
assert parsed_msg.humid == 60
|
|
||||||
assert parsed_msg.totalElect == 1000.5
|
|
||||||
|
|
||||||
|
|
||||||
def test_station_info():
|
|
||||||
info = StationInfo(
|
|
||||||
name="Test Station",
|
|
||||||
stationID="100001",
|
|
||||||
version="V2.6.2",
|
|
||||||
startDate="2024-02-24",
|
|
||||||
robotNum=2,
|
|
||||||
robotID="1234567890123456,1234567890123457",
|
|
||||||
chgNum=8,
|
|
||||||
chgID="CHG001,CHG002,CHG003,CHG004,CHG005,CHG006,CHG007,CHG008",
|
|
||||||
laneType=1,
|
|
||||||
rateModelID="RATE001",
|
|
||||||
chgVersion="V1.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert info.name == "Test Station"
|
|
||||||
assert info.stationID == "100001"
|
|
||||||
assert info.robotNum == 2
|
|
||||||
assert info.chgNum == 8
|
|
||||||
|
|
||||||
|
|
||||||
def test_station_state_validation():
|
|
||||||
# Test invalid state
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
StationStateMessage.create(
|
|
||||||
state=999, # Invalid state
|
|
||||||
smoke="1,1,1,1",
|
|
||||||
fire=DEVICE_STATE_NORMAL,
|
|
||||||
temp=25,
|
|
||||||
humid=60,
|
|
||||||
totalElect=1000.5
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test invalid smoke detector format
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
StationStateMessage.create(
|
|
||||||
state=STATION_MODE_OPERATION,
|
|
||||||
smoke="invalid",
|
|
||||||
fire=DEVICE_STATE_NORMAL,
|
|
||||||
temp=25,
|
|
||||||
humid=60,
|
|
||||||
totalElect=1000.5
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_robot_state():
|
|
||||||
robot_msg = RobotStateMessage(
|
|
||||||
robotID="1234567890123456",
|
|
||||||
state=1,
|
|
||||||
fault=None,
|
|
||||||
runMode=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert robot_msg.robotID == "1234567890123456"
|
|
||||||
assert robot_msg.state == 1
|
|
||||||
assert robot_msg.fault is None
|
|
||||||
assert robot_msg.runMode == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_robot_state_with_fault():
|
|
||||||
robot_msg = RobotStateMessage(
|
|
||||||
robotID="1234567890123456",
|
|
||||||
state=3, # Fault state
|
|
||||||
fault="ERROR001,ERROR002",
|
|
||||||
runMode=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert robot_msg.state == 3
|
|
||||||
assert robot_msg.fault == "ERROR001,ERROR002"
|
|
@ -31,7 +31,7 @@ server = ChargingPileProxyServer(
|
|||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
python
|
||||||
```
|
```
|
||||||
|
|
||||||
## 日志
|
## 日志
|
||||||
|
Loading…
x
Reference in New Issue
Block a user