no message

This commit is contained in:
MATRIX\29620 2025-01-18 09:10:52 +08:00
commit b94aca789f
70 changed files with 16746 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View File

@ -0,0 +1 @@
main.py

11
.idea/analysis.iml generated Normal file
View 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>

View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View 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` 文件中,并同时输出到控制台。
## 许可证
[待添加]
## 作者
[赵子逸/玄驹易维]

View File

View 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()

View 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)

View 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)

View 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)

View 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)

View 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)

View 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())

View 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())

View 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响应。")

View 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)

View 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响应。")

View 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)

View 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())

View 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 # 日志文件备份数量

View File

View File

View File

View 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()

View 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)

View 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]

View File

View 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()

View 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 # 系统资源监控

View 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] 以一种访问权限不允许的方式做了一个访问套接字的尝试。

View 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校验码

View File

View 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

View 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 校验码

View 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
View 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尾字节校验码

View File

View 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
View 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
View 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
View 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

View 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
View 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

View 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
View 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

View 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

View 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
View 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

File diff suppressed because it is too large Load Diff