no message
This commit is contained in:
commit
b94aca789f
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
main.py
|
11
.idea/analysis.iml
generated
Normal file
11
.idea/analysis.iml
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/charging_pile_proxy" isTestSource="false" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/analysis.iml" filepath="$PROJECT_DIR$/.idea/analysis.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
44
charging_pile_proxy/README.md
Normal file
44
charging_pile_proxy/README.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# 充电桩代理服务器
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
本项目是一个用于处理充电桩通信协议的代理服务器,支持多种命令解析和转发。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
- 支持01H、02H、03H、07H等充电桩通信命令
|
||||||
|
- 实时日志记录
|
||||||
|
- MQTT消息发布
|
||||||
|
- 灵活的配置选项
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
- Python 3.7+
|
||||||
|
- paho-mqtt
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
可以在 `main.py` 中自定义服务器配置:
|
||||||
|
```python
|
||||||
|
server = ChargingPileProxyServer(
|
||||||
|
listen_host='0.0.0.0', # 监听地址
|
||||||
|
listen_port=52461, # 监听端口
|
||||||
|
forward_host='139.9.209.227', # 转发目标地址
|
||||||
|
forward_port=52461 # 转发目标端口
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志
|
||||||
|
日志将记录在 `charging_pile_proxy.log` 文件中,并同时输出到控制台。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
[待添加]
|
||||||
|
|
||||||
|
## 作者
|
||||||
|
[赵子逸/玄驹易维]
|
0
charging_pile_proxy/commands/__init__.py
Normal file
0
charging_pile_proxy/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
279
charging_pile_proxy/commands/command_02.py
Normal file
279
charging_pile_proxy/commands/command_02.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Command02:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x02 # 回复命令码02H
|
||||||
|
self.qr_fixed = "https://platform.enneagon.cn/ScanCharging?connectorCode="
|
||||||
|
|
||||||
|
def parse_pile_id(self, pile_id_bytes):
|
||||||
|
"""解析桩号"""
|
||||||
|
try:
|
||||||
|
vendor_id = struct.unpack("<H", pile_id_bytes[0:2])[0] # 运营商编号
|
||||||
|
gun_info = pile_id_bytes[2] # 枪数量信息
|
||||||
|
|
||||||
|
if gun_info <= 31:
|
||||||
|
gun_type = "交流"
|
||||||
|
gun_count = gun_info
|
||||||
|
elif 51 <= gun_info <= 81:
|
||||||
|
gun_type = "直流"
|
||||||
|
gun_count = gun_info - 50
|
||||||
|
else:
|
||||||
|
gun_type = "未知"
|
||||||
|
gun_count = gun_info
|
||||||
|
|
||||||
|
site_id = int.from_bytes(pile_id_bytes[3:6], 'little') # 站点编号
|
||||||
|
addr_in_site = struct.unpack("<H", pile_id_bytes[6:8])[0] # 站内桩地址
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vendor_id": f"{vendor_id:04d}",
|
||||||
|
"gun_type": gun_type,
|
||||||
|
"gun_count": gun_count,
|
||||||
|
"site_id": f"{site_id:06d}",
|
||||||
|
"addr_in_site": f"{addr_in_site:04d}"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Parse pile ID failed: {str(e)}")
|
||||||
|
print(f"解析桩号失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_frame(self, data):
|
||||||
|
"""验证帧格式"""
|
||||||
|
try:
|
||||||
|
print(f"\n验证帧格式:")
|
||||||
|
print(f"数据内容: {data.hex()}")
|
||||||
|
print(f"数据长度: {len(data)}字节")
|
||||||
|
|
||||||
|
# 1. 基本长度检查
|
||||||
|
if len(data) < 14:
|
||||||
|
print("数据长度不足14字节,无效")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. 检查帧起始标志
|
||||||
|
if data[0:2] != b'JX':
|
||||||
|
print("帧起始标志不是'JX',无效")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3. 获取并检查数据域长度
|
||||||
|
data_len = struct.unpack("<H", data[12:14])[0]
|
||||||
|
print(f"数据域长度字段值: {data_len}")
|
||||||
|
|
||||||
|
# 4. 计算并检查总长度
|
||||||
|
expected_len = 16 + data_len # 固定部分(14) + 数据域 + 校验码(1)
|
||||||
|
print(f"期望总长度: {expected_len}")
|
||||||
|
print(f"实际长度: {len(data)}")
|
||||||
|
|
||||||
|
if len(data) != expected_len:
|
||||||
|
print("数据总长度不匹配")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 5. 验证校验码
|
||||||
|
check_data = data[2:-1] # 从命令字节到校验码前的数据
|
||||||
|
calculated_check = 0
|
||||||
|
for b in check_data:
|
||||||
|
calculated_check ^= b
|
||||||
|
|
||||||
|
received_check = data[-1]
|
||||||
|
print(f"计算得到的校验码: {calculated_check:02X}")
|
||||||
|
print(f"接收到的校验码: {received_check:02X}")
|
||||||
|
|
||||||
|
if calculated_check != received_check:
|
||||||
|
print("校验码不匹配")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("帧格式验证通过")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"帧格式验证出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_01h(self, data):
|
||||||
|
"""解析01H命令数据"""
|
||||||
|
try:
|
||||||
|
print("\n开始解析01H命令...")
|
||||||
|
|
||||||
|
if not self.validate_frame(data):
|
||||||
|
raise ValueError("帧格式验证失败")
|
||||||
|
|
||||||
|
command = data[2]
|
||||||
|
pile_id = data[3:11]
|
||||||
|
encrypt_mode = data[11]
|
||||||
|
data_len = struct.unpack("<H", data[12:14])[0]
|
||||||
|
data_field = data[14:14 + data_len]
|
||||||
|
|
||||||
|
# 解析时间标识
|
||||||
|
time_bytes = data_field[0:6]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 解析密钥版本和校验密文
|
||||||
|
key_version = struct.unpack("<H", data_field[6:8])[0]
|
||||||
|
check_text = data_field[8:16]
|
||||||
|
|
||||||
|
# 解析桩号
|
||||||
|
pile_info = self.parse_pile_id(pile_id)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"command": command,
|
||||||
|
"pile_id": pile_id,
|
||||||
|
"pile_info": pile_info,
|
||||||
|
"encrypt_mode": encrypt_mode,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"key_version": key_version,
|
||||||
|
"check_text": check_text
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n解析结果:")
|
||||||
|
print(f"命令码: {command:02X}")
|
||||||
|
print(f"桩号信息: {pile_info}")
|
||||||
|
print(f"加密方式: {encrypt_mode:02X}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"密钥版本: {key_version}")
|
||||||
|
print(f"校验密文: {check_text.hex()}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析01H命令失败: {str(e)}")
|
||||||
|
print(f"解析失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_02h_response(self, pile_id, allow=True, reject_reason=0):
|
||||||
|
"""构建02H响应命令"""
|
||||||
|
try:
|
||||||
|
print("\n构建02H响应...")
|
||||||
|
|
||||||
|
frame = bytearray()
|
||||||
|
frame.extend(b'JX') # 帧起始标志
|
||||||
|
frame.append(self.command) # 命令
|
||||||
|
frame.extend(pile_id) # 桩号
|
||||||
|
frame.append(0x01) # 数据加密方式
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
|
||||||
|
# 时间标识
|
||||||
|
now = datetime.now()
|
||||||
|
data.extend(struct.pack("<BBBBBB",
|
||||||
|
now.year - 2000, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second))
|
||||||
|
|
||||||
|
# 请求结果
|
||||||
|
data.append(0x01 if allow else 0x02)
|
||||||
|
|
||||||
|
# 拒绝原因
|
||||||
|
data.append(reject_reason)
|
||||||
|
|
||||||
|
if allow:
|
||||||
|
# 二维码固定段
|
||||||
|
data.extend(self.qr_fixed.ljust(100, '\x00').encode())
|
||||||
|
|
||||||
|
# 二维码枪号段数量
|
||||||
|
data.append(0x02)
|
||||||
|
|
||||||
|
# 二维码枪号段1和2
|
||||||
|
pile_id_str = ''.join([f"{b:02X}" for b in pile_id])
|
||||||
|
gun1 = f"{pile_id_str}001"
|
||||||
|
gun2 = f"{pile_id_str}002"
|
||||||
|
|
||||||
|
data.extend(gun1.ljust(20, '\x00').encode())
|
||||||
|
data.extend(gun2.ljust(20, '\x00').encode())
|
||||||
|
|
||||||
|
# 数据域长度
|
||||||
|
frame.extend(struct.pack("<H", len(data)))
|
||||||
|
|
||||||
|
# 数据域
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验码
|
||||||
|
check = 0
|
||||||
|
for b in frame[2:]:
|
||||||
|
check ^= b
|
||||||
|
frame.append(check)
|
||||||
|
|
||||||
|
print("响应数据构建成功:")
|
||||||
|
print(f"数据内容: {frame.hex()}")
|
||||||
|
print(f"数据长度: {len(frame)}字节")
|
||||||
|
|
||||||
|
return bytes(frame)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"构建02H响应失败: {str(e)}")
|
||||||
|
print(f"构建响应失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_and_respond(self, received_data, sock):
|
||||||
|
"""处理收到的01H命令并回复02H"""
|
||||||
|
try:
|
||||||
|
print("\n处理01H命令并生成响应...")
|
||||||
|
|
||||||
|
# 解析收到的01H命令
|
||||||
|
parsed = self.parse_01h(received_data)
|
||||||
|
if not parsed:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 构建02H响应
|
||||||
|
allow = True # 这里可以根据业务逻辑判断是否允许连接
|
||||||
|
reject_reason = 0
|
||||||
|
|
||||||
|
response = self.build_02h_response(parsed["pile_id"], allow, reject_reason)
|
||||||
|
if not response:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 发送响应
|
||||||
|
if hasattr(sock, 'send'):
|
||||||
|
sock.send(response)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理和响应失败: {str(e)}")
|
||||||
|
print(f"处理失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_command():
|
||||||
|
"""测试函数"""
|
||||||
|
print("开始测试01H/02H命令处理...")
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
# logging.basicConfig(
|
||||||
|
# filename='command_response_02h.log',
|
||||||
|
# level=logging.INFO,
|
||||||
|
# format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
# encoding='utf-8'
|
||||||
|
# )
|
||||||
|
|
||||||
|
# 创建响应处理器
|
||||||
|
handler = Command02()
|
||||||
|
|
||||||
|
# 测试数据 - 使用实际收到的数据
|
||||||
|
test_data = bytes.fromhex("4A5801031767631136065701100019010909371501000000000000000000004D")
|
||||||
|
|
||||||
|
print("\n测试数据:")
|
||||||
|
print(f"十六进制: {test_data.hex()}")
|
||||||
|
print(f"长度: {len(test_data)}字节")
|
||||||
|
|
||||||
|
# 创建模拟socket
|
||||||
|
class MockSocket:
|
||||||
|
def send(self, data):
|
||||||
|
print(f"\n模拟发送响应数据:")
|
||||||
|
print(f"数据内容: {data.hex()}")
|
||||||
|
print(f"数据长度: {len(data)}字节")
|
||||||
|
|
||||||
|
mock_sock = MockSocket()
|
||||||
|
|
||||||
|
# 测试完整处理流程
|
||||||
|
result = handler.process_and_respond(test_data, mock_sock)
|
||||||
|
print(f"\n最终处理结果: {'成功' if result else '失败'}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_command()
|
137
charging_pile_proxy/commands/command_03.py
Normal file
137
charging_pile_proxy/commands/command_03.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Command03:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x03 # 03H命令码
|
||||||
|
|
||||||
|
def parse_03h(self, data):
|
||||||
|
"""
|
||||||
|
解析03H登录信息命令
|
||||||
|
|
||||||
|
:param data: 完整的03H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x03:
|
||||||
|
logging.warning("03H命令帧格式不正确")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 解析桩型号 (16字节ASCII)
|
||||||
|
pile_type = data[6:22].decode('ascii').rstrip('\x00')
|
||||||
|
|
||||||
|
# 解析硬件版本 (2字节压缩BCD)
|
||||||
|
hw_version_bytes = data[22:24]
|
||||||
|
hw_version_major = hw_version_bytes[0] >> 4
|
||||||
|
hw_version_minor = hw_version_bytes[0] & 0x0F
|
||||||
|
hw_version_patch = hw_version_bytes[1] >> 4
|
||||||
|
hw_version = f"{hw_version_major}.{hw_version_minor}.{hw_version_patch}"
|
||||||
|
|
||||||
|
# 解析软件版本 (2字节压缩BCD)
|
||||||
|
sw_version_bytes = data[24:26]
|
||||||
|
sw_version_major = sw_version_bytes[0] >> 4
|
||||||
|
sw_version_minor = sw_version_bytes[0] & 0x0F
|
||||||
|
sw_version_patch = sw_version_bytes[1] >> 4
|
||||||
|
sw_version = f"{sw_version_major}.{sw_version_minor}.{sw_version_patch}"
|
||||||
|
|
||||||
|
# 解析次级单元硬件版本
|
||||||
|
sub_hw_version_bytes = data[26:28]
|
||||||
|
sub_hw_version_major = sub_hw_version_bytes[0] >> 4
|
||||||
|
sub_hw_version_minor = sub_hw_version_bytes[0] & 0x0F
|
||||||
|
sub_hw_version_patch = sub_hw_version_bytes[1] >> 4
|
||||||
|
sub_hw_version = f"{sub_hw_version_major}.{sub_hw_version_minor}.{sub_hw_version_patch}"
|
||||||
|
|
||||||
|
# 解析次级单元软件版本
|
||||||
|
sub_sw_version_bytes = data[28:30]
|
||||||
|
sub_sw_version_major = sub_sw_version_bytes[0] >> 4
|
||||||
|
sub_sw_version_minor = sub_sw_version_bytes[0] & 0x0F
|
||||||
|
sub_sw_version_patch = sub_sw_version_bytes[1] >> 4
|
||||||
|
sub_sw_version = f"{sub_sw_version_major}.{sub_sw_version_minor}.{sub_sw_version_patch}"
|
||||||
|
|
||||||
|
# 解析直流模块类型
|
||||||
|
dc_module_type = data[30]
|
||||||
|
|
||||||
|
# 解析直流模块总数
|
||||||
|
dc_module_count = data[31]
|
||||||
|
|
||||||
|
# 解析直流模块单模块功率
|
||||||
|
dc_module_power = data[32]
|
||||||
|
|
||||||
|
# 解析计费模型版本
|
||||||
|
fee_model_version = struct.unpack("<H", data[33:35])[0]
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n03H命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"桩型号: {pile_type}")
|
||||||
|
print(f"硬件版本: {hw_version}")
|
||||||
|
print(f"软件版本: {sw_version}")
|
||||||
|
print(f"次级单元硬件版本: {sub_hw_version}")
|
||||||
|
print(f"次级单元软件版本: {sub_sw_version}")
|
||||||
|
print(f"直流模块类型: {dc_module_type}")
|
||||||
|
print(f"直流模块总数: {dc_module_count}")
|
||||||
|
print(f"直流模块单模块功率: {dc_module_power}kW")
|
||||||
|
print(f"计费模型版本: {fee_model_version}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"pile_type": pile_type,
|
||||||
|
"hw_version": hw_version,
|
||||||
|
"sw_version": sw_version,
|
||||||
|
"sub_hw_version": sub_hw_version,
|
||||||
|
"sub_sw_version": sub_sw_version,
|
||||||
|
"dc_module_type": dc_module_type,
|
||||||
|
"dc_module_count": dc_module_count,
|
||||||
|
"dc_module_power": dc_module_power,
|
||||||
|
"fee_model_version": fee_model_version
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析03H命令失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_03h(self, data):
|
||||||
|
"""
|
||||||
|
处理03H登录信息命令
|
||||||
|
|
||||||
|
:param data: 完整的03H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_03h(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("03H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 可以在这里添加额外的处理逻辑,比如记录日志、更新状态等
|
||||||
|
logging.info(f"成功解析03H登录信息: 桩号 {parsed_data['pile_id']}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理03H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例报文
|
||||||
|
test_data = bytes.fromhex(
|
||||||
|
"4A 58 03 03 17 67 63 11 36 06 57 01 42 00 19 01 09 09 37 1B 41 49 4F 44 43 32 50 31 42 56 39 30 30 00 00 00 00 00 01 04 00 00 00 00 00 00 00 1B 00 00 00 19 01 09 09 37 1B 0F 00 05 19 00 19 00 0F 00 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 0C")
|
||||||
|
|
||||||
|
parser = Command03()
|
||||||
|
parser.process_03h(test_data)
|
96
charging_pile_proxy/commands/command_07.py
Normal file
96
charging_pile_proxy/commands/command_07.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Command07:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x07 # 07H命令码
|
||||||
|
|
||||||
|
def parse_07h(self, data):
|
||||||
|
"""
|
||||||
|
解析07H回复对时命令
|
||||||
|
|
||||||
|
:param data: 完整的07H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x07:
|
||||||
|
logging.warning("07H命令帧格式不正确")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 解析对时结果
|
||||||
|
time_sync_result = data[20]
|
||||||
|
time_sync_result_text = "成功" if time_sync_result == 0x01 else "失败"
|
||||||
|
|
||||||
|
# 解析失败原因(如果有)
|
||||||
|
failure_reason = data[21] if len(data) > 21 else 0x00
|
||||||
|
failure_reason_text = {
|
||||||
|
0x00: "无",
|
||||||
|
0x01: "数据格式异常"
|
||||||
|
}.get(failure_reason, "未知原因")
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n07H命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"对时结果: {time_sync_result_text}")
|
||||||
|
print(f"失败原因: {failure_reason_text}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"time_sync_result": time_sync_result,
|
||||||
|
"time_sync_result_text": time_sync_result_text,
|
||||||
|
"failure_reason": failure_reason,
|
||||||
|
"failure_reason_text": failure_reason_text
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析07H命令失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_07h(self, data):
|
||||||
|
"""
|
||||||
|
处理07H回复对时命令
|
||||||
|
|
||||||
|
:param data: 完整的07H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_07h(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("07H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录对时结果日志
|
||||||
|
if parsed_data['time_sync_result'] == 0x01:
|
||||||
|
logging.info(f"成功处理07H对时命令: 桩号 {parsed_data['pile_id']} 对时成功")
|
||||||
|
else:
|
||||||
|
logging.warning(
|
||||||
|
f"处理07H对时命令: 桩号 {parsed_data['pile_id']} 对时失败, 原因: {parsed_data['failure_reason_text']}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理07H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例报文
|
||||||
|
test_data = bytes.fromhex("4A 58 07 03 17 67 63 11 36 06 57 01 08 00 19 01 09 09 37 1F 01 00 59")
|
||||||
|
|
||||||
|
parser = Command07()
|
||||||
|
parser.process_07h(test_data)
|
154
charging_pile_proxy/commands/command_08.py
Normal file
154
charging_pile_proxy/commands/command_08.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Command08:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x08 # 08H命令码
|
||||||
|
|
||||||
|
def parse_08h(self, data):
|
||||||
|
"""
|
||||||
|
解析08H故障命令
|
||||||
|
|
||||||
|
:param data: 完整的08H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x08:
|
||||||
|
logging.warning("08H命令帧格式不正确")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 提取故障状态字节
|
||||||
|
current_index = 20
|
||||||
|
fault_states = {
|
||||||
|
# 从文档3.3.1节提取的故障状态映射
|
||||||
|
"汇流接触器": (data[current_index] & 0x01) != 0,
|
||||||
|
"输入接触器": (data[current_index] & 0x02) != 0,
|
||||||
|
"电表通讯": (data[current_index] & 0x04) != 0,
|
||||||
|
"读卡器通讯": (data[current_index] & 0x08) != 0,
|
||||||
|
"HMI通讯": (data[current_index] & 0x10) != 0,
|
||||||
|
"绝缘检测模块": (data[current_index] & 0x20) != 0,
|
||||||
|
"急停": (data[current_index] & 0x40) != 0,
|
||||||
|
"柜门打开": (data[current_index] & 0x80) != 0,
|
||||||
|
|
||||||
|
"温湿度传感器": (data[current_index + 1] & 0x01) != 0,
|
||||||
|
"风机": (data[current_index + 1] & 0x02) != 0,
|
||||||
|
"加热器": (data[current_index + 1] & 0x04) != 0,
|
||||||
|
"防雷器": (data[current_index + 1] & 0x08) != 0,
|
||||||
|
"控制板硬件": (data[current_index + 1] & 0x10) != 0,
|
||||||
|
"机柜过温": (data[current_index + 1] & 0x20) != 0,
|
||||||
|
"湿度过高": (data[current_index + 1] & 0x40) != 0,
|
||||||
|
"烟感报警": (data[current_index + 1] & 0x80) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取充电枪数量
|
||||||
|
current_index += 2
|
||||||
|
gun_count = data[current_index]
|
||||||
|
|
||||||
|
# 存储每个枪的故障状态
|
||||||
|
gun_faults = []
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
for i in range(gun_count):
|
||||||
|
# 每个枪有多个故障位
|
||||||
|
gun_fault_bytes = data[current_index:current_index + 3]
|
||||||
|
gun_faults.append({
|
||||||
|
"gun_index": i + 1,
|
||||||
|
"output_short_circuit": (gun_fault_bytes[0] & 0x01) != 0,
|
||||||
|
"output_contactor": (gun_fault_bytes[0] & 0x02) != 0,
|
||||||
|
"electronic_lock": (gun_fault_bytes[0] & 0x04) != 0,
|
||||||
|
"meter_communication": (gun_fault_bytes[0] & 0x08) != 0,
|
||||||
|
"charging_module_communication": (gun_fault_bytes[0] & 0x10) != 0,
|
||||||
|
"slave_control_communication": (gun_fault_bytes[0] & 0x20) != 0,
|
||||||
|
"insulation_module_communication": (gun_fault_bytes[0] & 0x40) != 0,
|
||||||
|
"insulation_fault": (gun_fault_bytes[0] & 0x80) != 0,
|
||||||
|
"module_overtemperature": (gun_fault_bytes[1] & 0x01) != 0,
|
||||||
|
"module_pfc": (gun_fault_bytes[1] & 0x02) != 0,
|
||||||
|
"module_fan": (gun_fault_bytes[1] & 0x04) != 0,
|
||||||
|
"module_address_conflict": (gun_fault_bytes[1] & 0x08) != 0,
|
||||||
|
"module_input_overvoltage": (gun_fault_bytes[1] & 0x10) != 0,
|
||||||
|
"module_input_undervoltage": (gun_fault_bytes[1] & 0x20) != 0,
|
||||||
|
"module_input_phase_loss": (gun_fault_bytes[1] & 0x40) != 0,
|
||||||
|
"module_other_fault": (gun_fault_bytes[1] & 0x80) != 0
|
||||||
|
})
|
||||||
|
current_index += 3
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n08H命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print("系统故障状态:")
|
||||||
|
for fault, state in fault_states.items():
|
||||||
|
if state:
|
||||||
|
print(f" {fault}: 故障")
|
||||||
|
|
||||||
|
print(f"充电枪数量: {gun_count}")
|
||||||
|
for gun_fault in gun_faults:
|
||||||
|
print(f"枪 {gun_fault['gun_index']} 故障状态:")
|
||||||
|
for fault, state in gun_fault.items():
|
||||||
|
if fault != "gun_index" and state:
|
||||||
|
print(f" {fault}: 故障")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"system_faults": {k: v for k, v in fault_states.items() if v},
|
||||||
|
"gun_count": gun_count,
|
||||||
|
"gun_faults": gun_faults
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析08H命令失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_08h(self, data):
|
||||||
|
"""
|
||||||
|
处理08H故障命令
|
||||||
|
|
||||||
|
:param data: 完整的08H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_08h(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("08H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录故障信息日志
|
||||||
|
fault_summary = f"桩号 {parsed_data['pile_id']} 报告故障: "
|
||||||
|
system_faults = list(parsed_data['system_faults'].keys())
|
||||||
|
gun_faults_count = len(parsed_data['gun_faults'])
|
||||||
|
|
||||||
|
if system_faults:
|
||||||
|
fault_summary += f"系统故障 {system_faults}, "
|
||||||
|
|
||||||
|
fault_summary += f"充电枪数量 {gun_faults_count}"
|
||||||
|
|
||||||
|
logging.warning(fault_summary)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理08H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例报文
|
||||||
|
test_data = bytes.fromhex(
|
||||||
|
"4A 58 08 03 17 67 63 11 36 06 57 01 11 00 19 01 09 0A 05 04 00 00 00 00 02 00 00 00 00 00 00 66")
|
||||||
|
|
||||||
|
parser = Command08()
|
||||||
|
parser.process_08h(test_data)
|
152
charging_pile_proxy/commands/command_09.py
Normal file
152
charging_pile_proxy/commands/command_09.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Command09:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x09 # 09H命令码
|
||||||
|
|
||||||
|
def parse_09h(self, data):
|
||||||
|
"""
|
||||||
|
解析09H遥信命令
|
||||||
|
|
||||||
|
:param data: 完整的09H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x09:
|
||||||
|
logging.warning("09H命令帧格式不正确")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 提取充电枪数量
|
||||||
|
gun_count = data[20]
|
||||||
|
|
||||||
|
# 存储充电枪状态的列表
|
||||||
|
gun_states = []
|
||||||
|
|
||||||
|
# 从第21字节开始解析充电枪状态
|
||||||
|
current_index = 21
|
||||||
|
for i in range(gun_count):
|
||||||
|
# 提取充电枪状态
|
||||||
|
gun_state = data[current_index]
|
||||||
|
gun_state_text = self.get_gun_state_text(gun_state)
|
||||||
|
|
||||||
|
# 工作模式
|
||||||
|
current_index += 1
|
||||||
|
work_mode = data[current_index]
|
||||||
|
work_mode_text = self.get_work_mode_text(work_mode)
|
||||||
|
|
||||||
|
gun_states.append({
|
||||||
|
"gun_index": i + 1,
|
||||||
|
"state": gun_state,
|
||||||
|
"state_text": gun_state_text,
|
||||||
|
"work_mode": work_mode,
|
||||||
|
"work_mode_text": work_mode_text
|
||||||
|
})
|
||||||
|
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n09H命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"充电枪数量: {gun_count}")
|
||||||
|
print("充电枪状态:")
|
||||||
|
for gun in gun_states:
|
||||||
|
print(f" 枪 {gun['gun_index']}:")
|
||||||
|
print(f" 状态: {gun['state_text']} (0x{gun['state']:02X})")
|
||||||
|
print(f" 工作模式: {gun['work_mode_text']} (0x{gun['work_mode']:02X})")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"gun_count": gun_count,
|
||||||
|
"gun_states": gun_states
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析09H命令失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_gun_state_text(self, state):
|
||||||
|
"""
|
||||||
|
解析充电枪状态
|
||||||
|
|
||||||
|
:param state: 充电枪状态字节
|
||||||
|
:return: 状态文本描述
|
||||||
|
"""
|
||||||
|
state_map = {
|
||||||
|
0x01: "待机",
|
||||||
|
0x02: "等待连接",
|
||||||
|
0x03: "启动中",
|
||||||
|
0x04: "充电中",
|
||||||
|
0x05: "停止中",
|
||||||
|
0x06: "预约中",
|
||||||
|
0x07: "占用中",
|
||||||
|
0x08: "测试中",
|
||||||
|
0x09: "故障中",
|
||||||
|
0x0A: "定时充电",
|
||||||
|
0x0B: "充电完成",
|
||||||
|
0x0C: "升级中"
|
||||||
|
}
|
||||||
|
return state_map.get(state, f"未知状态 (0x{state:02X})")
|
||||||
|
|
||||||
|
def get_work_mode_text(self, mode):
|
||||||
|
"""
|
||||||
|
解析工作模式
|
||||||
|
|
||||||
|
:param mode: 工作模式字节
|
||||||
|
:return: 工作模式文本描述
|
||||||
|
"""
|
||||||
|
mode_map = {
|
||||||
|
0x01: "普通充电",
|
||||||
|
0x02: "轮充",
|
||||||
|
0x03: "大功率",
|
||||||
|
0x04: "超级充",
|
||||||
|
0x05: "电池维护",
|
||||||
|
0x06: "柔性充"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知模式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def process_09h(self, data):
|
||||||
|
"""
|
||||||
|
处理09H遥信命令
|
||||||
|
|
||||||
|
:param data: 完整的09H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_09h(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("09H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录遥信信息日志
|
||||||
|
logging.info(f"成功处理09H遥信命令: 桩号 {parsed_data['pile_id']}, 充电枪数量 {parsed_data['gun_count']}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理09H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例报文
|
||||||
|
test_data = bytes.fromhex(
|
||||||
|
"4A 58 09 03 17 67 63 11 36 06 57 01 13 00 19 01 09 09 37 1F 00 00 02 01 01 0A 00 00 01 01 0A 00 00 4F")
|
||||||
|
|
||||||
|
parser = Command09()
|
||||||
|
parser.process_09h(test_data)
|
148
charging_pile_proxy/commands/command_0A.py
Normal file
148
charging_pile_proxy/commands/command_0A.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Command0A:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x0A # 0AH命令码
|
||||||
|
|
||||||
|
def parse_0ah(self, data):
|
||||||
|
"""
|
||||||
|
解析0AH遥测命令
|
||||||
|
|
||||||
|
:param data: 完整的0AH命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x0A:
|
||||||
|
logging.warning("0AH命令帧格式不正确")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 解析总体电气参数
|
||||||
|
current_index = 20
|
||||||
|
power_params = {
|
||||||
|
"A相电压": struct.unpack("<H", data[current_index:current_index + 2])[0] / 10, # 0.1V
|
||||||
|
"B相电压": struct.unpack("<H", data[current_index + 2:current_index + 4])[0] / 10,
|
||||||
|
"C相电压": struct.unpack("<H", data[current_index + 4:current_index + 6])[0] / 10,
|
||||||
|
"A相电流": struct.unpack("<H", data[current_index + 6:current_index + 8])[0] / 100, # 0.01A
|
||||||
|
"B相电流": struct.unpack("<H", data[current_index + 8:current_index + 10])[0] / 100,
|
||||||
|
"C相电流": struct.unpack("<H", data[current_index + 10:current_index + 12])[0] / 100,
|
||||||
|
"总电表电量": struct.unpack("<I", data[current_index + 12:current_index + 16])[0] / 100 # 0.01kWh
|
||||||
|
}
|
||||||
|
current_index += 16
|
||||||
|
|
||||||
|
# 解析温度相关参数
|
||||||
|
temp_params = {
|
||||||
|
"桩内温度": data[current_index] - 50, # 偏移量-50℃
|
||||||
|
"进风口温度": data[current_index + 1] - 50,
|
||||||
|
"出风口温度": data[current_index + 2] - 50,
|
||||||
|
"控制板温度": data[current_index + 3] - 50,
|
||||||
|
"桩内湿度": data[current_index + 4] # 0-100%RH
|
||||||
|
}
|
||||||
|
current_index += 5
|
||||||
|
|
||||||
|
# 跳过预留字节
|
||||||
|
current_index += 8
|
||||||
|
|
||||||
|
# 解析充电枪数量
|
||||||
|
gun_count = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 存储每个充电枪的遥测数据
|
||||||
|
gun_params = []
|
||||||
|
for i in range(gun_count):
|
||||||
|
gun_data = {
|
||||||
|
"gun_index": i + 1,
|
||||||
|
"电表电压": struct.unpack("<H", data[current_index:current_index + 2])[0] / 10, # 0.1V
|
||||||
|
"电表电流": struct.unpack("<H", data[current_index + 2:current_index + 4])[0] / 100, # 0.01A
|
||||||
|
"电表电量": struct.unpack("<I", data[current_index + 4:current_index + 8])[0] / 100, # 0.01kWh
|
||||||
|
"充电模块电压": struct.unpack("<H", data[current_index + 8:current_index + 10])[0] / 10, # 0.1V
|
||||||
|
"充电模块电流": struct.unpack("<H", data[current_index + 10:current_index + 12])[0] / 10, # 0.1A
|
||||||
|
"充电模块温度": data[current_index + 12] - 50, # 偏移量-50℃
|
||||||
|
"充电枪温度": data[current_index + 13] - 50
|
||||||
|
}
|
||||||
|
gun_params.append(gun_data)
|
||||||
|
current_index += 14
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n0AH命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
|
||||||
|
print("总体电气参数:")
|
||||||
|
for param, value in power_params.items():
|
||||||
|
print(f" {param}: {value}")
|
||||||
|
|
||||||
|
print("温度和湿度参数:")
|
||||||
|
for param, value in temp_params.items():
|
||||||
|
print(f" {param}: {value}")
|
||||||
|
|
||||||
|
print(f"充电枪数量: {gun_count}")
|
||||||
|
for gun in gun_params:
|
||||||
|
print(f"枪 {gun['gun_index']} 遥测数据:")
|
||||||
|
for param, value in gun.items():
|
||||||
|
if param != "gun_index":
|
||||||
|
print(f" {param}: {value}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"power_params": power_params,
|
||||||
|
"temp_params": temp_params,
|
||||||
|
"gun_count": gun_count,
|
||||||
|
"gun_params": gun_params
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析0AH命令失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_0ah(self, data):
|
||||||
|
"""
|
||||||
|
处理0AH遥测命令
|
||||||
|
|
||||||
|
:param data: 完整的0AH命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_0ah(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("0AH命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录遥测信息日志
|
||||||
|
log_message = (
|
||||||
|
f"桩号 {parsed_data['pile_id']} 遥测数据: "
|
||||||
|
f"A相电压 {parsed_data['power_params']['A相电压']}V, "
|
||||||
|
f"总电量 {parsed_data['power_params']['总电表电量']}kWh, "
|
||||||
|
f"桩内温度 {parsed_data['temp_params']['桩内温度']}℃"
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info(log_message)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理0AH命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例报文
|
||||||
|
test_data = bytes.fromhex(
|
||||||
|
"4A 58 0A 03 17 67 63 11 36 06 57 01 48 00 19 01 09 09 37 39 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 74 29 AD 00 00 00 00 00 00 67 00 00 00 00 00 00 00 00 27 63 F1 00 00 00 00 00 00 E1 00 00 00 00 F2")
|
||||||
|
|
||||||
|
parser = Command0A()
|
||||||
|
parser.process_0ah(test_data)
|
185
charging_pile_proxy/commands/command_19_1A.py
Normal file
185
charging_pile_proxy/commands/command_19_1A.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
class Command191A:
|
||||||
|
def __init__(self):
|
||||||
|
self.command_19 = 0x19 # 卡鉴权上报命令
|
||||||
|
self.command_1a = 0x1A # 平台回复卡鉴权命令
|
||||||
|
|
||||||
|
def parse_19_card_auth(self, data):
|
||||||
|
"""
|
||||||
|
解析19H卡鉴权上报命令
|
||||||
|
|
||||||
|
:param data: 完整的19H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x19:
|
||||||
|
logging.warning(f"19H命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 提取卡号(通常是ASCII字符串)
|
||||||
|
card_number_start = 22
|
||||||
|
card_number_end = card_number_start + 16
|
||||||
|
card_number = data[card_number_start:card_number_end].decode('ascii').rstrip('\x00')
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n19H卡鉴权上报命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"卡号: {card_number}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"card_number": card_number
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析19H命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_1a_card_auth_response(self, card_number):
|
||||||
|
"""
|
||||||
|
生成1AH卡鉴权响应命令
|
||||||
|
|
||||||
|
:param card_number: 卡号
|
||||||
|
:return: 1AH响应报文
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.extend(b'JX') # 帧起始标志
|
||||||
|
frame.append(self.command_1a) # 命令码
|
||||||
|
frame.extend(bytes.fromhex('0317665611360637')) # 桩号(固定值)
|
||||||
|
frame.append(0x01) # 数据加密方式
|
||||||
|
|
||||||
|
# 构建数据域
|
||||||
|
data = bytearray()
|
||||||
|
|
||||||
|
# 时间标识(当前时间)
|
||||||
|
from datetime import datetime
|
||||||
|
now = datetime.now()
|
||||||
|
data.extend(struct.pack("<BBBBBB",
|
||||||
|
now.year - 2000, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second))
|
||||||
|
|
||||||
|
# 卡号(16字节ASCII,不足补0)
|
||||||
|
card_number_bytes = card_number.ljust(16, '\x00').encode('ascii')
|
||||||
|
data.extend(card_number_bytes)
|
||||||
|
|
||||||
|
# 卡余额(假设为0)
|
||||||
|
data.extend(struct.pack("<I", 0)) # 4字节,分辨率0.01元
|
||||||
|
|
||||||
|
# 允许充电标志(1-可充电;2-禁止充电)
|
||||||
|
data.append(0x01)
|
||||||
|
|
||||||
|
# 不可充电原因(如果允许充电,则为0)
|
||||||
|
data.append(0x00)
|
||||||
|
|
||||||
|
# 计费模型选择(1-使用本地计费模型)
|
||||||
|
data.append(0x01)
|
||||||
|
|
||||||
|
# 计费模型版本(假设为1)
|
||||||
|
data.extend(struct.pack("<H", 1))
|
||||||
|
|
||||||
|
# 停车费费率(假设为0)
|
||||||
|
data.extend(struct.pack("<I", 0))
|
||||||
|
|
||||||
|
# 时段数(假设为1个)
|
||||||
|
data.append(0x01)
|
||||||
|
|
||||||
|
# 第1个时段起始时间(假设为全天)
|
||||||
|
data.extend([0x00, 0x00]) # 起始时
|
||||||
|
|
||||||
|
# 第1个时段类型(平段)
|
||||||
|
data.append(0x03)
|
||||||
|
|
||||||
|
# 第1个时段电价费率(假设为0.1元/kWh)
|
||||||
|
data.extend(struct.pack("<I", 1000))
|
||||||
|
|
||||||
|
# 第1个时段服务费率(假设为0.05元/kWh)
|
||||||
|
data.extend(struct.pack("<I", 500))
|
||||||
|
|
||||||
|
# 数据域长度
|
||||||
|
frame.extend(struct.pack("<H", len(data)))
|
||||||
|
|
||||||
|
# 加入数据域
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验码
|
||||||
|
check = 0
|
||||||
|
for b in frame[2:]:
|
||||||
|
check ^= b
|
||||||
|
frame.append(check)
|
||||||
|
|
||||||
|
print("1AH卡鉴权响应数据构建成功:")
|
||||||
|
print(f"数据内容: {frame.hex()}")
|
||||||
|
print(f"数据长度: {len(frame)}字节")
|
||||||
|
|
||||||
|
return bytes(frame)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"生成1AH卡鉴权响应出错: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_19_card_auth(self, data):
|
||||||
|
"""
|
||||||
|
处理19H卡鉴权上报命令
|
||||||
|
|
||||||
|
:param data: 完整的19H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_19_card_auth(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("19H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录卡鉴权信息日志
|
||||||
|
logging.info(f"收到桩号 {parsed_data['pile_id']} 的卡鉴权请求,卡号 {parsed_data['card_number']}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理19H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 19H命令测试报文
|
||||||
|
test_19_data = bytes.fromhex(
|
||||||
|
"4A 58 19 03 17 66 56 11 36 06 37 01 16 00 19 01 09 0C 15 2B 65 36 39 61 32 31 30 33 00 00 00 00 00 00 00 00 14")
|
||||||
|
|
||||||
|
# 1AH命令测试报文
|
||||||
|
test_1a_data = bytes.fromhex(
|
||||||
|
"4A 58 1A 03 17 66 56 11 36 06 37 01 1D 00 19 01 09 0C 15 2E 65 36 39 61 32 31 30 33 00 00 00 00 00 00 00 00 A5 0E 0D 00 01 00 01 BF")
|
||||||
|
|
||||||
|
parser = Command191A()
|
||||||
|
|
||||||
|
# 测试解析19H命令
|
||||||
|
parser.process_19_card_auth(test_19_data)
|
||||||
|
|
||||||
|
# 测试生成1AH响应
|
||||||
|
card_number = "e69a21033"
|
||||||
|
response = parser.generate_1a_card_auth_response(card_number)
|
||||||
|
print("\n1AH卡鉴权响应:")
|
||||||
|
print(response.hex())
|
293
charging_pile_proxy/commands/command_21_22.py
Normal file
293
charging_pile_proxy/commands/command_21_22.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Command2122:
|
||||||
|
def __init__(self):
|
||||||
|
self.command_21 = 0x21 # 充电启动结果命令
|
||||||
|
self.command_22 = 0x22 # 平台回复启动充电结果命令
|
||||||
|
|
||||||
|
def parse_21h_charging_start_result(self, data):
|
||||||
|
"""
|
||||||
|
解析21H充电启动结果命令
|
||||||
|
|
||||||
|
:param data: 完整的21H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x21:
|
||||||
|
logging.warning(f"21H命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
current_index = 22
|
||||||
|
|
||||||
|
# 解析充电订单号
|
||||||
|
charging_order_number = data[current_index:current_index + 32].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 32
|
||||||
|
|
||||||
|
# 解析用户ID
|
||||||
|
user_id = data[current_index:current_index + 32].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 32
|
||||||
|
|
||||||
|
# 解析用户类型
|
||||||
|
user_type = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析车牌号
|
||||||
|
vehicle_number = data[current_index:current_index + 9].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 9
|
||||||
|
|
||||||
|
# 解析控制方式
|
||||||
|
control_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析控制参数
|
||||||
|
control_param = struct.unpack("<I", data[current_index:current_index + 4])[0]
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析充电模式
|
||||||
|
charging_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析充电桩类型
|
||||||
|
pile_type = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析启动结果
|
||||||
|
start_result = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析启动失败原因
|
||||||
|
start_failure_reason = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析充电起始时间
|
||||||
|
start_charging_time_bytes = data[current_index:current_index + 6]
|
||||||
|
start_charging_time = datetime(
|
||||||
|
start_charging_time_bytes[0] + 2000,
|
||||||
|
start_charging_time_bytes[1],
|
||||||
|
start_charging_time_bytes[2],
|
||||||
|
start_charging_time_bytes[3],
|
||||||
|
start_charging_time_bytes[4],
|
||||||
|
start_charging_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析充电起始电量
|
||||||
|
start_charging_amount = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析绝缘检测电压
|
||||||
|
insulation_voltage = struct.unpack("<H", data[current_index:current_index + 2])[0] / 10 # 0.1V
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n21H充电启动结果命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"充电订单号: {charging_order_number}")
|
||||||
|
print(f"用户ID: {user_id}")
|
||||||
|
print(f"用户类型: {self.get_user_type_text(user_type)}")
|
||||||
|
print(f"车牌号: {vehicle_number}")
|
||||||
|
print(f"控制方式: {self.get_control_mode_text(control_mode)}")
|
||||||
|
print(f"控制参数: {control_param}")
|
||||||
|
print(f"充电模式: {self.get_charging_mode_text(charging_mode)}")
|
||||||
|
print(f"充电桩类型: {self.get_pile_type_text(pile_type)}")
|
||||||
|
print(f"启动结果: {self.get_start_result_text(start_result)}")
|
||||||
|
print(f"启动失败原因: {self.get_start_failure_reason_text(start_failure_reason)}")
|
||||||
|
print(f"充电起始时间: {start_charging_time}")
|
||||||
|
print(f"充电起始电量: {start_charging_amount}kWh")
|
||||||
|
print(f"绝缘检测电压: {insulation_voltage}V")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"charging_order_number": charging_order_number,
|
||||||
|
"user_id": user_id,
|
||||||
|
"user_type": self.get_user_type_text(user_type),
|
||||||
|
"vehicle_number": vehicle_number,
|
||||||
|
"control_mode": self.get_control_mode_text(control_mode),
|
||||||
|
"control_param": control_param,
|
||||||
|
"charging_mode": self.get_charging_mode_text(charging_mode),
|
||||||
|
"pile_type": self.get_pile_type_text(pile_type),
|
||||||
|
"start_result": self.get_start_result_text(start_result),
|
||||||
|
"start_failure_reason": self.get_start_failure_reason_text(start_failure_reason),
|
||||||
|
"start_charging_time": start_charging_time,
|
||||||
|
"start_charging_amount": start_charging_amount,
|
||||||
|
"insulation_voltage": insulation_voltage
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析21H命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_22h_charging_start_response(self, pile_id_bytes):
|
||||||
|
"""
|
||||||
|
生成22H平台回复启动充电结果命令
|
||||||
|
|
||||||
|
:param pile_id_bytes: 充电桩桩号字节
|
||||||
|
:return: 22H响应报文
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.extend(b'JX') # 帧起始标志
|
||||||
|
frame.append(self.command_22) # 命令码
|
||||||
|
frame.extend(pile_id_bytes) # 桩号
|
||||||
|
frame.append(0x01) # 数据加密方式
|
||||||
|
|
||||||
|
# 构建数据域
|
||||||
|
data = bytearray()
|
||||||
|
|
||||||
|
# 时间标识(当前时间)
|
||||||
|
now = datetime.now()
|
||||||
|
data.extend(struct.pack("<BBBBBB",
|
||||||
|
now.year - 2000, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second))
|
||||||
|
|
||||||
|
# 数据域长度
|
||||||
|
frame.extend(struct.pack("<H", len(data)))
|
||||||
|
|
||||||
|
# 加入数据域
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验码
|
||||||
|
check = 0
|
||||||
|
for b in frame[2:]:
|
||||||
|
check ^= b
|
||||||
|
frame.append(check)
|
||||||
|
|
||||||
|
print("22H充电启动结果响应数据构建成功:")
|
||||||
|
print(f"数据内容: {frame.hex()}")
|
||||||
|
print(f"数据长度: {len(frame)}字节")
|
||||||
|
|
||||||
|
return bytes(frame)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"生成22H充电启动结果响应出错: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_21h_charging_start_result(self, data):
|
||||||
|
"""
|
||||||
|
处理21H充电启动结果命令
|
||||||
|
|
||||||
|
:param data: 完整的21H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_21h_charging_start_result(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("21H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录充电启动结果信息日志
|
||||||
|
logging.info(
|
||||||
|
f"收到桩号 {parsed_data['pile_id']} 的充电启动结果: "
|
||||||
|
f"订单号 {parsed_data['charging_order_number']}, "
|
||||||
|
f"启动结果 {parsed_data['start_result']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理21H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user_type_text(self, user_type):
|
||||||
|
"""解析用户类型"""
|
||||||
|
type_map = {
|
||||||
|
1: "超级卡",
|
||||||
|
2: "在线卡",
|
||||||
|
3: "离线卡",
|
||||||
|
5: "本地管理员",
|
||||||
|
6: "VIN鉴权"
|
||||||
|
}
|
||||||
|
return type_map.get(user_type, f"未知类型 (0x{user_type:02X})")
|
||||||
|
|
||||||
|
def get_control_mode_text(self, mode):
|
||||||
|
"""解析控制方式"""
|
||||||
|
mode_map = {
|
||||||
|
1: "定时长充",
|
||||||
|
2: "定电量充",
|
||||||
|
3: "定金额充",
|
||||||
|
4: "自动充满"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知方式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_charging_mode_text(self, mode):
|
||||||
|
"""解析充电模式"""
|
||||||
|
mode_map = {
|
||||||
|
1: "普通充电",
|
||||||
|
2: "轮充",
|
||||||
|
3: "大功率",
|
||||||
|
4: "超级充",
|
||||||
|
5: "电池维护",
|
||||||
|
6: "柔性充"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知模式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_pile_type_text(self, pile_type):
|
||||||
|
"""解析充电桩类型"""
|
||||||
|
type_map = {
|
||||||
|
1: "交流",
|
||||||
|
2: "直流"
|
||||||
|
}
|
||||||
|
return type_map.get(pile_type, f"未知类型 (0x{pile_type:02X})")
|
||||||
|
|
||||||
|
def get_start_result_text(self, result):
|
||||||
|
"""解析启动结果"""
|
||||||
|
result_map = {
|
||||||
|
1: "成功",
|
||||||
|
2: "失败"
|
||||||
|
}
|
||||||
|
return result_map.get(result, f"未知结果 (0x{result:02X})")
|
||||||
|
|
||||||
|
def get_start_failure_reason_text(self, reason):
|
||||||
|
"""解析启动失败原因"""
|
||||||
|
reason_map = {
|
||||||
|
1: "设备故障",
|
||||||
|
2: "充电枪使用中",
|
||||||
|
3: "充电设备已被预约",
|
||||||
|
4: "不允许充电",
|
||||||
|
5: "参数不支持",
|
||||||
|
6: "其他原因"
|
||||||
|
}
|
||||||
|
return reason_map.get(reason, f"未知原因 (0x{reason:04X})")
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 21H命令测试报文
|
||||||
|
test_21_data = bytes.fromhex(
|
||||||
|
"4A 58 21 03 17 66 56 11 36 06 37 01 B3 00 19 01 09 0B 28 1D 01 31 38 37 37 31 39 38 35 39 35 39 37 30 35 39 36 38 36 00 00 00 00 00 00 00 00 00 00 00 00 00 38 34 30 34 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 16 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 F4 01 00 00 01 02 01 00 00 19 01 09 0B 27 37 D4 6C 68 01 00 00 00 00 00 00 00 00 00 03 D0 11 26 18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4C 5A 47 4A 4C 4D 34 34 35 50 58 31 31 34 35 33 37 01 01 00 00 00 00 00 00 77 01 DC 05 03 0B 60 1B 73 DA 02 E8 18 E8")
|
||||||
|
|
||||||
|
# 22H命令测试报文
|
||||||
|
test_22_data = bytes.fromhex("4A 58 22 03 17 66 56 11 36 06 37 01 07 00 19 01 09 0B 28 20 01 05")
|
||||||
|
|
||||||
|
parser = Command2122()
|
||||||
|
|
||||||
|
# 测试解析21H命令
|
||||||
|
parser.process_21h_charging_start_result(test_21_data)
|
||||||
|
|
||||||
|
# 测试生成22H响应
|
||||||
|
pile_id_bytes = bytes.fromhex("0317665611360637")
|
||||||
|
response = parser.generate_22h_charging_start_response(pile_id_bytes)
|
||||||
|
print("\n22H充电启动结果响应:")
|
||||||
|
print(response.hex())
|
381
charging_pile_proxy/commands/command_23_24.py
Normal file
381
charging_pile_proxy/commands/command_23_24.py
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
|
||||||
|
class Command2324:
|
||||||
|
def __init__(self):
|
||||||
|
self.command_23 = 0x23 # 最新充电订单命令
|
||||||
|
self.command_24 = 0x24 # 平台回复最新充电订单命令
|
||||||
|
|
||||||
|
def parse_23h_latest_charging_order(self, data):
|
||||||
|
"""
|
||||||
|
解析23H最新充电订单命令
|
||||||
|
|
||||||
|
:param data: 完整的23H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != self.command_23:
|
||||||
|
logging.warning(f"23H命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
current_index = 20
|
||||||
|
|
||||||
|
# 解析记录索引号
|
||||||
|
record_index = struct.unpack("<I", data[current_index:current_index + 4])[0]
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析充电订单号
|
||||||
|
charging_order_number = data[current_index:current_index + 32].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 32
|
||||||
|
|
||||||
|
# 解析用户ID
|
||||||
|
user_id = data[current_index:current_index + 32].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 32
|
||||||
|
|
||||||
|
# 解析用户类型
|
||||||
|
user_type = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析组织机构代码
|
||||||
|
org_code = data[current_index:current_index + 9].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 9
|
||||||
|
|
||||||
|
# 解析充电卡余额(用于离线卡)
|
||||||
|
card_balance = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析VIN
|
||||||
|
vin = data[current_index:current_index + 17].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 17
|
||||||
|
|
||||||
|
# 解析开始充电时间
|
||||||
|
start_charging_time_bytes = data[current_index:current_index + 6]
|
||||||
|
start_charging_time = datetime(
|
||||||
|
start_charging_time_bytes[0] + 2000,
|
||||||
|
start_charging_time_bytes[1],
|
||||||
|
start_charging_time_bytes[2],
|
||||||
|
start_charging_time_bytes[3],
|
||||||
|
start_charging_time_bytes[4],
|
||||||
|
start_charging_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析结束充电时间
|
||||||
|
end_charging_time_bytes = data[current_index:current_index + 6]
|
||||||
|
end_charging_time = datetime(
|
||||||
|
end_charging_time_bytes[0] + 2000,
|
||||||
|
end_charging_time_bytes[1],
|
||||||
|
end_charging_time_bytes[2],
|
||||||
|
end_charging_time_bytes[3],
|
||||||
|
end_charging_time_bytes[4],
|
||||||
|
end_charging_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析开始充电电量
|
||||||
|
start_charging_amount = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析结束充电电量
|
||||||
|
end_charging_amount = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析开始SOC
|
||||||
|
start_soc = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析结束SOC
|
||||||
|
end_soc = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析控制方式
|
||||||
|
control_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析控制参数
|
||||||
|
control_param = struct.unpack("<I", data[current_index:current_index + 4])[0]
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析启动类型
|
||||||
|
start_type = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 如果启动类型为定时启动,解析定时启动时间
|
||||||
|
start_timing_time = None
|
||||||
|
if start_type == 2:
|
||||||
|
start_timing_time_bytes = data[current_index:current_index + 6]
|
||||||
|
start_timing_time = datetime(
|
||||||
|
start_timing_time_bytes[0] + 2000,
|
||||||
|
start_timing_time_bytes[1],
|
||||||
|
start_timing_time_bytes[2],
|
||||||
|
start_timing_time_bytes[3],
|
||||||
|
start_timing_time_bytes[4],
|
||||||
|
start_timing_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析充电模式
|
||||||
|
charging_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析停止原因
|
||||||
|
stop_reason = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n23H最新充电订单命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"记录索引号: {record_index}")
|
||||||
|
print(f"充电订单号: {charging_order_number}")
|
||||||
|
print(f"用户ID: {user_id}")
|
||||||
|
print(f"用户类型: {self.get_user_type_text(user_type)}")
|
||||||
|
print(f"组织机构代码: {org_code}")
|
||||||
|
print(f"充电卡余额: {card_balance}元")
|
||||||
|
print(f"VIN: {vin}")
|
||||||
|
print(f"开始充电时间: {start_charging_time}")
|
||||||
|
print(f"结束充电时间: {end_charging_time}")
|
||||||
|
print(f"开始充电电量: {start_charging_amount}kWh")
|
||||||
|
print(f"结束充电电量: {end_charging_amount}kWh")
|
||||||
|
print(f"开始SOC: {start_soc}%")
|
||||||
|
print(f"结束SOC: {end_soc}%")
|
||||||
|
print(f"控制方式: {self.get_control_mode_text(control_mode)}")
|
||||||
|
print(f"控制参数: {control_param}")
|
||||||
|
print(f"启动类型: {self.get_start_type_text(start_type)}")
|
||||||
|
if start_timing_time:
|
||||||
|
print(f"定时启动时间: {start_timing_time}")
|
||||||
|
print(f"充电模式: {self.get_charging_mode_text(charging_mode)}")
|
||||||
|
print(f"停止原因: {self.get_stop_reason_text(stop_reason)}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"record_index": record_index,
|
||||||
|
"charging_order_number": charging_order_number,
|
||||||
|
"user_id": user_id,
|
||||||
|
"user_type": self.get_user_type_text(user_type),
|
||||||
|
"org_code": org_code,
|
||||||
|
"card_balance": card_balance,
|
||||||
|
"vin": vin,
|
||||||
|
"start_charging_time": start_charging_time,
|
||||||
|
"end_charging_time": end_charging_time,
|
||||||
|
"start_charging_amount": start_charging_amount,
|
||||||
|
"end_charging_amount": end_charging_amount,
|
||||||
|
"start_soc": start_soc,
|
||||||
|
"end_soc": end_soc,
|
||||||
|
"control_mode": self.get_control_mode_text(control_mode),
|
||||||
|
"control_param": control_param,
|
||||||
|
"start_type": self.get_start_type_text(start_type),
|
||||||
|
"start_timing_time": start_timing_time,
|
||||||
|
"charging_mode": self.get_charging_mode_text(charging_mode),
|
||||||
|
"stop_reason": self.get_stop_reason_text(stop_reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析23H命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_24h_charging_order_response(self, pile_id_bytes, record_index):
|
||||||
|
"""
|
||||||
|
生成24H平台回复最新充电订单命令
|
||||||
|
|
||||||
|
:param pile_id_bytes: 充电桩桩号字节
|
||||||
|
:param record_index: 记录索引号
|
||||||
|
:return: 24H响应报文
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.extend(b'JX') # 帧起始标志
|
||||||
|
frame.append(self.command_24) # 命令码
|
||||||
|
frame.extend(pile_id_bytes) # 桩号
|
||||||
|
frame.append(0x01) # 数据加密方式
|
||||||
|
|
||||||
|
# 构建数据域
|
||||||
|
data = bytearray()
|
||||||
|
|
||||||
|
# 时间标识(当前时间)
|
||||||
|
now = datetime.now()
|
||||||
|
data.extend(struct.pack("<BBBBBB",
|
||||||
|
now.year - 2000, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second))
|
||||||
|
|
||||||
|
# 记录索引号
|
||||||
|
data.extend(struct.pack("<I", record_index))
|
||||||
|
|
||||||
|
# 数据域长度
|
||||||
|
frame.extend(struct.pack("<H", len(data)))
|
||||||
|
|
||||||
|
# 加入数据域
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验码(从命令码开始到数据域结束的所有字节异或)
|
||||||
|
check = 0
|
||||||
|
for b in frame[2:]:
|
||||||
|
check ^= b
|
||||||
|
frame.append(check)
|
||||||
|
|
||||||
|
print("24H最新充电订单响应数据构建成功:")
|
||||||
|
print(f"数据内容: {frame.hex()}")
|
||||||
|
print(f"数据长度: {len(frame)}字节")
|
||||||
|
|
||||||
|
return bytes(frame)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"生成24H最新充电订单响应出错: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_23h_latest_charging_order(self, data):
|
||||||
|
"""
|
||||||
|
处理23H最新充电订单命令
|
||||||
|
|
||||||
|
:param data: 完整的23H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_23h_latest_charging_order(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("23H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录最新充电订单信息日志
|
||||||
|
logging.info(
|
||||||
|
f"收到桩号 {parsed_data['pile_id']} 的最新充电订单: "
|
||||||
|
f"订单号 {parsed_data['charging_order_number']}, "
|
||||||
|
f"充电时间 {parsed_data['start_charging_time']} - {parsed_data['end_charging_time']}, "
|
||||||
|
f"充电电量 {parsed_data['start_charging_amount']} - {parsed_data['end_charging_amount']}kWh"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理23H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user_type_text(self, user_type):
|
||||||
|
"""解析用户类型"""
|
||||||
|
type_map = {
|
||||||
|
1: "超级卡",
|
||||||
|
2: "在线卡",
|
||||||
|
3: "离线卡",
|
||||||
|
5: "本地管理员",
|
||||||
|
6: "VIN鉴权"
|
||||||
|
}
|
||||||
|
return type_map.get(user_type, f"未知类型 (0x{user_type:02X})")
|
||||||
|
|
||||||
|
def get_control_mode_text(self, mode):
|
||||||
|
"""解析控制方式"""
|
||||||
|
mode_map = {
|
||||||
|
1: "定时长充",
|
||||||
|
2: "定电量充",
|
||||||
|
3: "定金额充",
|
||||||
|
4: "自动充满"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知方式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_charging_mode_text(self, mode):
|
||||||
|
"""解析充电模式"""
|
||||||
|
mode_map = {
|
||||||
|
1: "普通充电",
|
||||||
|
2: "轮充",
|
||||||
|
3: "大功率",
|
||||||
|
4: "超级充",
|
||||||
|
5: "电池维护",
|
||||||
|
6: "柔性充"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知模式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_start_type_text(self, start_type):
|
||||||
|
"""解析启动类型"""
|
||||||
|
type_map = {
|
||||||
|
1: "立即启动",
|
||||||
|
2: "定时启动"
|
||||||
|
}
|
||||||
|
return type_map.get(start_type, f"未知类型 (0x{start_type:02X})")
|
||||||
|
|
||||||
|
def get_stop_reason_text(self, reason):
|
||||||
|
"""解析停止原因"""
|
||||||
|
reason_map = {
|
||||||
|
3: "强制拔枪",
|
||||||
|
5: "电子锁故障",
|
||||||
|
7: "启动绝缘电压失败",
|
||||||
|
8: "绝缘低故障",
|
||||||
|
9: "绝缘检测故障",
|
||||||
|
10: "绝缘泄放电压异常",
|
||||||
|
11: "电池外侧电压大于10V",
|
||||||
|
12: "BRM报文超时",
|
||||||
|
13: "BCP报文超时",
|
||||||
|
14: "BRO_00超时",
|
||||||
|
15: "BRO超时",
|
||||||
|
16: "BCL超时",
|
||||||
|
17: "BCS超时",
|
||||||
|
18: "电池电压不匹配",
|
||||||
|
20: "启动预充电压失败",
|
||||||
|
21: "电池单体电压过高",
|
||||||
|
22: "电池单体电压过低",
|
||||||
|
23: "SOC过高",
|
||||||
|
24: "SOC过低",
|
||||||
|
26: "过温",
|
||||||
|
31: "输出电压过高",
|
||||||
|
32: "充电过流",
|
||||||
|
51: "到达设定的SOC",
|
||||||
|
52: "到达设定的电压",
|
||||||
|
53: "到达设定的单体电压",
|
||||||
|
54: "充电机主动停止",
|
||||||
|
61: "绝缘故障",
|
||||||
|
62: "电池输出连接器异常",
|
||||||
|
63: "输出连接器过温"
|
||||||
|
}
|
||||||
|
return reason_map.get(reason, f"未知原因 (0x{reason:04X})")
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建解析器实例
|
||||||
|
parser = Command2324()
|
||||||
|
|
||||||
|
# 23H命令测试报文
|
||||||
|
# 注意:确保测试报文长度和格式与解析逻辑一致
|
||||||
|
test_23_data = bytes.fromhex(
|
||||||
|
"4A5823031766561136063701A3001901090B251E01C20A0000313837373139373830313631353535363631300000000000000000003840343300000000000000000000000000000000000000000000000000000000160000000000000000004C5A474A4C4D34443550583131343533371901090B242D1901090B251BBE6C6801C66C6801494903F4010000041901090B242F01F903011B000300000003000000000000010308002F"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 24H命令测试报文
|
||||||
|
test_24_data = bytes.fromhex(
|
||||||
|
"4A58240317665611360637010B001901090B252001C20A0000CA"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 测试解析23H命令
|
||||||
|
parsed_data = parser.parse_23h_latest_charging_order(test_23_data)
|
||||||
|
|
||||||
|
# 检查解析是否成功
|
||||||
|
if parsed_data:
|
||||||
|
# 测试生成24H响应
|
||||||
|
pile_id_bytes = bytes.fromhex("0317665611360637") # 从测试报文中提取的桩号
|
||||||
|
record_index = parsed_data['record_index'] # 使用解析得到的记录索引号
|
||||||
|
response = parser.generate_24h_charging_order_response(pile_id_bytes, record_index)
|
||||||
|
print("\n24H最新充电订单响应:")
|
||||||
|
print(response.hex())
|
||||||
|
else:
|
||||||
|
logging.error("23H命令解析失败,无法生成24H响应。")
|
215
charging_pile_proxy/commands/command_25.py
Normal file
215
charging_pile_proxy/commands/command_25.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Command25:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x25 # 充电信息命令
|
||||||
|
|
||||||
|
def parse_25h_charging_info(self, data):
|
||||||
|
"""
|
||||||
|
解析25H充电信息命令
|
||||||
|
|
||||||
|
:param data: 完整的25H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x25:
|
||||||
|
logging.warning(f"25H命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 解析充电参数
|
||||||
|
current_index = 20
|
||||||
|
charging_voltage = struct.unpack("<H", data[current_index:current_index + 2])[0] / 10 # 0.1V
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
charging_current = struct.unpack("<H", data[current_index:current_index + 2])[0] / 10 # 0.1A
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
charging_power = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kW
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
charging_duration = struct.unpack("<I", data[current_index:current_index + 4])[0] # 秒
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
charging_amount = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
charging_fee = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析充电模块接入数量
|
||||||
|
charging_module_count = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析充电电费
|
||||||
|
charging_electricity_fee = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析服务费
|
||||||
|
service_fee = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析充电订单号(17字节)
|
||||||
|
charging_order_number = data[current_index:current_index + 17].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 17
|
||||||
|
|
||||||
|
# 解析时间段信息
|
||||||
|
time_periods = []
|
||||||
|
time_period_count = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
for _ in range(time_period_count):
|
||||||
|
# 每个时间段的解析
|
||||||
|
period_start_time = datetime(
|
||||||
|
year, month, day,
|
||||||
|
data[current_index],
|
||||||
|
data[current_index + 1]
|
||||||
|
)
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
period_type = data[current_index] # 1-尖;2-峰;3-平;4-谷
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
period_electricity_price = struct.unpack("<I", data[current_index:current_index + 4])[
|
||||||
|
0] / 10000 # 0.0001元/kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
period_service_price = struct.unpack("<I", data[current_index:current_index + 4])[
|
||||||
|
0] / 10000 # 0.0001元/kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
period_electricity_amount = struct.unpack("<H", data[current_index:current_index + 2])[
|
||||||
|
0] / 100 # 0.01kWh
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
period_electricity_fee = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
period_service_fee = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
time_periods.append({
|
||||||
|
"start_time": period_start_time,
|
||||||
|
"type": self.get_period_type_text(period_type),
|
||||||
|
"electricity_price": period_electricity_price,
|
||||||
|
"service_price": period_service_price,
|
||||||
|
"electricity_amount": period_electricity_amount,
|
||||||
|
"electricity_fee": period_electricity_fee,
|
||||||
|
"service_fee": period_service_fee
|
||||||
|
})
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n25H充电信息命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"充电电压: {charging_voltage}V")
|
||||||
|
print(f"充电电流: {charging_current}A")
|
||||||
|
print(f"充电功率: {charging_power}kW")
|
||||||
|
print(f"充电时长: {charging_duration}秒")
|
||||||
|
print(f"充电电量: {charging_amount}kWh")
|
||||||
|
print(f"充电金额: {charging_fee}元")
|
||||||
|
print(f"充电模块接入数量: {charging_module_count}")
|
||||||
|
print(f"充电电费: {charging_electricity_fee}元")
|
||||||
|
print(f"服务费: {service_fee}元")
|
||||||
|
print(f"充电订单号: {charging_order_number}")
|
||||||
|
print("时间段信息:")
|
||||||
|
for period in time_periods:
|
||||||
|
print(f" - 开始时间: {period['start_time']}")
|
||||||
|
print(f" 类型: {period['type']}")
|
||||||
|
print(f" 电价: {period['electricity_price']}元/kWh")
|
||||||
|
print(f" 服务费率: {period['service_price']}元/kWh")
|
||||||
|
print(f" 电量: {period['electricity_amount']}kWh")
|
||||||
|
print(f" 电费: {period['electricity_fee']}元")
|
||||||
|
print(f" 服务费: {period['service_fee']}元")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"charging_voltage": charging_voltage,
|
||||||
|
"charging_current": charging_current,
|
||||||
|
"charging_power": charging_power,
|
||||||
|
"charging_duration": charging_duration,
|
||||||
|
"charging_amount": charging_amount,
|
||||||
|
"charging_fee": charging_fee,
|
||||||
|
"charging_module_count": charging_module_count,
|
||||||
|
"charging_electricity_fee": charging_electricity_fee,
|
||||||
|
"service_fee": service_fee,
|
||||||
|
"charging_order_number": charging_order_number,
|
||||||
|
"time_periods": time_periods
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析25H命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_period_type_text(self, period_type):
|
||||||
|
"""
|
||||||
|
解析时间段类型
|
||||||
|
|
||||||
|
:param period_type: 时间段类型字节
|
||||||
|
:return: 时间段类型文本描述
|
||||||
|
"""
|
||||||
|
type_map = {
|
||||||
|
1: "尖",
|
||||||
|
2: "峰",
|
||||||
|
3: "平",
|
||||||
|
4: "谷"
|
||||||
|
}
|
||||||
|
return type_map.get(period_type, f"未知类型 (0x{period_type:02X})")
|
||||||
|
|
||||||
|
def process_25h_charging_info(self, data):
|
||||||
|
"""
|
||||||
|
处理25H充电信息命令
|
||||||
|
|
||||||
|
:param data: 完整的25H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_25h_charging_info(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("25H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录充电信息日志
|
||||||
|
logging.info(
|
||||||
|
f"收到桩号 {parsed_data['pile_id']} 的充电信息: "
|
||||||
|
f"充电电量 {parsed_data['charging_amount']}kWh, "
|
||||||
|
f"充电时长 {parsed_data['charging_duration']}秒, "
|
||||||
|
f"充电订单号 {parsed_data['charging_order_number']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理25H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 25H命令测试报文
|
||||||
|
test_25_data = bytes.fromhex(
|
||||||
|
"4A 58 25 03 17 66 56 11 36 06 37 01 61 00 19 01 09 0B 25 13 01 DA 07 00 00 00 00 00 00 22 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 31 38 37 37 31 39 37 38 30 31 36 31 35 35 35 36 36 31 30 00 00 00 00 00 00 00 00 00 00 00 00 00 01 19 01 09 0B 24 2D 19 01 09 0B 25 13 32 0F 00 00 AC 0D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3B 4A 58 30 03 17 66 56 11 36 06 37 01 1C 00 19 01 09 0B 25 13 01 00 00 A0 0F 02 DA 07 A0 0F 00 00 00 00 00 00 00 00 00 00 00 00 ED")
|
||||||
|
|
||||||
|
parser = Command25()
|
||||||
|
|
||||||
|
# 测试解析25H命令
|
||||||
|
parser.process_25h_charging_info(test_25_data)
|
381
charging_pile_proxy/commands/command_26_27.py
Normal file
381
charging_pile_proxy/commands/command_26_27.py
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
|
||||||
|
class Command2324:
|
||||||
|
def __init__(self):
|
||||||
|
self.command_23 = 0x23 # 最新充电订单命令
|
||||||
|
self.command_24 = 0x24 # 平台回复最新充电订单命令
|
||||||
|
|
||||||
|
def parse_23h_latest_charging_order(self, data):
|
||||||
|
"""
|
||||||
|
解析23H最新充电订单命令
|
||||||
|
|
||||||
|
:param data: 完整的23H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != self.command_23:
|
||||||
|
logging.warning(f"23H命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
current_index = 20
|
||||||
|
|
||||||
|
# 解析记录索引号
|
||||||
|
record_index = struct.unpack("<I", data[current_index:current_index + 4])[0]
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析充电订单号
|
||||||
|
charging_order_number = data[current_index:current_index + 32].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 32
|
||||||
|
|
||||||
|
# 解析用户ID
|
||||||
|
user_id = data[current_index:current_index + 32].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 32
|
||||||
|
|
||||||
|
# 解析用户类型
|
||||||
|
user_type = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析组织机构代码
|
||||||
|
org_code = data[current_index:current_index + 9].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 9
|
||||||
|
|
||||||
|
# 解析充电卡余额(用于离线卡)
|
||||||
|
card_balance = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01元
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析VIN
|
||||||
|
vin = data[current_index:current_index + 17].decode('ascii').rstrip('\x00')
|
||||||
|
current_index += 17
|
||||||
|
|
||||||
|
# 解析开始充电时间
|
||||||
|
start_charging_time_bytes = data[current_index:current_index + 6]
|
||||||
|
start_charging_time = datetime(
|
||||||
|
start_charging_time_bytes[0] + 2000,
|
||||||
|
start_charging_time_bytes[1],
|
||||||
|
start_charging_time_bytes[2],
|
||||||
|
start_charging_time_bytes[3],
|
||||||
|
start_charging_time_bytes[4],
|
||||||
|
start_charging_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析结束充电时间
|
||||||
|
end_charging_time_bytes = data[current_index:current_index + 6]
|
||||||
|
end_charging_time = datetime(
|
||||||
|
end_charging_time_bytes[0] + 2000,
|
||||||
|
end_charging_time_bytes[1],
|
||||||
|
end_charging_time_bytes[2],
|
||||||
|
end_charging_time_bytes[3],
|
||||||
|
end_charging_time_bytes[4],
|
||||||
|
end_charging_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析开始充电电量
|
||||||
|
start_charging_amount = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析结束充电电量
|
||||||
|
end_charging_amount = struct.unpack("<I", data[current_index:current_index + 4])[0] / 100 # 0.01kWh
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析开始SOC
|
||||||
|
start_soc = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析结束SOC
|
||||||
|
end_soc = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析控制方式
|
||||||
|
control_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析控制参数
|
||||||
|
control_param = struct.unpack("<I", data[current_index:current_index + 4])[0]
|
||||||
|
current_index += 4
|
||||||
|
|
||||||
|
# 解析启动类型
|
||||||
|
start_type = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 如果启动类型为定时启动,解析定时启动时间
|
||||||
|
start_timing_time = None
|
||||||
|
if start_type == 2:
|
||||||
|
start_timing_time_bytes = data[current_index:current_index + 6]
|
||||||
|
start_timing_time = datetime(
|
||||||
|
start_timing_time_bytes[0] + 2000,
|
||||||
|
start_timing_time_bytes[1],
|
||||||
|
start_timing_time_bytes[2],
|
||||||
|
start_timing_time_bytes[3],
|
||||||
|
start_timing_time_bytes[4],
|
||||||
|
start_timing_time_bytes[5]
|
||||||
|
)
|
||||||
|
current_index += 6
|
||||||
|
|
||||||
|
# 解析充电模式
|
||||||
|
charging_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析停止原因
|
||||||
|
stop_reason = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n23H最新充电订单命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"记录索引号: {record_index}")
|
||||||
|
print(f"充电订单号: {charging_order_number}")
|
||||||
|
print(f"用户ID: {user_id}")
|
||||||
|
print(f"用户类型: {self.get_user_type_text(user_type)}")
|
||||||
|
print(f"组织机构代码: {org_code}")
|
||||||
|
print(f"充电卡余额: {card_balance}元")
|
||||||
|
print(f"VIN: {vin}")
|
||||||
|
print(f"开始充电时间: {start_charging_time}")
|
||||||
|
print(f"结束充电时间: {end_charging_time}")
|
||||||
|
print(f"开始充电电量: {start_charging_amount}kWh")
|
||||||
|
print(f"结束充电电量: {end_charging_amount}kWh")
|
||||||
|
print(f"开始SOC: {start_soc}%")
|
||||||
|
print(f"结束SOC: {end_soc}%")
|
||||||
|
print(f"控制方式: {self.get_control_mode_text(control_mode)}")
|
||||||
|
print(f"控制参数: {control_param}")
|
||||||
|
print(f"启动类型: {self.get_start_type_text(start_type)}")
|
||||||
|
if start_timing_time:
|
||||||
|
print(f"定时启动时间: {start_timing_time}")
|
||||||
|
print(f"充电模式: {self.get_charging_mode_text(charging_mode)}")
|
||||||
|
print(f"停止原因: {self.get_stop_reason_text(stop_reason)}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"record_index": record_index,
|
||||||
|
"charging_order_number": charging_order_number,
|
||||||
|
"user_id": user_id,
|
||||||
|
"user_type": self.get_user_type_text(user_type),
|
||||||
|
"org_code": org_code,
|
||||||
|
"card_balance": card_balance,
|
||||||
|
"vin": vin,
|
||||||
|
"start_charging_time": start_charging_time,
|
||||||
|
"end_charging_time": end_charging_time,
|
||||||
|
"start_charging_amount": start_charging_amount,
|
||||||
|
"end_charging_amount": end_charging_amount,
|
||||||
|
"start_soc": start_soc,
|
||||||
|
"end_soc": end_soc,
|
||||||
|
"control_mode": self.get_control_mode_text(control_mode),
|
||||||
|
"control_param": control_param,
|
||||||
|
"start_type": self.get_start_type_text(start_type),
|
||||||
|
"start_timing_time": start_timing_time,
|
||||||
|
"charging_mode": self.get_charging_mode_text(charging_mode),
|
||||||
|
"stop_reason": self.get_stop_reason_text(stop_reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析23H命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_24h_charging_order_response(self, pile_id_bytes, record_index):
|
||||||
|
"""
|
||||||
|
生成24H平台回复最新充电订单命令
|
||||||
|
|
||||||
|
:param pile_id_bytes: 充电桩桩号字节
|
||||||
|
:param record_index: 记录索引号
|
||||||
|
:return: 24H响应报文
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.extend(b'JX') # 帧起始标志
|
||||||
|
frame.append(self.command_24) # 命令码
|
||||||
|
frame.extend(pile_id_bytes) # 桩号
|
||||||
|
frame.append(0x01) # 数据加密方式
|
||||||
|
|
||||||
|
# 构建数据域
|
||||||
|
data = bytearray()
|
||||||
|
|
||||||
|
# 时间标识(当前时间)
|
||||||
|
now = datetime.now()
|
||||||
|
data.extend(struct.pack("<BBBBBB",
|
||||||
|
now.year - 2000, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second))
|
||||||
|
|
||||||
|
# 记录索引号
|
||||||
|
data.extend(struct.pack("<I", record_index))
|
||||||
|
|
||||||
|
# 数据域长度
|
||||||
|
frame.extend(struct.pack("<H", len(data)))
|
||||||
|
|
||||||
|
# 加入数据域
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验码(从命令码开始到数据域结束的所有字节异或)
|
||||||
|
check = 0
|
||||||
|
for b in frame[2:]:
|
||||||
|
check ^= b
|
||||||
|
frame.append(check)
|
||||||
|
|
||||||
|
print("24H最新充电订单响应数据构建成功:")
|
||||||
|
print(f"数据内容: {frame.hex()}")
|
||||||
|
print(f"数据长度: {len(frame)}字节")
|
||||||
|
|
||||||
|
return bytes(frame)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"生成24H最新充电订单响应出错: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_23h_latest_charging_order(self, data):
|
||||||
|
"""
|
||||||
|
处理23H最新充电订单命令
|
||||||
|
|
||||||
|
:param data: 完整的23H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_23h_latest_charging_order(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("23H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录最新充电订单信息日志
|
||||||
|
logging.info(
|
||||||
|
f"收到桩号 {parsed_data['pile_id']} 的最新充电订单: "
|
||||||
|
f"订单号 {parsed_data['charging_order_number']}, "
|
||||||
|
f"充电时间 {parsed_data['start_charging_time']} - {parsed_data['end_charging_time']}, "
|
||||||
|
f"充电电量 {parsed_data['start_charging_amount']} - {parsed_data['end_charging_amount']}kWh"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理23H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user_type_text(self, user_type):
|
||||||
|
"""解析用户类型"""
|
||||||
|
type_map = {
|
||||||
|
1: "超级卡",
|
||||||
|
2: "在线卡",
|
||||||
|
3: "离线卡",
|
||||||
|
5: "本地管理员",
|
||||||
|
6: "VIN鉴权"
|
||||||
|
}
|
||||||
|
return type_map.get(user_type, f"未知类型 (0x{user_type:02X})")
|
||||||
|
|
||||||
|
def get_control_mode_text(self, mode):
|
||||||
|
"""解析控制方式"""
|
||||||
|
mode_map = {
|
||||||
|
1: "定时长充",
|
||||||
|
2: "定电量充",
|
||||||
|
3: "定金额充",
|
||||||
|
4: "自动充满"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知方式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_charging_mode_text(self, mode):
|
||||||
|
"""解析充电模式"""
|
||||||
|
mode_map = {
|
||||||
|
1: "普通充电",
|
||||||
|
2: "轮充",
|
||||||
|
3: "大功率",
|
||||||
|
4: "超级充",
|
||||||
|
5: "电池维护",
|
||||||
|
6: "柔性充"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知模式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_start_type_text(self, start_type):
|
||||||
|
"""解析启动类型"""
|
||||||
|
type_map = {
|
||||||
|
1: "立即启动",
|
||||||
|
2: "定时启动"
|
||||||
|
}
|
||||||
|
return type_map.get(start_type, f"未知类型 (0x{start_type:02X})")
|
||||||
|
|
||||||
|
def get_stop_reason_text(self, reason):
|
||||||
|
"""解析停止原因"""
|
||||||
|
reason_map = {
|
||||||
|
3: "强制拔枪",
|
||||||
|
5: "电子锁故障",
|
||||||
|
7: "启动绝缘电压失败",
|
||||||
|
8: "绝缘低故障",
|
||||||
|
9: "绝缘检测故障",
|
||||||
|
10: "绝缘泄放电压异常",
|
||||||
|
11: "电池外侧电压大于10V",
|
||||||
|
12: "BRM报文超时",
|
||||||
|
13: "BCP报文超时",
|
||||||
|
14: "BRO_00超时",
|
||||||
|
15: "BRO超时",
|
||||||
|
16: "BCL超时",
|
||||||
|
17: "BCS超时",
|
||||||
|
18: "电池电压不匹配",
|
||||||
|
20: "启动预充电压失败",
|
||||||
|
21: "电池单体电压过高",
|
||||||
|
22: "电池单体电压过低",
|
||||||
|
23: "SOC过高",
|
||||||
|
24: "SOC过低",
|
||||||
|
26: "过温",
|
||||||
|
31: "输出电压过高",
|
||||||
|
32: "充电过流",
|
||||||
|
51: "到达设定的SOC",
|
||||||
|
52: "到达设定的电压",
|
||||||
|
53: "到达设定的单体电压",
|
||||||
|
54: "充电机主动停止",
|
||||||
|
61: "绝缘故障",
|
||||||
|
62: "电池输出连接器异常",
|
||||||
|
63: "输出连接器过温"
|
||||||
|
}
|
||||||
|
return reason_map.get(reason, f"未知原因 (0x{reason:04X})")
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建解析器实例
|
||||||
|
parser = Command2324()
|
||||||
|
|
||||||
|
# 23H命令测试报文
|
||||||
|
# 注意:确保测试报文长度和格式与解析逻辑一致
|
||||||
|
test_23_data = bytes.fromhex(
|
||||||
|
"4A5823031766561136063701A3001901090B251E01C20A0000313837373139373830313631353535363631300000000000000000003840343300000000000000000000000000000000000000000000000000000000160000000000000000004C5A474A4C4D34443550583131343533371901090B242D1901090B251BBE6C6801C66C6801494903F4010000041901090B242F01F903011B000300000003000000000000010308002F"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 24H命令测试报文
|
||||||
|
test_24_data = bytes.fromhex(
|
||||||
|
"4A58240317665611360637010B001901090B252001C20A0000CA"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 测试解析23H命令
|
||||||
|
parsed_data = parser.parse_23h_latest_charging_order(test_23_data)
|
||||||
|
|
||||||
|
# 检查解析是否成功
|
||||||
|
if parsed_data:
|
||||||
|
# 测试生成24H响应
|
||||||
|
pile_id_bytes = bytes.fromhex("0317665611360637") # 从测试报文中提取的桩号
|
||||||
|
record_index = parsed_data['record_index'] # 使用解析得到的记录索引号
|
||||||
|
response = parser.generate_24h_charging_order_response(pile_id_bytes, record_index)
|
||||||
|
print("\n24H最新充电订单响应:")
|
||||||
|
print(response.hex())
|
||||||
|
else:
|
||||||
|
logging.error("23H命令解析失败,无法生成24H响应。")
|
208
charging_pile_proxy/commands/command_30.py
Normal file
208
charging_pile_proxy/commands/command_30.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
class Command30:
|
||||||
|
def __init__(self):
|
||||||
|
self.command = 0x30 # BMS状态信息命令
|
||||||
|
|
||||||
|
def parse_30h_bms_status(self, data):
|
||||||
|
"""
|
||||||
|
解析30H BMS状态需求报文
|
||||||
|
|
||||||
|
:param data: 完整的30H命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x30:
|
||||||
|
logging.warning(f"30H命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 初始化解析索引
|
||||||
|
current_index = 20
|
||||||
|
|
||||||
|
# 解析电压需求
|
||||||
|
voltage_request = struct.unpack("<H", data[current_index:current_index + 2])[0] / 10 # 0.1V
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析电流需求
|
||||||
|
current_request = struct.unpack("<H", data[current_index:current_index + 2])[0] / 10 # 0.1A
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析充电模式
|
||||||
|
charging_mode = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析最高单体电压
|
||||||
|
max_cell_voltage = struct.unpack("<H", data[current_index:current_index + 2])[0] / 1000 # 0.001V
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析最高单体电压所在电池组号
|
||||||
|
max_cell_group = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析SOC
|
||||||
|
soc = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析剩余充电时间
|
||||||
|
remaining_charging_time = struct.unpack("<H", data[current_index:current_index + 2])[0]
|
||||||
|
current_index += 2
|
||||||
|
|
||||||
|
# 解析最高单体电压电池组编号
|
||||||
|
max_voltage_group_number = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析最高动力蓄电池温度
|
||||||
|
max_battery_temperature = data[current_index] - 50 # 偏移量-50
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析最高温度检测点编号
|
||||||
|
max_temperature_point = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析最低动力蓄电池温度
|
||||||
|
min_battery_temperature = data[current_index] - 50 # 偏移量-50
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析最低温度监测点编号
|
||||||
|
min_temperature_point = data[current_index]
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 解析告警信息
|
||||||
|
warning_bytes = data[current_index]
|
||||||
|
warnings = {
|
||||||
|
"单体电压过高/过低": (warning_bytes & 0x03),
|
||||||
|
"SOC过高/过低": ((warning_bytes >> 2) & 0x03),
|
||||||
|
"充电过流": ((warning_bytes >> 4) & 0x03),
|
||||||
|
"动力蓄电池温度过高": ((warning_bytes >> 6) & 0x03)
|
||||||
|
}
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n30H BMS状态需求报文解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"电压需求: {voltage_request}V")
|
||||||
|
print(f"电流需求: {current_request}A")
|
||||||
|
print(f"充电模式: {self.get_charging_mode_text(charging_mode)}")
|
||||||
|
print(f"最高单体电压: {max_cell_voltage}V")
|
||||||
|
print(f"最高单体电压所在电池组号: {max_cell_group}")
|
||||||
|
print(f"SOC: {soc}%")
|
||||||
|
print(f"剩余充电时间: {remaining_charging_time}分钟")
|
||||||
|
print(f"最高单体电压电池组编号: {max_voltage_group_number}")
|
||||||
|
print(f"最高动力蓄电池温度: {max_battery_temperature}°C")
|
||||||
|
print(f"最高温度检测点编号: {max_temperature_point}")
|
||||||
|
print(f"最低动力蓄电池温度: {min_battery_temperature}°C")
|
||||||
|
print(f"最低温度监测点编号: {min_temperature_point}")
|
||||||
|
print("告警信息:")
|
||||||
|
for warning, level in warnings.items():
|
||||||
|
print(f" {warning}: {self.get_warning_level_text(level)}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"voltage_request": voltage_request,
|
||||||
|
"current_request": current_request,
|
||||||
|
"charging_mode": self.get_charging_mode_text(charging_mode),
|
||||||
|
"max_cell_voltage": max_cell_voltage,
|
||||||
|
"max_cell_group": max_cell_group,
|
||||||
|
"soc": soc,
|
||||||
|
"remaining_charging_time": remaining_charging_time,
|
||||||
|
"max_voltage_group_number": max_voltage_group_number,
|
||||||
|
"max_battery_temperature": max_battery_temperature,
|
||||||
|
"max_temperature_point": max_temperature_point,
|
||||||
|
"min_battery_temperature": min_battery_temperature,
|
||||||
|
"min_temperature_point": min_temperature_point,
|
||||||
|
"warnings": {k: self.get_warning_level_text(v) for k, v in warnings.items()}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析30H命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_charging_mode_text(self, mode):
|
||||||
|
"""
|
||||||
|
解析充电模式
|
||||||
|
|
||||||
|
:param mode: 充电模式字节
|
||||||
|
:return: 充电模式文本描述
|
||||||
|
"""
|
||||||
|
mode_map = {
|
||||||
|
0x01: "恒流充电",
|
||||||
|
0x02: "恒压充电",
|
||||||
|
0x03: "涓流充电",
|
||||||
|
0x04: "充电完成",
|
||||||
|
0x05: "充电终止"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知模式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
def get_warning_level_text(self, level):
|
||||||
|
"""
|
||||||
|
解析告警级别
|
||||||
|
|
||||||
|
:param level: 告警级别
|
||||||
|
:return: 告警级别文本描述
|
||||||
|
"""
|
||||||
|
level_map = {
|
||||||
|
0x00: "正常",
|
||||||
|
0x01: "预警",
|
||||||
|
0x02: "严重告警",
|
||||||
|
0x03: "故障"
|
||||||
|
}
|
||||||
|
return level_map.get(level, f"未知级别 (0x{level:02X})")
|
||||||
|
|
||||||
|
def process_30h_bms_status(self, data):
|
||||||
|
"""
|
||||||
|
处理30H BMS状态信息命令
|
||||||
|
|
||||||
|
:param data: 完整的30H命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_30h_bms_status(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("30H命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录BMS状态信息日志
|
||||||
|
logging.info(
|
||||||
|
f"收到桩号 {parsed_data['pile_id']} 的BMS状态信息: "
|
||||||
|
f"SOC {parsed_data['soc']}%, "
|
||||||
|
f"电压需求 {parsed_data['voltage_request']}V, "
|
||||||
|
f"电流需求 {parsed_data['current_request']}A"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理30H命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 30H命令测试报文
|
||||||
|
test_30_data = bytes.fromhex(
|
||||||
|
"4A 58 30 03 17 66 56 11 36 06 37 01 1C 00 19 01 09 0B 25 15 01 60 1B 8D 07 02 DA 07 A0 0F 4A C1 49 78 00 01 40 03 3C 05 00 10 64")
|
||||||
|
|
||||||
|
parser = Command30()
|
||||||
|
|
||||||
|
# 测试解析30H命令
|
||||||
|
parser.process_30h_bms_status(test_30_data)
|
214
charging_pile_proxy/commands/command_heartbeat.py
Normal file
214
charging_pile_proxy/commands/command_heartbeat.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
class Command0B0CH:
|
||||||
|
def __init__(self):
|
||||||
|
self.command_0b = 0x0B # 平台心跳命令
|
||||||
|
self.command_0c = 0x0C # 桩心跳命令
|
||||||
|
|
||||||
|
def parse_0c_heartbeat(self, data):
|
||||||
|
"""
|
||||||
|
解析0CH桩心跳命令
|
||||||
|
|
||||||
|
:param data: 完整的0CH命令报文
|
||||||
|
:return: 解析后的字典或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证基本帧格式
|
||||||
|
if len(data) < 14 or data[0:2] != b'JX' or data[2] != 0x0C:
|
||||||
|
logging.warning(f"0CH命令帧格式不正确,原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 打印完整的原始报文以便调试
|
||||||
|
print(f"完整原始报文: {binascii.hexlify(data)}")
|
||||||
|
|
||||||
|
# 提取桩号
|
||||||
|
pile_id_bytes = data[3:11]
|
||||||
|
|
||||||
|
# 提取时间标识
|
||||||
|
time_bytes = data[14:20]
|
||||||
|
year = time_bytes[0] + 2000
|
||||||
|
month, day, hour, minute, second = time_bytes[1:6]
|
||||||
|
timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
|
||||||
|
|
||||||
|
# 解析充电枪数量
|
||||||
|
gun_count = data[20]
|
||||||
|
|
||||||
|
# 解析充电枪状态
|
||||||
|
gun_states = []
|
||||||
|
current_index = 21
|
||||||
|
for i in range(gun_count):
|
||||||
|
try:
|
||||||
|
# 每个充电枪的状态信息占2个字节
|
||||||
|
if current_index + 1 < len(data):
|
||||||
|
gun_state = data[current_index]
|
||||||
|
gun_work_mode = data[current_index + 1]
|
||||||
|
|
||||||
|
gun_states.append({
|
||||||
|
"gun_index": i + 1,
|
||||||
|
"state": gun_state,
|
||||||
|
"state_text": self.get_gun_state_text(gun_state),
|
||||||
|
"work_mode": gun_work_mode,
|
||||||
|
"work_mode_text": self.get_work_mode_text(gun_work_mode)
|
||||||
|
})
|
||||||
|
|
||||||
|
current_index += 2
|
||||||
|
except Exception as gun_parse_error:
|
||||||
|
logging.warning(f"解析第 {i + 1} 个充电枪状态时出错: {gun_parse_error}")
|
||||||
|
|
||||||
|
# 打印解析结果
|
||||||
|
print("\n0CH桩心跳命令解析结果:")
|
||||||
|
print(f"桩号: {pile_id_bytes.hex()}")
|
||||||
|
print(f"时间标识: {timestamp}")
|
||||||
|
print(f"充电枪数量: {gun_count}")
|
||||||
|
for gun_state in gun_states:
|
||||||
|
print(f"枪 {gun_state['gun_index']}:")
|
||||||
|
print(f" 状态: {gun_state['state_text']} (0x{gun_state['state']:02X})")
|
||||||
|
print(f" 工作模式: {gun_state['work_mode_text']} (0x{gun_state['work_mode']:02X})")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pile_id": pile_id_bytes.hex(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"gun_count": gun_count,
|
||||||
|
"gun_states": gun_states
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"解析0CH命令失败: {str(e)}")
|
||||||
|
logging.error(f"原始报文: {binascii.hexlify(data)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_0b_heartbeat_response(self, pile_id_bytes):
|
||||||
|
"""
|
||||||
|
生成0BH平台心跳响应
|
||||||
|
|
||||||
|
:param pile_id_bytes: 充电桩桩号字节
|
||||||
|
:return: 0BH心跳响应报文
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建帧
|
||||||
|
frame = bytearray()
|
||||||
|
frame.extend(b'JX') # 帧起始标志
|
||||||
|
frame.append(self.command_0b) # 命令码
|
||||||
|
frame.extend(pile_id_bytes) # 桩号
|
||||||
|
frame.append(0x01) # 数据加密方式
|
||||||
|
|
||||||
|
# 构建数据域
|
||||||
|
data = bytearray()
|
||||||
|
|
||||||
|
# 时间标识(当前时间)
|
||||||
|
from datetime import datetime
|
||||||
|
now = datetime.now()
|
||||||
|
data.extend(struct.pack("<BBBBBB",
|
||||||
|
now.year - 2000, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second))
|
||||||
|
|
||||||
|
# 心跳超时次数(这里固定为0)
|
||||||
|
data.append(0x00)
|
||||||
|
|
||||||
|
# 数据域长度
|
||||||
|
frame.extend(struct.pack("<H", len(data)))
|
||||||
|
|
||||||
|
# 加入数据域
|
||||||
|
frame.extend(data)
|
||||||
|
|
||||||
|
# 计算校验码
|
||||||
|
check = 0
|
||||||
|
for b in frame[2:]:
|
||||||
|
check ^= b
|
||||||
|
frame.append(check)
|
||||||
|
|
||||||
|
print("0BH心跳响应数据构建成功:")
|
||||||
|
print(f"数据内容: {frame.hex()}")
|
||||||
|
print(f"数据长度: {len(frame)}字节")
|
||||||
|
|
||||||
|
return bytes(frame)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"生成0BH心跳响应出错: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_0c_heartbeat(self, data):
|
||||||
|
"""
|
||||||
|
处理0CH桩心跳命令
|
||||||
|
|
||||||
|
:param data: 完整的0CH命令报文
|
||||||
|
:return: 是否成功处理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = self.parse_0c_heartbeat(data)
|
||||||
|
|
||||||
|
if parsed_data is None:
|
||||||
|
logging.warning("0CH命令解析失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录心跳信息日志
|
||||||
|
logging.info(f"收到桩号 {parsed_data['pile_id']} 的心跳, 充电枪数量 {parsed_data['gun_count']}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理0CH命令出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_gun_state_text(self, state):
|
||||||
|
"""
|
||||||
|
解析充电枪状态
|
||||||
|
|
||||||
|
:param state: 充电枪状态字节
|
||||||
|
:return: 状态文本描述
|
||||||
|
"""
|
||||||
|
state_map = {
|
||||||
|
0x01: "待机",
|
||||||
|
0x02: "等待连接",
|
||||||
|
0x03: "启动中",
|
||||||
|
0x04: "充电中",
|
||||||
|
0x05: "停止中",
|
||||||
|
0x06: "预约中",
|
||||||
|
0x07: "占用中",
|
||||||
|
0x08: "测试中",
|
||||||
|
0x09: "故障中",
|
||||||
|
0x0A: "定时充电",
|
||||||
|
0x0B: "充电完成",
|
||||||
|
0x0C: "升级中"
|
||||||
|
}
|
||||||
|
return state_map.get(state, f"未知状态 (0x{state:02X})")
|
||||||
|
|
||||||
|
def get_work_mode_text(self, mode):
|
||||||
|
"""
|
||||||
|
解析工作模式
|
||||||
|
|
||||||
|
:param mode: 工作模式字节
|
||||||
|
:return: 工作模式文本描述
|
||||||
|
"""
|
||||||
|
mode_map = {
|
||||||
|
0x01: "普通充电",
|
||||||
|
0x02: "轮充",
|
||||||
|
0x03: "大功率",
|
||||||
|
0x04: "超级充",
|
||||||
|
0x05: "电池维护",
|
||||||
|
0x06: "柔性充"
|
||||||
|
}
|
||||||
|
return mode_map.get(mode, f"未知模式 (0x{mode:02X})")
|
||||||
|
|
||||||
|
|
||||||
|
# 测试用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 0C命令测试报文
|
||||||
|
test_0c_data = bytes.fromhex("4A 58 0C 03 17 67 63 11 36 06 57 01 0C 00 19 01 09 09 37 3B 01 02 01 01 01 01 70")
|
||||||
|
|
||||||
|
# 0B命令测试报文
|
||||||
|
test_0b_data = bytes.fromhex("4A 58 0B 03 17 67 63 11 36 06 57 01 07 00 19 01 09 09 38 00 00 4B")
|
||||||
|
|
||||||
|
parser = Command0B0CH()
|
||||||
|
|
||||||
|
# 测试解析0C心跳
|
||||||
|
parser.process_0c_heartbeat(test_0c_data)
|
||||||
|
|
||||||
|
# 测试生成0B心跳响应
|
||||||
|
pile_id_bytes = bytes.fromhex("0317676311360657")
|
||||||
|
response = parser.generate_0b_heartbeat_response(pile_id_bytes)
|
||||||
|
print("\n0B心跳响应:")
|
||||||
|
print(response.hex())
|
25
charging_pile_proxy/config.yaml
Normal file
25
charging_pile_proxy/config.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 充电桩代理服务器配置文件
|
||||||
|
|
||||||
|
# 服务器监听配置
|
||||||
|
server:
|
||||||
|
host: '0.0.0.0' # 监听地址
|
||||||
|
port: 52461 # 监听端口
|
||||||
|
|
||||||
|
# 转发目标配置
|
||||||
|
forward:
|
||||||
|
host: '139.9.209.227' # 转发目标地址
|
||||||
|
port: 52461 # 转发目标端口
|
||||||
|
|
||||||
|
# MQTT配置
|
||||||
|
mqtt:
|
||||||
|
host: 'localhost' # MQTT代理地址
|
||||||
|
port: 1883 # MQTT代理端口
|
||||||
|
client_id: 'charging_pile_proxy' # MQTT客户端ID
|
||||||
|
topic_prefix: 'charging_pile/' # MQTT主题前缀
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
level: 'INFO' # 日志级别
|
||||||
|
file: 'charging_pile_proxy.log' # 日志文件路径
|
||||||
|
max_size: 10485760 # 日志文件最大大小 (10MB)
|
||||||
|
backup_count: 5 # 日志文件备份数量
|
0
charging_pile_proxy/config/__init__.py
Normal file
0
charging_pile_proxy/config/__init__.py
Normal file
0
charging_pile_proxy/config/settings.py
Normal file
0
charging_pile_proxy/config/settings.py
Normal file
0
charging_pile_proxy/core/__init__.py
Normal file
0
charging_pile_proxy/core/__init__.py
Normal file
BIN
charging_pile_proxy/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
charging_pile_proxy/core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
charging_pile_proxy/core/__pycache__/mqtt_client.cpython-311.pyc
Normal file
BIN
charging_pile_proxy/core/__pycache__/mqtt_client.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
charging_pile_proxy/core/__pycache__/utils.cpython-311.pyc
Normal file
BIN
charging_pile_proxy/core/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
42
charging_pile_proxy/core/mqtt_client.py
Normal file
42
charging_pile_proxy/core/mqtt_client.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import json
|
||||||
|
|
||||||
|
class MQTTClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.client = mqtt.Client(client_id="GoClientExample", protocol=mqtt.MQTTv311,
|
||||||
|
callback_api_version=mqtt.CallbackAPIVersion.VERSION1)
|
||||||
|
self.client.username_pw_set("emqx_test", "emqx_test")
|
||||||
|
self.client.on_connect = self.on_connect
|
||||||
|
self.client.on_disconnect = self.on_disconnect
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def on_connect(self, client, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected to MQTT broker")
|
||||||
|
self.connected = True
|
||||||
|
else:
|
||||||
|
print(f"Failed to connect to MQTT broker with code: {rc}")
|
||||||
|
|
||||||
|
def on_disconnect(self, client, userdata, rc):
|
||||||
|
print("Disconnected from MQTT broker")
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
try:
|
||||||
|
self.client.connect("123.6.102.119", 1883, 60)
|
||||||
|
self.client.loop_start()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"MQTT connection error: {str(e)}")
|
||||||
|
|
||||||
|
def publish_message(self, message):
|
||||||
|
try:
|
||||||
|
if self.connected:
|
||||||
|
self.client.publish("hejin/charging/log", json.dumps(message), qos=1)
|
||||||
|
else:
|
||||||
|
print("MQTT client not connected")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"MQTT publish error: {str(e)}")
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.client.loop_stop()
|
||||||
|
self.client.disconnect()
|
268
charging_pile_proxy/core/proxy_server.py
Normal file
268
charging_pile_proxy/core/proxy_server.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import socket
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from .utils import ProxyUtils # 使用相对导入
|
||||||
|
from .mqtt_client import MQTTClient # 使用相对导入
|
||||||
|
from commands.command_heartbeat import Command0B0CH
|
||||||
|
from commands.command_02 import Command02
|
||||||
|
from commands.command_03 import Command03
|
||||||
|
from commands.command_07 import Command07
|
||||||
|
from commands.command_08 import Command08
|
||||||
|
from commands.command_09 import Command09
|
||||||
|
from commands.command_0A import Command0A
|
||||||
|
from commands.command_25 import Command25
|
||||||
|
from commands.command_30 import Command30
|
||||||
|
from commands.command_19_1A import Command191A
|
||||||
|
from commands.command_21_22 import Command2122
|
||||||
|
from commands.command_23_24 import Command2324
|
||||||
|
from commands.command_26_27 import Command2627
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ChargingPileProxyServer:
|
||||||
|
def __init__(self, listen_host='0.0.0.0', listen_port=52461,
|
||||||
|
forward_host='139.9.209.227', forward_port=52461):
|
||||||
|
self.listen_host = listen_host
|
||||||
|
self.listen_port = listen_port
|
||||||
|
self.forward_host = forward_host
|
||||||
|
self.forward_port = forward_port
|
||||||
|
self.server_socket = None
|
||||||
|
self.running = False
|
||||||
|
self.clients = {}
|
||||||
|
self.remote_connections = {}
|
||||||
|
self.mqtt_client = MQTTClient()
|
||||||
|
self.pile_ids = {}
|
||||||
|
self.utils = ProxyUtils()
|
||||||
|
self.command_handler = Command0B0CH()
|
||||||
|
self.command_handler = Command02()
|
||||||
|
self.command_handler = Command03()
|
||||||
|
self.command_handler = Command07()
|
||||||
|
self.command_handler = Command08()
|
||||||
|
self.command_handler = Command09()
|
||||||
|
self.command_handler = Command0A()
|
||||||
|
self.command_handler = Command25()
|
||||||
|
self.command_handler = Command30()
|
||||||
|
self.command_handler = Command191A()
|
||||||
|
self.command_handler = Command2122()
|
||||||
|
self.command_handler = Command2324()
|
||||||
|
self.command_handler = Command2627()
|
||||||
|
|
||||||
|
# 存储登录信息的字典,以桩号为键
|
||||||
|
self.login_info = {}
|
||||||
|
# 存储对时信息的字典
|
||||||
|
self.time_sync_info = {}
|
||||||
|
# 存储遥信信息的字典
|
||||||
|
self.remote_signal_info = {}
|
||||||
|
# 存储故障信息的字典
|
||||||
|
self.fault_info = {}
|
||||||
|
# 存储心跳信息的字典
|
||||||
|
self.heartbeat_info = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动代理服务器"""
|
||||||
|
try:
|
||||||
|
self.mqtt_client.connect()
|
||||||
|
|
||||||
|
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.server_socket.bind((self.listen_host, self.listen_port))
|
||||||
|
self.server_socket.listen(5)
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
start_msg = f"代理服务器已启动,监听地址: {self.listen_host}:{self.listen_port}"
|
||||||
|
logging.info(start_msg)
|
||||||
|
print(start_msg)
|
||||||
|
self.mqtt_client.publish_message(start_msg)
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
client_socket, address = self.server_socket.accept()
|
||||||
|
client_msg = f"收到新的客户端连接,地址: {address}"
|
||||||
|
logging.info(client_msg)
|
||||||
|
print(client_msg)
|
||||||
|
self.mqtt_client.publish_message(client_msg)
|
||||||
|
|
||||||
|
client_thread = threading.Thread(target=self.handle_client,
|
||||||
|
args=(client_socket, address))
|
||||||
|
client_thread.daemon = True
|
||||||
|
client_thread.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"代理服务器错误: {str(e)}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
print(error_msg)
|
||||||
|
self.mqtt_client.publish_message(error_msg)
|
||||||
|
finally:
|
||||||
|
if self.server_socket:
|
||||||
|
self.server_socket.close()
|
||||||
|
|
||||||
|
def create_remote_connection(self, client_address):
|
||||||
|
"""创建与远程服务器的连接"""
|
||||||
|
try:
|
||||||
|
remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
remote_socket.connect((self.forward_host, self.forward_port))
|
||||||
|
|
||||||
|
connect_msg = f"已连接到远程服务器 {self.forward_host}:{self.forward_port}"
|
||||||
|
logging.info(connect_msg)
|
||||||
|
print(connect_msg)
|
||||||
|
self.mqtt_client.publish_message(connect_msg)
|
||||||
|
|
||||||
|
self.remote_connections[client_address] = remote_socket
|
||||||
|
return remote_socket
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"连接远程服务器失败: {str(e)}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
print(error_msg)
|
||||||
|
self.mqtt_client.publish_message(error_msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def forward_data(self, source_socket, destination_socket, source_address):
|
||||||
|
"""转发数据"""
|
||||||
|
try:
|
||||||
|
while self.running:
|
||||||
|
data = source_socket.recv(1024) # 接收数据
|
||||||
|
if not data:
|
||||||
|
break # 如果没有数据,退出循环
|
||||||
|
|
||||||
|
# 如果接收到的数据长度大于等于14字节,且以'JX'开头
|
||||||
|
if len(data) >= 14 and data[0:2] == b'JX':
|
||||||
|
command = data[2] # 提取命令字节
|
||||||
|
|
||||||
|
# 根据命令字节处理不同命令
|
||||||
|
if command == 0x01:
|
||||||
|
logging.info(f"处理 01H 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_01h(data)
|
||||||
|
|
||||||
|
elif command == 0x02:
|
||||||
|
logging.info(f"处理 02H 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_02h(data)
|
||||||
|
|
||||||
|
elif command == 0x03:
|
||||||
|
logging.info(f"处理 03H 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_03h(data)
|
||||||
|
|
||||||
|
elif command == 0x07:
|
||||||
|
logging.info(f"处理 07H 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_07h(data)
|
||||||
|
|
||||||
|
elif command == 0x08:
|
||||||
|
logging.info(f"处理 08H 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_08h(data)
|
||||||
|
|
||||||
|
elif command == 0x09:
|
||||||
|
logging.info(f"处理 09H 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_09h(data)
|
||||||
|
|
||||||
|
elif command == 0x0A:
|
||||||
|
logging.info(f"处理 0AH 命令,数据内容: {data.hex()}")
|
||||||
|
self.command_handler.parse_0Ah(data)
|
||||||
|
|
||||||
|
#其他命令待添加...
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 未知命令,日志记录
|
||||||
|
logging.warning(f"未知命令:{command:02X},数据内容: {data.hex()}")
|
||||||
|
|
||||||
|
# 将数据转发到远程或充电桩,判断方向
|
||||||
|
if source_socket not in self.remote_connections.values():
|
||||||
|
# 这里是判断是否是客户端连接,进行桩ID提取
|
||||||
|
pile_id = self.utils.extract_pile_id(data)
|
||||||
|
if pile_id:
|
||||||
|
self.pile_ids[source_address] = pile_id
|
||||||
|
|
||||||
|
# 获取本地和远程端口信息
|
||||||
|
source_local, source_remote = self.utils.get_socket_info(source_socket)
|
||||||
|
dest_local, dest_remote = self.utils.get_socket_info(destination_socket)
|
||||||
|
|
||||||
|
# 判断数据发送方向:是发送到远程服务器还是充电桩
|
||||||
|
is_to_remote = destination_socket in self.remote_connections.values()
|
||||||
|
direction = "发送到远程服务器" if is_to_remote else "发送到充电桩"
|
||||||
|
mqtt_direction = "u" if is_to_remote else "d"
|
||||||
|
|
||||||
|
# 发送数据到目的地
|
||||||
|
destination_socket.send(data)
|
||||||
|
|
||||||
|
# 记录数据转发日志
|
||||||
|
msg = f"数据转发成功: {direction} | 本地地址: {source_local} | 远程地址: {dest_remote} | 数据长度: {len(data)}"
|
||||||
|
logging.info(msg)
|
||||||
|
self.mqtt_client.publish_message(msg)
|
||||||
|
|
||||||
|
# 每次数据转发完成后,处理其他相关操作
|
||||||
|
# 例如检查是否需要断开连接等
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 异常处理,日志记录错误信息
|
||||||
|
logging.error(f"转发数据出错: {str(e)}")
|
||||||
|
print(f"转发数据出错: {str(e)}")
|
||||||
|
self.mqtt_client.publish_message(f"转发数据出错: {str(e)}")
|
||||||
|
# 若需要,可以选择关闭连接
|
||||||
|
if source_socket:
|
||||||
|
source_socket.close()
|
||||||
|
if destination_socket:
|
||||||
|
destination_socket.close()
|
||||||
|
|
||||||
|
def handle_client(self, client_socket, client_address):
|
||||||
|
"""处理客户端连接"""
|
||||||
|
try:
|
||||||
|
remote_socket = self.create_remote_connection(client_address)
|
||||||
|
if not remote_socket:
|
||||||
|
client_socket.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
forward_thread = threading.Thread(
|
||||||
|
target=self.forward_data,
|
||||||
|
args=(client_socket, remote_socket, client_address)
|
||||||
|
)
|
||||||
|
backward_thread = threading.Thread(
|
||||||
|
target=self.forward_data,
|
||||||
|
args=(remote_socket, client_socket, client_address)
|
||||||
|
)
|
||||||
|
|
||||||
|
forward_thread.daemon = True
|
||||||
|
backward_thread.daemon = True
|
||||||
|
|
||||||
|
forward_thread.start()
|
||||||
|
backward_thread.start()
|
||||||
|
|
||||||
|
forward_thread.join()
|
||||||
|
backward_thread.join()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"处理客户端连接错误: {str(e)}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
print(error_msg)
|
||||||
|
self.mqtt_client.publish_message(error_msg)
|
||||||
|
finally:
|
||||||
|
if client_address in self.remote_connections:
|
||||||
|
self.remote_connections[client_address].close()
|
||||||
|
del self.remote_connections[client_address]
|
||||||
|
if client_address in self.pile_ids:
|
||||||
|
del self.pile_ids[client_address]
|
||||||
|
client_socket.close()
|
||||||
|
close_msg = f"客户端连接已关闭 {client_address}"
|
||||||
|
logging.info(close_msg)
|
||||||
|
print(close_msg)
|
||||||
|
self.mqtt_client.publish_message(close_msg)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止代理服务器"""
|
||||||
|
self.running = False
|
||||||
|
if self.server_socket:
|
||||||
|
self.server_socket.close()
|
||||||
|
|
||||||
|
for remote_socket in self.remote_connections.values():
|
||||||
|
remote_socket.close()
|
||||||
|
self.remote_connections.clear()
|
||||||
|
self.pile_ids.clear()
|
||||||
|
|
||||||
|
self.mqtt_client.disconnect()
|
||||||
|
|
||||||
|
stop_msg = "代理服务器已停止"
|
||||||
|
logging.info(stop_msg)
|
||||||
|
print(stop_msg)
|
||||||
|
self.mqtt_client.publish_message(stop_msg)
|
34
charging_pile_proxy/core/utils.py
Normal file
34
charging_pile_proxy/core/utils.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class ProxyUtils:
|
||||||
|
@staticmethod
|
||||||
|
def format_hex_data(data):
|
||||||
|
"""格式化十六进制数据显示"""
|
||||||
|
return ' '.join([f"{b:02X}" for b in data])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_pile_id(data):
|
||||||
|
"""从数据包中提取桩号"""
|
||||||
|
try:
|
||||||
|
if len(data) > 10: # 确保数据包足够长
|
||||||
|
# 桩号在第5-8个字节
|
||||||
|
pile_id = ''.join([f"{b:02X}" for b in data[3:11]])
|
||||||
|
return pile_id
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_socket_info(socket_obj):
|
||||||
|
"""获取socket的本地和远程地址信息"""
|
||||||
|
try:
|
||||||
|
local_address = socket_obj.getsockname()
|
||||||
|
remote_address = socket_obj.getpeername()
|
||||||
|
return local_address, remote_address
|
||||||
|
except:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_current_time():
|
||||||
|
"""获取当前时间的格式化字符串"""
|
||||||
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
0
charging_pile_proxy/logs/command.log
Normal file
0
charging_pile_proxy/logs/command.log
Normal file
35
charging_pile_proxy/main.py
Normal file
35
charging_pile_proxy/main.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 将项目根目录添加到Python路径
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.append(current_dir)
|
||||||
|
|
||||||
|
from core.proxy_server import ChargingPileProxyServer
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 设置系统默认编码为UTF-8
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
filename='test.log',
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
server = ChargingPileProxyServer()
|
||||||
|
try:
|
||||||
|
server.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
server.stop()
|
||||||
|
msg = "代理服务器已完全关闭"
|
||||||
|
logging.info(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
18
charging_pile_proxy/requirements.txt
Normal file
18
charging_pile_proxy/requirements.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 充电桩代理服务器依赖
|
||||||
|
|
||||||
|
# 通信相关
|
||||||
|
paho-mqtt==1.6.1 # MQTT通信库
|
||||||
|
pyserial==3.5 # 串口通信(可选)
|
||||||
|
|
||||||
|
# 日志和配置
|
||||||
|
PyYAML==6.0.1 # YAML配置文件解析
|
||||||
|
loguru==0.7.2 # 增强日志库(可选)
|
||||||
|
|
||||||
|
# 开发和测试工具
|
||||||
|
pytest==7.4.3 # 单元测试框架
|
||||||
|
black==23.10.1 # 代码格式化
|
||||||
|
flake8==6.1.0 # 代码风格检查
|
||||||
|
mypy==1.6.1 # 静态类型检查
|
||||||
|
|
||||||
|
# 性能和系统监控(可选)
|
||||||
|
psutil==5.9.6 # 系统资源监控
|
6
charging_pile_proxy/test.log
Normal file
6
charging_pile_proxy/test.log
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
2025-01-17 14:13:57,200 - ERROR - 代理服务器错误: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
|
||||||
|
2025-01-17 14:20:03,831 - ERROR - 代理服务器错误: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
|
||||||
|
2025-01-17 14:22:45,907 - ERROR - 代理服务器错误: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
|
||||||
|
2025-01-17 14:41:09,050 - ERROR - 代理服务器错误: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
|
||||||
|
2025-01-17 14:58:36,033 - ERROR - 代理服务器错误: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
|
||||||
|
2025-01-17 17:12:27,913 - ERROR - 代理服务器错误: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
|
9
玖行原始报文/01.txt
Normal file
9
玖行原始报文/01.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
4A 58 帧起始
|
||||||
|
01 登录命令
|
||||||
|
03 17 00 65 21 81 24 86桩号
|
||||||
|
01 数据加密方式
|
||||||
|
10 00 数据域长度
|
||||||
|
18 09 18 0C 13 2F 时间标识
|
||||||
|
01 00 密钥版本
|
||||||
|
00 00 00 00 00 00 00 00 校验密文
|
||||||
|
5B校验码
|
0
玖行原始报文/02.txt
Normal file
0
玖行原始报文/02.txt
Normal file
9
玖行原始报文/07.txt
Normal file
9
玖行原始报文/07.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
4A 58
|
||||||
|
07
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
08 00
|
||||||
|
19 01 09 0C 14 0F 时间标识
|
||||||
|
01 对时结果1-成功;2-失败
|
||||||
|
00 失败原因0-无;1-数据格式异常
|
||||||
|
3B
|
8
玖行原始报文/08.txt
Normal file
8
玖行原始报文/08.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
4A 58 帧起始
|
||||||
|
08 故障命令
|
||||||
|
03 17 00 65 21 81 24 86桩号
|
||||||
|
01 数据加密方式
|
||||||
|
11 00 数据域长度
|
||||||
|
18 09 18 0C 13 31 时间格式
|
||||||
|
00 00 00 00 02 00 00 00 00 00 00
|
||||||
|
4E 校验码
|
8
玖行原始报文/09.txt
Normal file
8
玖行原始报文/09.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
4A 58 帧起始
|
||||||
|
09 遥信命令
|
||||||
|
03 17 00 65 21 81 24 86桩号
|
||||||
|
01 数据加密方式
|
||||||
|
13 00 数据域长度
|
||||||
|
18 09 18 0C 13 31
|
||||||
|
00 00 02 0B 01 01 00 00 01 01 01 00 00
|
||||||
|
47校验码
|
37
玖行原始报文/0A.txt
Normal file
37
玖行原始报文/0A.txt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
4A 58 帧起始
|
||||||
|
0A 命令
|
||||||
|
03 17 00 65 21 81 24 86 桩号
|
||||||
|
01 数据加密方式不加密
|
||||||
|
48 00 数据域长度
|
||||||
|
18 09 18 0C 14 09 时间标识
|
||||||
|
00 00 A相电压分辨率 0.1V
|
||||||
|
00 00 B相电压分辨率 0.1V
|
||||||
|
00 00 C相电压分辨率 0.1V
|
||||||
|
00 00 A相电流分辨率 0.01A
|
||||||
|
00 00 B相电流分辨率 0.01A
|
||||||
|
00 00 C相电流分辨率 0.01A
|
||||||
|
00 00 00 00 总电表电量分辨率 0.01kWh
|
||||||
|
32 桩内温度偏移量-50℃
|
||||||
|
32 进风口温度偏移量-50℃
|
||||||
|
32 出风口温度偏移量-50℃
|
||||||
|
32 控制板温度偏移量-50℃
|
||||||
|
00 桩内湿度0-100%RH
|
||||||
|
00 00 00 00 00 00 00 00预留 置 0
|
||||||
|
02 充电枪数量N1-30
|
||||||
|
00 00 1#-电表电压分辨率 0.1V
|
||||||
|
00 00 1#-电表电流分辨率 0.01A
|
||||||
|
48 E6 CB 00 电表电量分辨率 0.01kWh
|
||||||
|
00 00 1#-充电模块电压分辨率 0.1V
|
||||||
|
00 00 1#-充电模块电流分辨率 0.1A
|
||||||
|
00 1#-充电模块温度偏移量-50℃
|
||||||
|
55 1#-充电枪温度偏移量-50℃
|
||||||
|
00 00 00 00 预留置 0
|
||||||
|
00 00 N#-电表电压分辨率 0.1V
|
||||||
|
00 00 N#-电表电流分辨率 0.01A
|
||||||
|
12 68 BC 00 电表电量分辨率 0.01kWh
|
||||||
|
00 00 N#-充电模块电压分辨率 0.1V
|
||||||
|
00 00 N#-充电模块电流分辨率 0.1A
|
||||||
|
00 N#-充电模块温度偏移量-50℃
|
||||||
|
54 1#-充电枪温度偏移量-50℃
|
||||||
|
00 00 00 00 N#-预留置 0
|
||||||
|
88尾字节校验码
|
0
玖行原始报文/0B.txt
Normal file
0
玖行原始报文/0B.txt
Normal file
7
玖行原始报文/0C.txt
Normal file
7
玖行原始报文/0C.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
4A 58
|
||||||
|
0B
|
||||||
|
03 17 00 65 21 81 24 86
|
||||||
|
01
|
||||||
|
07 00
|
||||||
|
18 09 18 0C 13 30 00
|
||||||
|
58
|
11
玖行原始报文/19.txt
Normal file
11
玖行原始报文/19.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
4A 58
|
||||||
|
19
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
16 00
|
||||||
|
19 01 09 0C 15 2B 时间标识
|
||||||
|
65 36 39 61 32 31 30 33 00 00 00 00 00 00 00 00 卡号
|
||||||
|
14
|
||||||
|
|
||||||
|
|
||||||
|
|
12
玖行原始报文/1A.txt
Normal file
12
玖行原始报文/1A.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
4A 58
|
||||||
|
1A
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
1D 00
|
||||||
|
19 01 09 0C 15 2E 时间标识
|
||||||
|
65 36 39 61 32 31 30 33 00 00 00 00 00 00 00 00 卡号
|
||||||
|
A5 0E 0D 00 卡余额
|
||||||
|
01 允许充电标志
|
||||||
|
00 不可充电原因
|
||||||
|
01 计费模型选择-使用本地计费模型
|
||||||
|
BF
|
48
玖行原始报文/21.txt
Normal file
48
玖行原始报文/21.txt
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
每隔 5 秒发送一次直至收到回复“启动充电结果(22H) ”或本次充电结束
|
||||||
|
|
||||||
|
|
||||||
|
4A 58
|
||||||
|
21
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
B3 00
|
||||||
|
19 01 09 0B 25 13 (时间标识)
|
||||||
|
01 (枪号)
|
||||||
|
31 38 37 37 31 39 37 38 30 31 36 31 35 35 35 36 36 31 30 00 00 00 00 00 00 00 00 00 00 00 00 00 (充电订单号)
|
||||||
|
38 34 30 34 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (用户ID)
|
||||||
|
16 00 (用户类型)
|
||||||
|
00 00 00 00 00 00 00 00 00(组织机构代码)
|
||||||
|
00 00 00 00 00 00 00 00 00 (车牌号)
|
||||||
|
03 (控制方式)
|
||||||
|
F4 01 00 00 (控制参数)
|
||||||
|
01 (充电模式)
|
||||||
|
02 (充电桩类型)
|
||||||
|
01 (启动结果)
|
||||||
|
00 00 (启动失败原因)
|
||||||
|
19 01 09 0B 24 2D (充电起始时间)
|
||||||
|
BE 6C 68 01 (充电起始电量)
|
||||||
|
00 00 (绝缘检测电压)
|
||||||
|
00 00 (DC+绝缘值)
|
||||||
|
00 00 (DC-绝缘值)
|
||||||
|
00 00 00 (协议版本)
|
||||||
|
03 (电池类型)
|
||||||
|
D0 11 (额定容量)
|
||||||
|
26 18 (额定总电压)
|
||||||
|
00 00 00 00 (电池厂商)
|
||||||
|
00 00 00 00 (电池组序号)
|
||||||
|
00 (电池生产年)
|
||||||
|
00 (电池生产月)
|
||||||
|
00 (电池生产日)
|
||||||
|
00 00 00 (充电次数)
|
||||||
|
00 (电池产权)
|
||||||
|
00 (预留)
|
||||||
|
4C 5A 47 4A 4C 4D 34 34 35 50 58 31 31 34 35 33 37 (VIN)
|
||||||
|
01 01 00 00 00 00 00 00 (软件版本)
|
||||||
|
77 01 (单体允许电压)
|
||||||
|
DC 05 (最高充电电流)
|
||||||
|
03 0B (标称总容量)
|
||||||
|
60 1B (最高充电电压)
|
||||||
|
73 (最高允许温度)
|
||||||
|
DA 02 (SOC)
|
||||||
|
E5 18 (当前电池电压)
|
||||||
|
95
|
8
玖行原始报文/22.txt
Normal file
8
玖行原始报文/22.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
4A 58
|
||||||
|
22
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
07 00
|
||||||
|
19 01 09 0B 25 16 时间标识
|
||||||
|
01 枪号
|
||||||
|
3E
|
38
玖行原始报文/23.txt
Normal file
38
玖行原始报文/23.txt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
用于告知平台充电完成及上报本次充电订单,每隔 5 秒发送一次直至收到“ 回 复最新充电订单(24H) ”或发送次数超过 10 次,若断网或始终未收到 24H 报文 则将最新充电订单并入历史充电订单。
|
||||||
|
|
||||||
|
|
||||||
|
4A 58
|
||||||
|
23
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
A3 00
|
||||||
|
19 01 09 0B 25 1E(时间标识)
|
||||||
|
01 (枪号)
|
||||||
|
C2 0A 00 00 (记录索引号)
|
||||||
|
31 38 37 37 31 39 37 38 30 31 36 31 35 35 35 36 36 31 30 00 00 00 00 00 00 00 00 00 00 00 00 00 (充电订单号)
|
||||||
|
38 34 30 34 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (用户ID)
|
||||||
|
16 00 (用户类型)
|
||||||
|
00 00 00 00 00 00 00 00 00(组织机构代码)
|
||||||
|
00 00 00 00 (充点卡余额)
|
||||||
|
4C 5A 47 4A 4C 4D 34 34 35 50 58 31 31 34 35 33 37 (VIN)
|
||||||
|
19 01 09 0B 24 2D (开始充电时间)
|
||||||
|
19 01 09 0B 25 1B (结束充电时间)
|
||||||
|
BE 6C 68 01 (开始充电电量)
|
||||||
|
C6 6C 68 01 (结束充电电量)
|
||||||
|
49 (开始充电soc)
|
||||||
|
49 (结束充电soc)
|
||||||
|
03 (控制方式)
|
||||||
|
F4 01 00 00 (控制参数)
|
||||||
|
04 (启动类型)
|
||||||
|
19 01 09 0B 24 2F (定时启动时间)
|
||||||
|
01 (充电模式)
|
||||||
|
F9 03 (停止充电原因)
|
||||||
|
01 (计费模型选择)
|
||||||
|
1B 00 (计费模型版本)
|
||||||
|
03 00 00 00 (电能费用)
|
||||||
|
03 00 00 00 (服务费费用)
|
||||||
|
00 00 00 00 (停车费费用)
|
||||||
|
01 (时间段数量N)
|
||||||
|
03 (段1计费模型索引)
|
||||||
|
08 00 (段1电量)
|
||||||
|
2F
|
9
玖行原始报文/24.txt
Normal file
9
玖行原始报文/24.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
4A 58
|
||||||
|
24
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
0B 00
|
||||||
|
19 01 09 0B 25 20 时间标识
|
||||||
|
01 枪号
|
||||||
|
C2 0A 00 00 记录索引号
|
||||||
|
CA
|
26
玖行原始报文/25.txt
Normal file
26
玖行原始报文/25.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
4A 58
|
||||||
|
25
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
61 00
|
||||||
|
19 01 09 0B 25 13 (时间标识)
|
||||||
|
01(枪号)
|
||||||
|
DA 07 (充电电压)
|
||||||
|
00 00 (充电电流)
|
||||||
|
00 00 00 00 *(充电电量)
|
||||||
|
22 00 00 00 (充电时长)
|
||||||
|
00 00 00 00 (充电金额)
|
||||||
|
00 (充电模块接入数量)
|
||||||
|
00 00 00 00 (充电电费金额)
|
||||||
|
00 00 00 00 (充电服务费金额)
|
||||||
|
31 38 37 37 31 39 37 38 30 31 36 31 35 35 35 36 36 31 30 00 00 00 00 00 00 00 00 00 00 00 00 00 (充电订单号)
|
||||||
|
01 (时间段数量)
|
||||||
|
19 01 09 0B 24 2D(段1开始时间)
|
||||||
|
19 01 09 0B 25 13 (段1结束时间)
|
||||||
|
32 0F 00 00 (段1电价)
|
||||||
|
AC 0D 00 00 (段1服务费价格)
|
||||||
|
00 00 00 00 (段1电量)
|
||||||
|
00 00 00 00 (段1电费)
|
||||||
|
00 00 00 00 (段1服务费)
|
||||||
|
3B 4A 58 30 03 17 66 56 11 36 06 37 01 1C 00 19 01 09 0B 25 13 01 00 00 A0 0F 02 DA 07 A0 0F 00 00 00 00 00 00 00 00 00 00 00 00 (N段)
|
||||||
|
ED
|
8
玖行原始报文/26.txt
Normal file
8
玖行原始报文/26.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
4A 58
|
||||||
|
26
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
07 00
|
||||||
|
19 01 09 0B 25 1C时间标识
|
||||||
|
01枪号
|
||||||
|
30
|
8
玖行原始报文/27.txt
Normal file
8
玖行原始报文/27.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
4A 58
|
||||||
|
27
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
07 00
|
||||||
|
19 01 09 0B 25 1A 时间标识
|
||||||
|
01 枪号
|
||||||
|
37
|
22
玖行原始报文/30.txt
Normal file
22
玖行原始报文/30.txt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
4A 58
|
||||||
|
30
|
||||||
|
03 17 66 56 11 36 06 37
|
||||||
|
01
|
||||||
|
1C 00
|
||||||
|
19 01 09 0B 25 15 (时间标识)
|
||||||
|
01 (枪号)
|
||||||
|
60 1B (电压需求)
|
||||||
|
8D 07 (电流需求)
|
||||||
|
02 (充电模式)
|
||||||
|
DA 07 (充电电压)
|
||||||
|
A0 0F (充电电流)
|
||||||
|
4A C1 (最高单体电压,最高单体所在组号)
|
||||||
|
49 (soc)
|
||||||
|
78 00 (剩余充电时间)
|
||||||
|
01 (最高单体电压所在编号)
|
||||||
|
40 (最高动力蓄电池温度)
|
||||||
|
03 (最高温度监测点编号)
|
||||||
|
3C (最低动力蓄电池温度)
|
||||||
|
05 (最低温度监测点编号)
|
||||||
|
00 10(单体电压过高,soc过高,充电过流,动力蓄电池温度过高,动力蓄电池绝缘状态,输出连接器连接状态,充电允许)
|
||||||
|
64
|
13119
玖行原始报文/玖行充电桩原始报文.log
Normal file
13119
玖行原始报文/玖行充电桩原始报文.log
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user