feat: migrate to pymodbus 3.11.4 and update bioyond configs

PyModbus 3.x Migration:
- Copied modbus.py and client.py from dev branch for compatibility
- Rewrote FLOAT32 decoding using struct module in coin_cell_assembly.py
- Fixed STRING decoding for QR codes (battery and electrolyte barcodes)
- Tested successfully on hardware with correct data decoding

Bioyond Studio Updates:
- Updated bioyond_studio config.py
- Modified bioyond_cell_workstation.py
- Enhanced warehouse.py and decks.py
- Added README_WAREHOUSE.md documentation

Parameter Enhancements:
- Enhanced coin_cell_workstation.yaml parameter descriptions
- Added matrix position ranges and indexing rules

Breaking changes:
- Requires pymodbus >= 3.9.0
- Removed deprecated BinaryPayloadDecoder/BinaryPayloadBuilder
- Updated to use client.convert_from/to_registers() methods
This commit is contained in:
Andy6M
2026-01-10 17:01:40 +08:00
parent 936834f8c3
commit f355722281
29 changed files with 1460 additions and 8006 deletions

View File

@@ -1,26 +1,12 @@
# coding=utf-8
from enum import Enum
from abc import ABC, abstractmethod
from typing import Tuple, Union, Optional, TYPE_CHECKING
from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian
if TYPE_CHECKING:
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient
# Define DataType enum for pymodbus 2.5.3 compatibility
class DataType(Enum):
INT16 = "int16"
UINT16 = "uint16"
INT32 = "int32"
UINT32 = "uint32"
INT64 = "int64"
UINT64 = "uint64"
FLOAT32 = "float32"
FLOAT64 = "float64"
STRING = "string"
BOOL = "bool"
from pymodbus.client import ModbusBaseSyncClient
from pymodbus.client.mixin import ModbusClientMixin
from typing import Tuple, Union, Optional
DataType = ModbusClientMixin.DATATYPE
class WorderOrder(Enum):
BIG = "big"
@@ -33,96 +19,8 @@ class DeviceType(Enum):
INPUT_REGISTER = 'input_register'
def _convert_from_registers(registers, data_type: DataType, word_order: str = 'big'):
"""Convert registers to a value using BinaryPayloadDecoder.
Args:
registers: List of register values
data_type: DataType enum specifying the target data type
word_order: 'big' or 'little' endian
Returns:
Converted value
"""
# Determine byte and word order based on word_order parameter
if word_order == 'little':
byte_order = Endian.Little
word_order_enum = Endian.Little
else:
byte_order = Endian.Big
word_order_enum = Endian.Big
decoder = BinaryPayloadDecoder.fromRegisters(registers, byteorder=byte_order, wordorder=word_order_enum)
if data_type == DataType.INT16:
return decoder.decode_16bit_int()
elif data_type == DataType.UINT16:
return decoder.decode_16bit_uint()
elif data_type == DataType.INT32:
return decoder.decode_32bit_int()
elif data_type == DataType.UINT32:
return decoder.decode_32bit_uint()
elif data_type == DataType.INT64:
return decoder.decode_64bit_int()
elif data_type == DataType.UINT64:
return decoder.decode_64bit_uint()
elif data_type == DataType.FLOAT32:
return decoder.decode_32bit_float()
elif data_type == DataType.FLOAT64:
return decoder.decode_64bit_float()
elif data_type == DataType.STRING:
return decoder.decode_string(len(registers) * 2)
else:
raise ValueError(f"Unsupported data type: {data_type}")
def _convert_to_registers(value, data_type: DataType, word_order: str = 'little'):
"""Convert a value to registers using BinaryPayloadBuilder.
Args:
value: Value to convert
data_type: DataType enum specifying the source data type
word_order: 'big' or 'little' endian
Returns:
List of register values
"""
# Determine byte and word order based on word_order parameter
if word_order == 'little':
byte_order = Endian.Little
word_order_enum = Endian.Little
else:
byte_order = Endian.Big
word_order_enum = Endian.Big
builder = BinaryPayloadBuilder(byteorder=byte_order, wordorder=word_order_enum)
if data_type == DataType.INT16:
builder.add_16bit_int(value)
elif data_type == DataType.UINT16:
builder.add_16bit_uint(value)
elif data_type == DataType.INT32:
builder.add_32bit_int(value)
elif data_type == DataType.UINT32:
builder.add_32bit_uint(value)
elif data_type == DataType.INT64:
builder.add_64bit_int(value)
elif data_type == DataType.UINT64:
builder.add_64bit_uint(value)
elif data_type == DataType.FLOAT32:
builder.add_32bit_float(value)
elif data_type == DataType.FLOAT64:
builder.add_64bit_float(value)
elif data_type == DataType.STRING:
builder.add_string(value)
else:
raise ValueError(f"Unsupported data type: {data_type}")
return builder.to_registers()
class Base(ABC):
def __init__(self, client, name: str, address: int, typ: DeviceType, data_type):
def __init__(self, client: ModbusBaseSyncClient, name: str, address: int, typ: DeviceType, data_type: DataType):
self._address: int = address
self._client = client
self._name = name
@@ -160,11 +58,7 @@ class Coil(Base):
count = value,
slave = slave)
# 检查是否读取出错
if resp.isError():
return [], True
return resp.bits, False
return resp.bits, resp.isError()
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
if isinstance(value, list):
@@ -197,18 +91,8 @@ class DiscreteInputs(Base):
count = value,
slave = slave)
# 检查是否读取出错
if resp.isError():
# 根据数据类型返回默认值
if data_type in [DataType.FLOAT32, DataType.FLOAT64]:
return 0.0, True
elif data_type == DataType.STRING:
return "", True
else:
return 0, True
# noinspection PyTypeChecker
return _convert_from_registers(resp.registers, data_type, word_order=word_order.value), False
return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError()
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
raise ValueError('discrete inputs only support read')
@@ -228,19 +112,8 @@ class HoldRegister(Base):
address = self.address,
count = value,
slave = slave)
# 检查是否读取出错
if resp.isError():
# 根据数据类型返回默认值
if data_type in [DataType.FLOAT32, DataType.FLOAT64]:
return 0.0, True
elif data_type == DataType.STRING:
return "", True
else:
return 0, True
# noinspection PyTypeChecker
return _convert_from_registers(resp.registers, data_type, word_order=word_order.value), False
return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError()
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
@@ -259,7 +132,7 @@ class HoldRegister(Base):
return self._client.write_register(self.address, value, slave= slave).isError()
else:
# noinspection PyTypeChecker
encoder_resp = _convert_to_registers(value, data_type=data_type, word_order=word_order.value)
encoder_resp = self._client.convert_to_registers(value, data_type=data_type, word_order=word_order.value)
return self._client.write_registers(self.address, encoder_resp, slave=slave).isError()
@@ -280,19 +153,8 @@ class InputRegister(Base):
address = self.address,
count = value,
slave = slave)
# 检查是否读取出错
if resp.isError():
# 根据数据类型返回默认值
if data_type in [DataType.FLOAT32, DataType.FLOAT64]:
return 0.0, True
elif data_type == DataType.STRING:
return "", True
else:
return 0, True
# noinspection PyTypeChecker
return _convert_from_registers(resp.registers, data_type, word_order=word_order.value), False
return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError()
def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool:
raise ValueError('input register only support read')