1104更新
parent
57b755f1e6
commit
9fd733fa8a
Binary file not shown.
@ -0,0 +1,23 @@
|
|||||||
|
/* ==================== HART Tab标签页样式 ==================== */
|
||||||
|
QTabBar#hartTabBar::tab {
|
||||||
|
font-family: "PingFangSC-Medium", "Microsoft YaHei", sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 160px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar#hartTabBar::tab:hover {
|
||||||
|
background-color: #E3F2FD;
|
||||||
|
color: #2277EF;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar#hartTabBar::tab:selected {
|
||||||
|
background-color: #2277EF;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
HART界面组件模块
|
||||||
|
提供HART协议相关的PyQt界面组件
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .HartMainWindow import HartMainWindow
|
||||||
|
from .HartConnectionWidget import HartConnectionWidget
|
||||||
|
from .HartDeviceInfoWidget import HartDeviceInfoWidget
|
||||||
|
from .HartDeviceConfigWidget import HartDeviceConfigWidget
|
||||||
|
from .HartSensorConfigWidget import HartSensorConfigWidget
|
||||||
|
from .HartCalibrationWidget import HartCalibrationWidget
|
||||||
|
from .HartVariableInfoWidget import HartVariableInfoWidget
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'HartMainWindow',
|
||||||
|
'HartConnectionWidget',
|
||||||
|
'HartDeviceInfoWidget',
|
||||||
|
'HartDeviceConfigWidget',
|
||||||
|
'HartSensorConfigWidget',
|
||||||
|
'HartCalibrationWidget',
|
||||||
|
'HartVariableInfoWidget'
|
||||||
|
]
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
HART通信工具主程序
|
||||||
|
基于PyQt的现代化HART通信界面应用程序
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from PyQt5.QtCore import QFile, QTextStream
|
||||||
|
from UI.HartWidgets.HartMainWindow import HartMainWindow
|
||||||
|
|
||||||
|
def loadStyleSheet(sheetName):
|
||||||
|
"""
|
||||||
|
加载样式表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sheetName: 样式表文件名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
样式表内容
|
||||||
|
"""
|
||||||
|
file = QFile(sheetName)
|
||||||
|
file.open(QFile.ReadOnly | QFile.Text)
|
||||||
|
stream = QTextStream(file)
|
||||||
|
return stream.readAll()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
主函数
|
||||||
|
"""
|
||||||
|
# 创建应用程序
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 设置应用程序样式
|
||||||
|
styleSheetPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style.qss")
|
||||||
|
if os.path.exists(styleSheetPath):
|
||||||
|
app.setStyleSheet(loadStyleSheet(styleSheetPath))
|
||||||
|
|
||||||
|
# 创建主窗口
|
||||||
|
mainWindow = HartMainWindow()
|
||||||
|
mainWindow.setWindowTitle("HART通信工具")
|
||||||
|
mainWindow.resize(800, 600)
|
||||||
|
mainWindow.show()
|
||||||
|
|
||||||
|
# 运行应用程序
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
UI模块初始化文件
|
||||||
|
"""
|
||||||
@ -0,0 +1 @@
|
|||||||
|
__version__ = "2023.6.0"
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
"""A sans-io python implementation of the Highway Addressable Remote Transducer Protocol."""
|
||||||
|
|
||||||
|
from .__version__ import *
|
||||||
|
|
||||||
|
# 延迟导入以避免循环依赖
|
||||||
|
def _get_hart_communication():
|
||||||
|
from .HARTCommunication import HARTCommunication, BurstModeON, BurstModeOFF, PrimaryMasterMode, SecondaryMasterMode
|
||||||
|
return HARTCommunication, BurstModeON, BurstModeOFF, PrimaryMasterMode, SecondaryMasterMode
|
||||||
|
|
||||||
|
# 导出常用模块
|
||||||
|
from . import common
|
||||||
|
from . import universal
|
||||||
|
from . import tools
|
||||||
|
from ._unpacker import *
|
||||||
|
|
||||||
|
# 导出主要类和常量
|
||||||
|
HARTCommunication, BurstModeON, BurstModeOFF, PrimaryMasterMode, SecondaryMasterMode = _get_hart_communication()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'HARTCommunication',
|
||||||
|
'BurstModeON',
|
||||||
|
'BurstModeOFF',
|
||||||
|
'PrimaryMasterMode',
|
||||||
|
'SecondaryMasterMode',
|
||||||
|
'common',
|
||||||
|
'universal',
|
||||||
|
'tools'
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
"""Define version."""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
from .VERSION import __version__
|
||||||
|
|
||||||
|
|
||||||
|
here = pathlib.Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["__version__", "__branch__"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
__branch__ = (
|
||||||
|
subprocess.run(["git", "branch", "--show-current"], capture_output=True, cwd=here)
|
||||||
|
.stdout.strip()
|
||||||
|
.decode()
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
__branch__ = ""
|
||||||
|
|
||||||
|
if __branch__:
|
||||||
|
__version__ += "+" + __branch__
|
||||||
@ -0,0 +1,261 @@
|
|||||||
|
import struct
|
||||||
|
from typing import MutableMapping, Union
|
||||||
|
from . import tools
|
||||||
|
|
||||||
|
|
||||||
|
def parse(response: bytes) -> MutableMapping[str, Union[int, bytes, str, float]]:
|
||||||
|
# print(response, 1111111111111)
|
||||||
|
out: MutableMapping[str, Union[int, bytes, str, float]] = dict()
|
||||||
|
out["full_response"] = response
|
||||||
|
if response[0] & 0x80: # long address
|
||||||
|
out["address"] = int.from_bytes(response[1:6], "big")
|
||||||
|
response = response[6:]
|
||||||
|
else: # short address
|
||||||
|
out["address"] = response[1]
|
||||||
|
response = response[2:]
|
||||||
|
command, bytecount, response_code, device_status = struct.unpack_from(">BBBB", response)
|
||||||
|
out["device_status"] = device_status
|
||||||
|
out["response_code"] = response_code
|
||||||
|
|
||||||
|
if response_code == 0:
|
||||||
|
out["status"] = "success"
|
||||||
|
else:
|
||||||
|
out["status"] = "error"
|
||||||
|
out["error"] = f"Device responded with error code: {response_code}"
|
||||||
|
|
||||||
|
data = response[4 : 4 + bytecount]
|
||||||
|
out["command"] = command
|
||||||
|
out["command_name"] = f"hart_command_{command}"
|
||||||
|
out["bytecount"] = bytecount
|
||||||
|
out["data"] = data
|
||||||
|
|
||||||
|
# handle error return
|
||||||
|
if bytecount == 2:
|
||||||
|
return out
|
||||||
|
|
||||||
|
# universal commands
|
||||||
|
if command in [0, 11]:
|
||||||
|
out["command_name"] = "read_unique_identifier"
|
||||||
|
out["manufacturer_id"] = data[1]
|
||||||
|
out["manufacturer_device_type"] = data[2]
|
||||||
|
out["number_response_preamble_characters"] = data[3]
|
||||||
|
out["universal_command_revision_level"] = data[4]
|
||||||
|
out["transmitter_specific_command_revision_level"] = data[5]
|
||||||
|
out["software_revision_level"] = data[6]
|
||||||
|
out["hardware_revision_level"] = data[7]
|
||||||
|
out["device_id"] = int.from_bytes(data[9:12], "big")
|
||||||
|
elif command in [1]:
|
||||||
|
out["command_name"] = "read_primary_variable"
|
||||||
|
units, variable = struct.unpack_from(">Bf", data)
|
||||||
|
out["primary_variable_units"] = units
|
||||||
|
out["primary_variable"] = variable
|
||||||
|
elif command in [2]:
|
||||||
|
out["command_name"] = "read_loop_current_and_percent"
|
||||||
|
analog_signal, primary_variable = struct.unpack_from(">ff", data)
|
||||||
|
out["analog_signal"] = analog_signal
|
||||||
|
out["primary_variable"] = primary_variable
|
||||||
|
elif command in [3]:
|
||||||
|
out["command_name"] = "read_dynamic_variables_and_loop_current"
|
||||||
|
if len(data) >= 4:
|
||||||
|
analog_signal = struct.unpack_from(">f", data, 0)[0]
|
||||||
|
out["analog_signal"] = analog_signal
|
||||||
|
|
||||||
|
# 计算剩余数据长度并确定支持的变量数量
|
||||||
|
remaining_length = len(data) - 4
|
||||||
|
variable_count = remaining_length // 5 # 每个变量占5字节 (1字节单位 + 4字节值)
|
||||||
|
|
||||||
|
# 动态解析变量数据
|
||||||
|
offset = 4 # 从第4字节开始是变量数据
|
||||||
|
for i in range(variable_count):
|
||||||
|
if offset + 5 > len(data): # 确保有足够数据
|
||||||
|
break
|
||||||
|
|
||||||
|
# 解析单位枚举值 (1字节)
|
||||||
|
unit_enum = struct.unpack_from(">B", data, offset)[0]
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
# 解析变量值 (4字节)
|
||||||
|
variable_value = struct.unpack_from(">f", data, offset)[0]
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
# 根据变量索引设置字段名
|
||||||
|
if i == 0:
|
||||||
|
out["primary_variable_units"] = unit_enum
|
||||||
|
out["primary_variable"] = variable_value
|
||||||
|
elif i == 1:
|
||||||
|
out["secondary_variable_units"] = unit_enum
|
||||||
|
out["secondary_variable"] = variable_value
|
||||||
|
elif i == 2:
|
||||||
|
out["tertiary_variable_units"] = unit_enum
|
||||||
|
out["tertiary_variable"] = variable_value
|
||||||
|
elif i == 3:
|
||||||
|
out["quaternary_variable_units"] = unit_enum
|
||||||
|
out["quaternary_variable"] = variable_value
|
||||||
|
elif command in [6]:
|
||||||
|
out["command_name"] = "write_polling_address"
|
||||||
|
polling_address = struct.unpack_from(">B", data)[0]
|
||||||
|
out["polling_address"] = polling_address
|
||||||
|
elif command in [12]:
|
||||||
|
# print(data)
|
||||||
|
out["command_name"] = "read_message"
|
||||||
|
out["message"] = data[0:24]
|
||||||
|
# print(out, 111111111)
|
||||||
|
elif command in [13]:
|
||||||
|
out["command_name"] = "read_tag_descriptor_date"
|
||||||
|
# Tag is 8 chars packed into 6 bytes
|
||||||
|
out["device_tag_name"] = tools.unpack_packed_ascii(data[0:6], 8)
|
||||||
|
# Descriptor is 16 chars packed into 12 bytes
|
||||||
|
out["device_descriptor"] = tools.unpack_packed_ascii(data[6:18], 16)
|
||||||
|
# Date is 3 bytes: day, month, year (year is offset from 1900)
|
||||||
|
day, month, year_offset = struct.unpack_from(">BBB", data, 18)
|
||||||
|
out["date"] = {"day": day, "month": month, "year": year_offset + 1900}
|
||||||
|
elif command in [14]:
|
||||||
|
out["command_name"] = "read_primary_variable_information"
|
||||||
|
out["serial_no"] = data[0:3]
|
||||||
|
sensor_limits_code, upper_limit, lower_limit, min_span = struct.unpack_from(
|
||||||
|
">xxxBfff", data
|
||||||
|
)
|
||||||
|
out["sensor_limits_code"] = sensor_limits_code
|
||||||
|
out["upper_limit"] = upper_limit
|
||||||
|
out["lower_limit"] = lower_limit
|
||||||
|
out["min_span"] = min_span
|
||||||
|
elif command in [15]:
|
||||||
|
out["command_name"] = "read_output_information"
|
||||||
|
(
|
||||||
|
alarm_code,
|
||||||
|
transfer_fn_code,
|
||||||
|
primary_variable_range_code,
|
||||||
|
upper_range_value,
|
||||||
|
lower_range_value,
|
||||||
|
damping_value,
|
||||||
|
write_protect,
|
||||||
|
private_label,
|
||||||
|
) = struct.unpack_from(">BBBfffBB", data)
|
||||||
|
out["alarm_code"] = alarm_code
|
||||||
|
out["transfer_fn_code"] = transfer_fn_code
|
||||||
|
out["primary_variable_range_code"] = primary_variable_range_code
|
||||||
|
out["upper_range_value"] = upper_range_value
|
||||||
|
out["lower_range_value"] = lower_range_value
|
||||||
|
out["damping_value"] = damping_value
|
||||||
|
out["write_protect"] = write_protect
|
||||||
|
out["private_label"] = private_label
|
||||||
|
elif command in [16]:
|
||||||
|
out["command_name"] = "read_final_assembly_number"
|
||||||
|
# print(data)
|
||||||
|
out["final_assembly_no"] = int.from_bytes(data[0:3], "big")
|
||||||
|
elif command in [17]:
|
||||||
|
out["command_name"] = "write_message"
|
||||||
|
out["message"] = data[0:24]
|
||||||
|
elif command in [18]:
|
||||||
|
out["command_name"] = "write_tag_descriptor_date"
|
||||||
|
out["device_tag_name"] = data[0:6]
|
||||||
|
out["device_descriptor"] = data[6:18]
|
||||||
|
out["date"] = data[18:21]
|
||||||
|
elif command in [19]:
|
||||||
|
out["command_name"] = "write_final_assembly_number"
|
||||||
|
out["final_assembly_no"] = int.from_bytes(data[0:2], "big")
|
||||||
|
elif command in [34]:
|
||||||
|
out["command_name"] = "write_primary_variable_damping"
|
||||||
|
out["damping_time"] = struct.unpack_from(">f", data)[0]
|
||||||
|
elif command in [35]:
|
||||||
|
out["command_name"] = "write_primary_variable_range"
|
||||||
|
out["upper_range"] = struct.unpack_from(">f", data)[0]
|
||||||
|
out["lower_range"] = struct.unpack_from(">f", data[4:])[0]
|
||||||
|
elif command in [36]:
|
||||||
|
out["command_name"] = "calibrate_20ma"
|
||||||
|
# 20mA校准命令无返回数据
|
||||||
|
elif command in [37]:
|
||||||
|
out["command_name"] = "calibrate_4ma"
|
||||||
|
# 4mA校准命令无返回数据
|
||||||
|
elif command in [40]:
|
||||||
|
out["command_name"] = "set_fixed_current_output"
|
||||||
|
if len(data) > 0:
|
||||||
|
out["fixed_output_enabled"] = data[0] == 1
|
||||||
|
if len(data) > 1:
|
||||||
|
out["fixed_current_value"] = struct.unpack_from(">f", data[1:])[0]
|
||||||
|
elif command in [43]:
|
||||||
|
out["command_name"] = "calibrate_zero_point"
|
||||||
|
# 零点校准命令无返回数据
|
||||||
|
elif command in [44]:
|
||||||
|
out["command_name"] = "write_primary_variable_units"
|
||||||
|
out["units_code"] = data[0]
|
||||||
|
elif command in [45]:
|
||||||
|
out["command_name"] = "trim_loop_current_4ma"
|
||||||
|
out["measured_current"] = struct.unpack_from(">f", data)[0]
|
||||||
|
elif command in [46]:
|
||||||
|
out["command_name"] = "trim_loop_current_20ma"
|
||||||
|
out["measured_current"] = struct.unpack_from(">f", data)[0]
|
||||||
|
elif command in [47]:
|
||||||
|
out["command_name"] = "write_primary_variable_output_function"
|
||||||
|
out["transfer_function_code"] = data[0]
|
||||||
|
|
||||||
|
|
||||||
|
# COMMON COMMANDS
|
||||||
|
|
||||||
|
# elif command in [37]:
|
||||||
|
# out["command_name"] = "set_primary_variable_lower_range_value"
|
||||||
|
# out[""] =
|
||||||
|
# request data bytes = NONE, response data bytes = NONE
|
||||||
|
|
||||||
|
# elif command in [38]:
|
||||||
|
# out["command_name"] = "reset_configuration_changed_flag"
|
||||||
|
# out[""] =
|
||||||
|
# request data bytes = NONE, response data bytes = NONE
|
||||||
|
|
||||||
|
# elif command in [42]:
|
||||||
|
# out["command_name"] = "perform_master_reset"
|
||||||
|
# out[""] =
|
||||||
|
# request data bytes = NONE, response data bytes = NONE
|
||||||
|
|
||||||
|
# elif command in [48]:
|
||||||
|
# out["command_name"] = "read_additional_transmitter_status"
|
||||||
|
# out[""] =
|
||||||
|
# request data bytes = NONE, response data bytes = NONE
|
||||||
|
|
||||||
|
elif command in [50]:
|
||||||
|
out["command_name"] = "read_dynamic_variable_assignments"
|
||||||
|
(
|
||||||
|
primary_transmitter_variable,
|
||||||
|
secondary_transmitter_variable,
|
||||||
|
tertiary_transmitter_variable,
|
||||||
|
quaternary_transmitter_variable,
|
||||||
|
) = struct.unpack_from(">BBBB", data)
|
||||||
|
out["primary_transmitter_variable"] = primary_transmitter_variable
|
||||||
|
out["secondary_transmitter_variable"] = secondary_transmitter_variable
|
||||||
|
out["tertiary_transmitter_variable"] = tertiary_transmitter_variable # NOT USED
|
||||||
|
out["quaternary_transmitter_variable"] = quaternary_transmitter_variable # NOT USED
|
||||||
|
elif command in [59]:
|
||||||
|
out["command_name"] = "write_number_of_response_preambles"
|
||||||
|
n_response_preambles = struct.unpack_from(">B", data)[0]
|
||||||
|
out["n_response_preambles"] = n_response_preambles
|
||||||
|
elif command in [66]:
|
||||||
|
out["command_name"] = "toggle_analog_output_mode"
|
||||||
|
(
|
||||||
|
analog_output_selection,
|
||||||
|
analog_output_units_code,
|
||||||
|
fixed_analog_output,
|
||||||
|
) = struct.unpack_from(">BBf", data)
|
||||||
|
out["analog_output_selection"] = analog_output_selection
|
||||||
|
out["analog_output_units_code"] = analog_output_units_code
|
||||||
|
out["fixed_analog_output"] = fixed_analog_output
|
||||||
|
elif command in [67]:
|
||||||
|
out["command_name"] = "trim_analog_output_zero"
|
||||||
|
analog_output_code, analog_output_units_code, measured_analog_output = struct.unpack_from(
|
||||||
|
">BBf", data
|
||||||
|
)
|
||||||
|
out["analog_output_code"] = analog_output_code
|
||||||
|
out["analog_output_units_code"] = analog_output_units_code
|
||||||
|
out["measured_analog_output"] = measured_analog_output
|
||||||
|
elif command in [68]:
|
||||||
|
out["command_name"] = "trim_analog_output_span"
|
||||||
|
analog_output_code, analog_output_units_code, measured_analog_output = struct.unpack_from(
|
||||||
|
">BBf", data
|
||||||
|
)
|
||||||
|
out["analog_output_code"] = analog_output_code
|
||||||
|
out["analog_output_units_code"] = analog_output_units_code
|
||||||
|
out["measured_analog_output"] = measured_analog_output
|
||||||
|
elif command in [123]:
|
||||||
|
out["command_name"] = "select_baud_rate"
|
||||||
|
out["baud_rate"] = int.from_bytes(data, "big")
|
||||||
|
|
||||||
|
return out
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
__all__ = ["Unpacker"]
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections import namedtuple
|
||||||
|
import io
|
||||||
|
import struct
|
||||||
|
from tabnanny import check
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from ._parsing import parse
|
||||||
|
from . import tools
|
||||||
|
|
||||||
|
|
||||||
|
class Unpacker:
|
||||||
|
"""
|
||||||
|
Create an Unpacker to decode a byte stream into HART protocol messages.
|
||||||
|
|
||||||
|
The ``file_like`` parameter should be an object which data can be sourced from.
|
||||||
|
It should support the ``read()`` method.
|
||||||
|
|
||||||
|
The ``on_error`` parameter selects the action to take if invalid data is detected.
|
||||||
|
If set to ``"continue"`` (the default), bytes will be discarded if the byte sequence
|
||||||
|
does not appear to be a valid message.
|
||||||
|
If set to ``"warn"``, the behaviour is identical, but a warning message will be emitted.
|
||||||
|
To instead immediately abort the stream decoding and raise a ``RuntimeError``, set to
|
||||||
|
``"raise"``.
|
||||||
|
|
||||||
|
:param file_like: A file-like object which data can be `read()` from.
|
||||||
|
:param on_error: Action to take if invalid data is detected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file_like=None, on_error="continue"):
|
||||||
|
if file_like is None:
|
||||||
|
self._file = io.BytesIO()
|
||||||
|
else:
|
||||||
|
self._file = file_like
|
||||||
|
self.buf = b""
|
||||||
|
self.on_error = on_error
|
||||||
|
# print(self._file)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _decoding_error(self, message="Error decoding message from buffer."):
|
||||||
|
"""
|
||||||
|
Take appropriate action if parsing of data stream fails.
|
||||||
|
|
||||||
|
:param message: Warning or error message string.
|
||||||
|
"""
|
||||||
|
if self.on_error == "raise":
|
||||||
|
raise RuntimeError(message)
|
||||||
|
if self.on_error == "warn":
|
||||||
|
warnings.warn(message)
|
||||||
|
|
||||||
|
def _read_one_byte_if_possible(self):
|
||||||
|
if self._file.in_waiting > 0:
|
||||||
|
# print(1)
|
||||||
|
return self._file.read(1)
|
||||||
|
else:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
# must work with at least two bytes to start with
|
||||||
|
while len(self.buf) < 3:
|
||||||
|
self.buf += self._read_one_byte_if_possible()
|
||||||
|
# keep reading until we find a minimum preamble
|
||||||
|
# print(self.buf, 11)
|
||||||
|
while self.buf[:3] not in [b"\xFF\xFF\x06", b"\xFF\xFF\x86"]:
|
||||||
|
self.buf += self._read_one_byte_if_possible()
|
||||||
|
# print(self.buf)
|
||||||
|
self.buf = self.buf[1:]
|
||||||
|
self._decoding_error("Head of buffer not recognized as valid preamble")
|
||||||
|
# now the head of our buffer is the start charachter plus two preamble
|
||||||
|
# we will read all the way through status
|
||||||
|
if self.buf[2] & 0x80:
|
||||||
|
l = 12
|
||||||
|
else:
|
||||||
|
l = 8
|
||||||
|
while len(self.buf) < l:
|
||||||
|
self.buf += self._read_one_byte_if_possible()
|
||||||
|
# now we can use the bytecount to read through the data and checksum
|
||||||
|
#
|
||||||
|
# print(self.buf)
|
||||||
|
# print(type(self.buf[l - 4]), 222)
|
||||||
|
if self.buf[l - 4] == 15: # 对command15进行特殊操作 修复报文错误
|
||||||
|
# bytecount = 19
|
||||||
|
self.buf = self.buf[:l - 3] + b'\x13\x00\x00\x00'
|
||||||
|
bytecount = self.buf[l - 3]
|
||||||
|
response_length = l + bytecount - 1
|
||||||
|
# print(self.buf, bytecount)
|
||||||
|
|
||||||
|
while len(self.buf) < response_length:
|
||||||
|
self.buf += self._read_one_byte_if_possible()
|
||||||
|
# checksum
|
||||||
|
# print(self.buf)
|
||||||
|
checksum = int.from_bytes(
|
||||||
|
tools.calculate_checksum(self.buf[2 : response_length - 1]), "big"
|
||||||
|
)
|
||||||
|
# print(self.buf)
|
||||||
|
if checksum != self.buf[response_length - 1]:
|
||||||
|
# print(66666666)
|
||||||
|
self._decoding_error("Invalid checksum.")
|
||||||
|
raise StopIteration
|
||||||
|
# print(self.buf)
|
||||||
|
# parse
|
||||||
|
response = self.buf[2:response_length]
|
||||||
|
# print(response, 'test')
|
||||||
|
# print(response)
|
||||||
|
dict_ = parse(response)
|
||||||
|
# clear buffer
|
||||||
|
if len(self.buf) == response_length:
|
||||||
|
self.buf = b""
|
||||||
|
else:
|
||||||
|
self.buf = self.buf[response_length + 3 :]
|
||||||
|
# return
|
||||||
|
return dict_
|
||||||
|
|
||||||
|
def __aiter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return next(self)
|
||||||
|
except StopIteration:
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
|
|
||||||
|
def feed(self, data: bytes):
|
||||||
|
"""
|
||||||
|
Add byte data to the input stream.
|
||||||
|
|
||||||
|
The input stream must support random access, if it does not, must be fed externally
|
||||||
|
(e.g. serial port data).
|
||||||
|
|
||||||
|
:param data: Byte array containing data to add.
|
||||||
|
"""
|
||||||
|
pos = self._file.tell()
|
||||||
|
self._file.seek(0, 2)
|
||||||
|
self._file.write(data)
|
||||||
|
self._file.seek(pos)
|
||||||
@ -0,0 +1,267 @@
|
|||||||
|
from . import tools
|
||||||
|
|
||||||
|
TRANSFER_FUNCTION_CODE = {
|
||||||
|
0: "线性 (Linear)",
|
||||||
|
1: "平方根 (Square Root)",
|
||||||
|
2: "平方根三次方 (Square Root 3rd Power)",
|
||||||
|
3: "平方根五次方 (Square Root 5th Power)",
|
||||||
|
4: "特殊曲线 (Special Curve) - 不推荐使用",
|
||||||
|
5: "平方 (Square)",
|
||||||
|
6: "带截止的平方根 (Square root with cut-off)",
|
||||||
|
10: "等百分比 1:25 (Equal Percentage 1:25)",
|
||||||
|
11: "等百分比 1:33 (Equal Percentage 1:33)",
|
||||||
|
12: "等百分比 1:50 (Equal Percentage 1:50)",
|
||||||
|
15: "快开 1:25 (Quick Open 1:25)",
|
||||||
|
16: "快开 1:33 (Quick Open 1:33)",
|
||||||
|
17: "快开 1:50 (Quick Open 1:50)",
|
||||||
|
30: "双曲线 (Hyperbolic) - Shape Factor 0.10",
|
||||||
|
31: "双曲线 (Hyperbolic) - Shape Factor 0.20",
|
||||||
|
32: "双曲线 (Hyperbolic) - Shape Factor 0.30",
|
||||||
|
34: "双曲线 (Hyperbolic) - Shape Factor 0.50",
|
||||||
|
37: "双曲线 (Hyperbolic) - Shape Factor 0.70",
|
||||||
|
40: "双曲线 (Hyperbolic) - Shape Factor 1.00",
|
||||||
|
41: "双曲线 (Hyperbolic) - Shape Factor 1.50",
|
||||||
|
42: "双曲线 (Hyperbolic) - Shape Factor 2.00",
|
||||||
|
43: "双曲线 (Hyperbolic) - Shape Factor 3.00",
|
||||||
|
44: "双曲线 (Hyperbolic) - Shape Factor 4.00",
|
||||||
|
45: "双曲线 (Hyperbolic) - Shape Factor 5.00",
|
||||||
|
100: "平底罐 (Flat bottom tank)",
|
||||||
|
101: "锥形或金字塔形底罐 (Conical or pyramidal bottom tank)",
|
||||||
|
102: "抛物线形底罐 (Parabolic bottom tank)",
|
||||||
|
103: "球形底罐 (Spherical bottom tank)",
|
||||||
|
104: "斜底罐 (Angled bottom tank)",
|
||||||
|
105: "平端圆柱罐 (Flat end cylinder tank)",
|
||||||
|
106: "抛物线端圆柱罐 (Parabolic end cylinder tank)",
|
||||||
|
107: "球形罐 (Spherical tank)",
|
||||||
|
230: "离散/开关 (Discrete/Switch)",
|
||||||
|
250: "未使用 (Not Used)",
|
||||||
|
251: "无 (None)",
|
||||||
|
252: "未知 (Unknown)",
|
||||||
|
253: "特殊 (Special)"
|
||||||
|
}
|
||||||
|
REVERSE_TRANSFER_FUNCTION_CODE = {v: k for k, v in TRANSFER_FUNCTION_CODE.items()}
|
||||||
|
|
||||||
|
UNITS_CODE = {
|
||||||
|
# Pressure
|
||||||
|
1: "inH2O @ 68 F",
|
||||||
|
2: "inHg @ 0 C",
|
||||||
|
3: "ftH2O @ 68 F",
|
||||||
|
4: "mmH2O @ 68 F",
|
||||||
|
5: "mmHg @ 0 C",
|
||||||
|
6: "psi",
|
||||||
|
7: "bar",
|
||||||
|
8: "mbar",
|
||||||
|
9: "g/cm2",
|
||||||
|
10: "kg/cm2",
|
||||||
|
11: "Pa",
|
||||||
|
12: "kPa",
|
||||||
|
13: "torr",
|
||||||
|
14: "atm",
|
||||||
|
145: "inH2O @ 60F",
|
||||||
|
237: "MPa",
|
||||||
|
238: "inH2O @ 4C",
|
||||||
|
239: "mmH2O @ 4C",
|
||||||
|
# Temperature
|
||||||
|
32: "deg C",
|
||||||
|
33: "deg F",
|
||||||
|
34: "deg R",
|
||||||
|
35: "K",
|
||||||
|
# Volumetric Flow
|
||||||
|
15: "cu ft/min",
|
||||||
|
16: "gal/min",
|
||||||
|
17: "liter/min",
|
||||||
|
18: "imp gal/min",
|
||||||
|
19: "cu m/hr",
|
||||||
|
22: "gal/sec",
|
||||||
|
23: "Mgal/day",
|
||||||
|
24: "liter/sec",
|
||||||
|
25: "Ml/day",
|
||||||
|
26: "cu ft/sec",
|
||||||
|
27: "cu ft/day",
|
||||||
|
28: "cu m/sec",
|
||||||
|
29: "cu m/day",
|
||||||
|
30: "imp gal/hr",
|
||||||
|
31: "imp gal/day",
|
||||||
|
121: "std cu m/hr",
|
||||||
|
122: "std liter/hr",
|
||||||
|
123: "std cu ft/min",
|
||||||
|
130: "cu ft/hr",
|
||||||
|
131: "cu m/min",
|
||||||
|
132: "bbl/sec",
|
||||||
|
133: "bbl/min",
|
||||||
|
134: "bbl/hr",
|
||||||
|
135: "bbl/day",
|
||||||
|
136: "gal/hr",
|
||||||
|
137: "imp gal/sec",
|
||||||
|
235: "gal/day",
|
||||||
|
# Velocity
|
||||||
|
20: "ft/sec",
|
||||||
|
21: "m/sec",
|
||||||
|
114: "in/sec",
|
||||||
|
115: "in/min",
|
||||||
|
116: "ft/min",
|
||||||
|
120: "m/hr",
|
||||||
|
# Volume
|
||||||
|
40: "gal",
|
||||||
|
41: "liter",
|
||||||
|
42: "imp gal",
|
||||||
|
43: "cu m",
|
||||||
|
46: "bbl",
|
||||||
|
110: "bushel",
|
||||||
|
111: "cu yd",
|
||||||
|
112: "cu ft",
|
||||||
|
113: "cu in",
|
||||||
|
134: "bbl liq",
|
||||||
|
166: "std cu m",
|
||||||
|
167: "std l",
|
||||||
|
168: "std cu ft",
|
||||||
|
236: "hectoliter",
|
||||||
|
# Mass
|
||||||
|
60: "g",
|
||||||
|
61: "kg",
|
||||||
|
62: "metric ton",
|
||||||
|
63: "lb",
|
||||||
|
64: "short ton",
|
||||||
|
65: "long ton",
|
||||||
|
125: "oz",
|
||||||
|
# Mass Flow
|
||||||
|
70: "g/sec",
|
||||||
|
71: "g/min",
|
||||||
|
72: "g/hr",
|
||||||
|
73: "kg/sec",
|
||||||
|
74: "kg/min",
|
||||||
|
75: "kg/hr",
|
||||||
|
76: "kg/day",
|
||||||
|
77: "metric ton/min",
|
||||||
|
78: "metric ton/hr",
|
||||||
|
79: "metric ton/day",
|
||||||
|
80: "lb/sec",
|
||||||
|
81: "lb/min",
|
||||||
|
82: "lb/hr",
|
||||||
|
83: "lb/day",
|
||||||
|
84: "short ton/min",
|
||||||
|
85: "short ton/hr",
|
||||||
|
86: "short ton/day",
|
||||||
|
87: "long ton/hr",
|
||||||
|
88: "long ton/day",
|
||||||
|
# Mass per Volume
|
||||||
|
90: "SGU",
|
||||||
|
91: "g/cu cm",
|
||||||
|
92: "kg/cu m",
|
||||||
|
93: "lb/gal",
|
||||||
|
94: "lb/cu ft",
|
||||||
|
95: "g/ml",
|
||||||
|
96: "kg/liter",
|
||||||
|
97: "g/liter",
|
||||||
|
98: "lb/cu in",
|
||||||
|
99: "short ton/cu yd",
|
||||||
|
100: "deg Twaddell",
|
||||||
|
102: "deg Baume",
|
||||||
|
103: "deg API",
|
||||||
|
104: "deg API",
|
||||||
|
146: "ug/liter",
|
||||||
|
147: "ug/cu m",
|
||||||
|
# Viscosity
|
||||||
|
54: "cSt",
|
||||||
|
55: "cP",
|
||||||
|
# Electric Potential
|
||||||
|
36: "mV",
|
||||||
|
58: "V",
|
||||||
|
# Electric Current
|
||||||
|
39: "mA",
|
||||||
|
# Electric Resistance
|
||||||
|
37: "ohm",
|
||||||
|
163: "kohm",
|
||||||
|
# Energy (includes Work)
|
||||||
|
69: "J",
|
||||||
|
89: "dtherm",
|
||||||
|
126: "kWh",
|
||||||
|
128: "MWh",
|
||||||
|
162: "kcal",
|
||||||
|
164: "MJ",
|
||||||
|
165: "Btu",
|
||||||
|
# Power
|
||||||
|
127: "kW",
|
||||||
|
129: "hp",
|
||||||
|
140: "Mcal/hr",
|
||||||
|
141: "MJ/hr",
|
||||||
|
142: "Btu/hr",
|
||||||
|
# Radial Velocity
|
||||||
|
117: "deg/sec",
|
||||||
|
118: "rev/sec",
|
||||||
|
119: "rpm",
|
||||||
|
# Miscellaneous
|
||||||
|
38: "Hz",
|
||||||
|
57: "percent",
|
||||||
|
59: "pH",
|
||||||
|
101: "deg Balling",
|
||||||
|
105: "percent solids/wt",
|
||||||
|
106: "percent solids/vol",
|
||||||
|
107: "deg Plato",
|
||||||
|
108: "proof/vol",
|
||||||
|
109: "proof/mass",
|
||||||
|
139: "ppm",
|
||||||
|
148: "percent consistency",
|
||||||
|
149: "vol percent",
|
||||||
|
150: "percent steam qual",
|
||||||
|
152: "cu ft/lb",
|
||||||
|
153: "pF",
|
||||||
|
154: "ml/liter",
|
||||||
|
155: "ul/liter",
|
||||||
|
156: "dB",
|
||||||
|
160: "deg Brix",
|
||||||
|
161: "percent LEL",
|
||||||
|
169: "ppb",
|
||||||
|
# Generic & Reserved
|
||||||
|
240: "Manufacturer specific",
|
||||||
|
241: "Manufacturer specific",
|
||||||
|
242: "Manufacturer specific",
|
||||||
|
243: "Manufacturer specific",
|
||||||
|
244: "Manufacturer specific",
|
||||||
|
245: "Manufacturer specific",
|
||||||
|
246: "Manufacturer specific",
|
||||||
|
247: "Manufacturer specific",
|
||||||
|
248: "Manufacturer specific",
|
||||||
|
249: "Manufacturer specific",
|
||||||
|
250: "Not Used",
|
||||||
|
251: "Unknown",
|
||||||
|
252: "Unknown",
|
||||||
|
253: "Special"
|
||||||
|
}
|
||||||
|
REVERSE_UNITS_CODE = {v: k for k, v in UNITS_CODE.items()}
|
||||||
|
|
||||||
|
def get_unit_description(code):
|
||||||
|
"""根据代码获取单位描述"""
|
||||||
|
return UNITS_CODE.get(code, f"未知代码({code})")
|
||||||
|
|
||||||
|
def set_primary_variable_lower_range_value(address: bytes, value) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=37)
|
||||||
|
|
||||||
|
def reset_configuration_changed_flag(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=38)
|
||||||
|
|
||||||
|
def perform_master_reset(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=42)
|
||||||
|
|
||||||
|
def read_additional_transmitter_status(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=48)
|
||||||
|
|
||||||
|
def read_dynamic_variable_assignments(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=50)
|
||||||
|
|
||||||
|
def write_number_of_response_preambles(address: bytes, number: int) -> bytes:
|
||||||
|
data = number.to_bytes(1, "big")
|
||||||
|
return tools.pack_command(address, command_id=59, data=data)
|
||||||
|
|
||||||
|
def toggle_analog_output_mode(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=66)
|
||||||
|
|
||||||
|
def trim_analog_output_zero(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=67)
|
||||||
|
|
||||||
|
def trim_analog_output_span(address: bytes) -> bytes:
|
||||||
|
return tools.pack_command(address, command_id=68)
|
||||||
|
|
||||||
|
def select_baud_rate(address: bytes, rate: int) -> bytes:
|
||||||
|
data = rate.to_bytes(1, "big")
|
||||||
|
return tools.pack_command(address, command_id=123, data=data)
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
import math
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_checksum(command: Union[int, bytes]) -> bytes:
|
||||||
|
# print(command)
|
||||||
|
if type(command) == int:
|
||||||
|
command = command.to_bytes(64, "big") # type: ignore
|
||||||
|
lrc = 0
|
||||||
|
for byte in command: # type: ignore
|
||||||
|
lrc ^= byte
|
||||||
|
out = lrc.to_bytes(1, "big")
|
||||||
|
# print(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_long_address(manufacturer_id: int, manufacturer_device_type: int, device_id: bytes):
|
||||||
|
out = int.from_bytes(device_id, "big")
|
||||||
|
out |= manufacturer_device_type << 24
|
||||||
|
out |= manufacturer_id << 32
|
||||||
|
return out.to_bytes(5, "big")
|
||||||
|
|
||||||
|
|
||||||
|
def pack_command(address, command_id, data=None):
|
||||||
|
# if type(address) == bytes:
|
||||||
|
# address = int.from_bytes(address, "big")
|
||||||
|
if type(command_id) == int:
|
||||||
|
command_id = command_id.to_bytes(1, "big")
|
||||||
|
command = b"\xFF\xFF\xFF\xFF\xFF\xFF" # preamble
|
||||||
|
command += b"\x82" if len(address) >= 5 else b"\x02"
|
||||||
|
command += address
|
||||||
|
command += command_id
|
||||||
|
if data is None:
|
||||||
|
command += b"\x00" # byte count
|
||||||
|
else:
|
||||||
|
# print(len(data), 22222222222)
|
||||||
|
command += len(data).to_bytes(1, "big") # byte count
|
||||||
|
command += data # data
|
||||||
|
# print(command[6:])
|
||||||
|
command += calculate_checksum(command[6:])
|
||||||
|
# print(command)
|
||||||
|
return command
|
||||||
|
|
||||||
|
|
||||||
|
PACKED_ASCII_MAP = {
|
||||||
|
0x00: '@', 0x01: 'A', 0x02: 'B', 0x03: 'C', 0x04: 'D', 0x05: 'E', 0x06: 'F', 0x07: 'G',
|
||||||
|
0x08: 'H', 0x09: 'I', 0x0A: 'J', 0x0B: 'K', 0x0C: 'L', 0x0D: 'M', 0x0E: 'N', 0x0F: 'O',
|
||||||
|
0x10: 'P', 0x11: 'Q', 0x12: 'R', 0x13: 'S', 0x14: 'T', 0x15: 'U', 0x16: 'V', 0x17: 'W',
|
||||||
|
0x18: 'X', 0x19: 'Y', 0x1A: 'Z', 0x1B: '[', 0x1C: '\\', 0x1D: ']', 0x1E: '^', 0x1F: '_', # 这里应该是 '_' 而不是 '-'
|
||||||
|
0x20: ' ', 0x21: '!', 0x22: '"', 0x23: '=', 0x24: '$', 0x25: '%', 0x26: '&', 0x27: '`', # 注意这里是反引号 '`'
|
||||||
|
0x28: '(', 0x29: ')', 0x2A: '*', 0x2B: '+', 0x2C: ',', 0x2D: '-', 0x2E: '.', 0x2F: '/',
|
||||||
|
0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6', 0x37: '7',
|
||||||
|
0x38: '8', 0x39: '9', 0x3A: ':', 0x3B: ';', 0x3C: '<', 0x3D: '=', 0x3E: '>', 0x3F: '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_packed_ascii(data: bytes, expected_len: int) -> str:
|
||||||
|
"""Unpacks a byte array into a string using the HART Packed ASCII 6-bit encoding by processing in chunks."""
|
||||||
|
chars = []
|
||||||
|
# Process data in 3-byte chunks, as 4 characters (24 bits) fit into 3 bytes (24 bits)
|
||||||
|
for i in range(0, len(data), 3):
|
||||||
|
chunk = data[i:i+3]
|
||||||
|
|
||||||
|
# Pad chunk to 3 bytes if it's a partial chunk at the end
|
||||||
|
if len(chunk) < 3:
|
||||||
|
chunk += b'\x00' * (3 - len(chunk))
|
||||||
|
|
||||||
|
byte1, byte2, byte3 = chunk
|
||||||
|
|
||||||
|
# Unpack 4 6-bit characters from the 3 8-bit bytes
|
||||||
|
c1 = byte1 >> 2
|
||||||
|
c2 = ((byte1 & 0x03) << 4) | (byte2 >> 4)
|
||||||
|
c3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6)
|
||||||
|
c4 = byte3 & 0x3F
|
||||||
|
|
||||||
|
codes = [c1, c2, c3, c4]
|
||||||
|
for code in codes:
|
||||||
|
chars.append(PACKED_ASCII_MAP.get(code, '?'))
|
||||||
|
|
||||||
|
# Join and then trim to the exact expected length. Do not rstrip() as spaces can be valid.
|
||||||
|
return "".join(chars[:expected_len])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Build a reverse map for packing, handling duplicates deterministically
|
||||||
|
REVERSE_PACKED_ASCII_MAP = {}
|
||||||
|
# Iterate through the original map, sorted by code value (key)
|
||||||
|
for code, char in sorted(PACKED_ASCII_MAP.items(), key=lambda item: item[0]):
|
||||||
|
# This ensures that for characters with multiple codes, the one with the highest code value is chosen.
|
||||||
|
REVERSE_PACKED_ASCII_MAP[char] = code
|
||||||
|
|
||||||
|
|
||||||
|
def pack_packed_ascii(text: Union[str, bytes], expected_len: int) -> bytes:
|
||||||
|
"""
|
||||||
|
Packs a string or bytes into a HART Packed ASCII byte array.
|
||||||
|
This function is the symmetrical inverse of the unpack_packed_ascii function.
|
||||||
|
"""
|
||||||
|
# Defensively decode if bytes are passed in
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
try:
|
||||||
|
# Try decoding with ascii, fall back to latin-1 which never fails
|
||||||
|
text = text.decode('ascii')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
text = text.decode('latin-1')
|
||||||
|
|
||||||
|
# 1. Pad/truncate the input string to the exact expected length
|
||||||
|
padded_text = text.ljust(expected_len, ' ')
|
||||||
|
padded_text = padded_text[:expected_len]
|
||||||
|
|
||||||
|
# 2. Convert all characters to their corresponding 6-bit codes
|
||||||
|
codes = [REVERSE_PACKED_ASCII_MAP.get(c, 0x3F) for c in padded_text] # Default to '?' (0x3F)
|
||||||
|
|
||||||
|
packed_bytes = bytearray()
|
||||||
|
|
||||||
|
# 3. Process the codes in chunks of 4
|
||||||
|
for i in range(0, len(codes), 4):
|
||||||
|
# Get a chunk of up to 4 codes.
|
||||||
|
chunk = codes[i:i+4]
|
||||||
|
|
||||||
|
# If it's a partial chunk at the end, pad with space codes to make a full chunk of 4.
|
||||||
|
while len(chunk) < 4:
|
||||||
|
chunk.append(REVERSE_PACKED_ASCII_MAP[' '])
|
||||||
|
|
||||||
|
c1, c2, c3, c4 = chunk
|
||||||
|
|
||||||
|
# 4. Pack the 4 6-bit codes into 3 8-bit bytes, mirroring the unpacking logic
|
||||||
|
byte1 = (c1 << 2) | (c2 >> 4)
|
||||||
|
byte2 = ((c2 & 0x0F) << 4) | (c3 >> 2)
|
||||||
|
byte3 = ((c3 & 0x03) << 6) | c4
|
||||||
|
|
||||||
|
packed_bytes.extend([byte1, byte2, byte3])
|
||||||
|
|
||||||
|
# 5. Calculate the exact number of bytes required for the original expected length
|
||||||
|
num_bytes = math.ceil(expected_len * 6 / 8)
|
||||||
|
|
||||||
|
# 6. Get the core byte array, trimmed to the precise required size
|
||||||
|
result = bytes(packed_bytes[:num_bytes])
|
||||||
|
|
||||||
|
# For Command 17 (Write Message), the spec requires a fixed 24-byte data field.
|
||||||
|
# This corresponds to an expected character length of 32.
|
||||||
|
if expected_len == 32:
|
||||||
|
# Pad the result to exactly 24 bytes.
|
||||||
|
return result.ljust(24, b'\x00')
|
||||||
|
|
||||||
|
return result
|
||||||
Loading…
Reference in New Issue