From f355722281f0a7e2f89ae4e5d02cf0c70d20a22d Mon Sep 17 00:00:00 2001 From: Andy6M Date: Sat, 10 Jan 2026 17:01:40 +0800 Subject: [PATCH] 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 --- unilabos/device_comms/modbus_plc/client.py | 5 +- unilabos/device_comms/modbus_plc/modbus.py | 158 +- .../bioyond_cell/202601081.xlsx | Bin 0 -> 10083 bytes .../bioyond_cell/202601083.xlsx | Bin 0 -> 10130 bytes .../bioyond_cell/20260108_multi_batch1.xlsx | Bin 0 -> 10380 bytes .../bioyond_cell/20260108_multi_batch2.xlsx | Bin 0 -> 10367 bytes .../bioyond_cell/202601091.xlsx | Bin 0 -> 10095 bytes .../bioyond_cell_workstation-2.py | 1234 ---- .../bioyond_cell/bioyond_cell_workstation.py | 416 +- .../bioyond_cell/material_template.xlsx | Bin 10675 -> 10674 bytes .../bioyond_cell/smiles&molweight.py | 12 + .../workstation/bioyond_studio/config.py | 40 + .../coin_cell_assembly/coin_cell_assembly.py | 178 +- .../coin_cell_assembly_1105.csv | 64 - .../coin_cell_assembly_1223.csv | 159 - .../coin_cell_assembly/date_20251224.csv | 2 - .../coin_cell_assembly/date_20251225.csv | 2 - .../coin_cell_assembly/date_20251229.csv | 2 - .../coin_cell_assembly/date_20251230.csv | 9 - .../coin_cell_assembly/date_20260106.csv | 3 - .../coin_cell_assembly/date_20260110.csv | 6 + .../interactive_battery_export_demo.py | 536 -- unilabos/registry/devices/bioyond_cell.yaml | 73 + .../devices/coin_cell_workstation.yaml | 125 +- unilabos/registry/devices/virtual_device.yaml | 5794 ----------------- .../resources/bioyond/README_WAREHOUSE.md | 548 ++ unilabos/resources/bioyond/YB_warehouses.py | 85 +- unilabos/resources/bioyond/decks.py | 10 +- unilabos/resources/warehouse.py | 5 +- 29 files changed, 1460 insertions(+), 8006 deletions(-) create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601081.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601083.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch1.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch2.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601091.xlsx delete mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation-2.py create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv create mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py create mode 100644 unilabos/resources/bioyond/README_WAREHOUSE.md diff --git a/unilabos/device_comms/modbus_plc/client.py b/unilabos/device_comms/modbus_plc/client.py index 4b46746..c239b9d 100644 --- a/unilabos/device_comms/modbus_plc/client.py +++ b/unilabos/device_comms/modbus_plc/client.py @@ -4,7 +4,8 @@ import traceback from typing import Any, Union, List, Dict, Callable, Optional, Tuple from pydantic import BaseModel -from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient +from pymodbus.client import ModbusSerialClient, ModbusTcpClient +from pymodbus.framer import FramerType from typing import TypedDict from unilabos.device_comms.modbus_plc.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder @@ -402,7 +403,7 @@ class TCPClient(BaseClient): class RTUClient(BaseClient): def __init__(self, port: str, baudrate: int, timeout: int): super().__init__() - self._set_client(ModbusSerialClient(method='rtu', port=port, baudrate=baudrate, timeout=timeout)) + self._set_client(ModbusSerialClient(framer=FramerType.RTU, port=port, baudrate=baudrate, timeout=timeout)) self._connect() if __name__ == '__main__': diff --git a/unilabos/device_comms/modbus_plc/modbus.py b/unilabos/device_comms/modbus_plc/modbus.py index 7e384d7..028477e 100644 --- a/unilabos/device_comms/modbus_plc/modbus.py +++ b/unilabos/device_comms/modbus_plc/modbus.py @@ -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') diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601081.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601081.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..16d76547f5f39f662c83bf4f0bc91944f4a2c686 GIT binary patch literal 10083 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 AR{#J2 literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601083.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601083.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a6dd057f7ca4821ada75a47255291893774745ed GIT binary patch literal 10130 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 B9L)d# literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3bd3439954b0fd2a9f47bff32832ab6f4c081d5d GIT binary patch literal 10380 zcmeHtWm_EU()GYVaCZnAB)Ge4(BK-}2{3~@1SeQ2sXGA-r4e; zot?AaUvPGR=$X0tUUhZ%)T&i?RjDWd;cx*@0EhqpfD%xIy6?y4f(Luaa&t;ug`CGndwKH)m(on(vFhBpNwk1k{t1RQ zycLTB2L-KG@bM1vgph1Y5-}bkBco``bbwAiD2&@Syc@Xl{8c3^BY9^*X8#Mu=d1nv z7!oYKPp=P+9R>>A;oiybaV>b9!$s>5Gi1$wKbez*3o7+k!FMZ73&)H~4vbfCciO`! zSivfpNwuwf$m7aeP~{M*Y_5^y)=cgw3(qr! z-F8pQ#4?AH6{x}E{a_2Pb)m*YQ2wDn32NkIVdu=s z^4s}eV*U@a@ux>GjaO0tvSWrq0v{X4nfnSn!N7woqy;R`)t~QDX0mJ69{GfV!*FZ|M8mp*t!wAQ_$B z!-rjn?WCHT3{6NHFmwCnfDgX5HDB5p=gms0jL!~RD+flA4v2Ly_lmtr*lbLDoELlS zOZDwPQievw=rRom08l_B4g||>S2HXCoXZe~+;AI}dc8rBLm>$~HL-lq@ zsu2tCDgkl?TsBVZm=oBfSaDL(`KAcc%93Fqg(}ke9{<${SYccgHe*4%)sEEZi`tIL z_wk1_7i=CMjT5MD_BQTLgtoTZsNu{p5B^-5LrdM5WSJa{0vDU3c*O%$_)J?$}NA@SI zPW(8Gj7y}u1=0m5Wx5!`?w;mre^F-`b-8AEg*>blB zJ$%CK)6%e+TDUgj=1`Q1VDA?dEAYwQD4~ZKGCg8t5sV_#kayN)Az<{SrKeSiWb-rJ zvZ^a|spT3GQHip3IGoa(fI5@U1LtnJA*ibOtMLVudmc&y(sOfxV+h@V%W2~M=O(A- z{m%Aq84EA*@c3102b|$?GeXXDTs~ELBOy4EVC-7a;NWt>&jhmN(8MW+p86N}K9?rW zZWr}PsS)Ow$wX?q`M!}oqAMS&k8ZjyA@xm60k`&39Xlb1-{9G`MhS%8zjfQQM(Z5WBBMbIi7`uqO0JfR94q2 z)1Kkk-O)sXS3S(f$<(hP+-G$I0>-|mWSj!Y4O~-_W=`FwDKS8EFNX+2mgxT3DWphE zSSC9@G|X4RS4b9CNY{}hnLG}7y}1v_6Y-)COCK=F)Oi(t=s@^1NE_C5W=TA0;=adp ztlm3BM}Jq==2h-!yE(l{hxi5HkS%?`*l$qITD`wLTdSz}asSYFo?h|A?}yXvdF1yR z$ePc?UWI6Rk+$+{U?bkSxFH@Q+&5c{K2Q`R>!%J(t7Kos?0`FiRZo`o+U$rJ7j`k}{s7D>uEI^0Icj`*4}3t0$=6b!O)ii?O8& zFEpclS3_$S`?iX+QfNSO$UlPG(Jd}7-Vl*>Tck~KNB3-Uqj_Il#azzUt}gQn2qS%3 zy0@*x$A#+^V2$MNSZVkH)wlmvz5tQL_h(T0Hba$<7=Q=^rSTu)_cNLQPY7Y487kDu z|MpR;rliozhS`SlBaF>G-33x5p;Q z<;y1@0zJtm&xnxZgwQ+zle;sNjMrV#mx!mW_m)bYW42v+x2WHqeo`PS;THA@b)g9R z{DS-qeZ2Fx+|(9L$cBr=-4K1p@+s5sImB{naA5YIlcmYe?58Pk06+&806+-!g+C*| zv$chVi!`Rby2>1b2&m``S713n{6 z>u%NE;cLDciz>5sC-D9UC))_Qn_e6mA8Q%CO>%ysKURIbrRLNJQA3%l#OqF_z9epy zDPfP>%ElXxHNA#QG%*TUw)T7_MEkB30yOI*oaMl-xFvy6;E!g!vofIJMP#k1XWGlu z?WqZ1j>9~goq+<(xRwjCBA?jP=VWYtYoF&ZxR}ekY>ws(1hKUm-s;HH^t6YmD!>@{21L6 zo;*8t$o|gz;IhXFx&YQS@c=}BVj`LUON^;v za2*C->M0s>`6Ni3_bP0IaYNBv5sunY5l(-b;p-SO%R!b{V|CbW6aIP{*(h$r%X~z1 z{wr~tI|&#gqgww4VRExqVF-4dSUM6!IO59;Q}*d%wZc~R>GVsq($*~)^2-Fsd?nT} zU_r7!7hbDEjoW{cZLo)(tp9RFMqh5vDZQMJX#bm4!+AHLKFOjSzM0+rvA;d-*UQSC z%Ma{WwPjSR^+;07D0B6xx%O(o^>^8|z7@7DFrX{y`uEVqY;~j??W0o1g@aO^lI4Fa zR`_{wj=#6sB%s0P?cprpvVf3rdU&U&`G0QYGNR7D&^jGA=TPLiz0vcpM4lT;HG@su za$6MxpXWDVZWQ6jRZ|d$wBtZGaJl!~jBARjBJ>hBzA8)YDxIut*8bi))b+f7-ZiYZ zOTW}aeT2(0Ev7wKXW25MUJh&NC}`?3gf(Uz%^}93|8nNx?B*~3(0_M`94Lk#9PEEVD&Jl6|@64La{_Aer6$a`zT4v=pn&B?+a1?}KpU#4l}o5A_InNOYt zK15UVT@n`5lv`V?%WnGkTAHKw&S|uid{@+9 zQ@40XN~fDuq7iWxe}{GujcJoQ(!|~vkIu7Vw_+#Wyr{>E?DBjUZN!-9!9d16*~~he z%l97zkPYLl?coyu;0x(*wVB_enTxfBy#?#<=idX_fvy6Wgcq}&VD}BhWtL7g%>WM- zLNXJL{ny^gBwT*AmKheaBPVgSdRNAHvjAA>>D>3QZ5JxFXt98{{EmM62p;%T0s(66 zPX*F9NpQZ>2;Ssts5_%XN3*tJ^pbZ{vO_gmDjoVLn1d2zlT09Z_q#R)MwobHD=bT) zXJy8U<_`7`4_D=1xszfODYD16h(Yk-YBx*Wfv6ko$?rX1a(M7=QS~8%q+2nOd;mV` z^_4oGm|62vEMbNmknJF-VO-S36}nIMakdSfG^EDgihu4cw^VwH929-IM@%_T-2=BY zP>RQYl&K*`SG0)i%SWfZw5`Q#}iYzg5&O3UR zF`ecO(D*g}{GprRs@LE+z=1HZP%D{XTn1e5CF{7<|KJ-FESBrgr`ya4oMurUe3648KDt2x6dX0BZCA$`g)WT23tG zv>A4_1*7t}Y=l$*d#8H9H@e1{;d}=#F`>d0!#wb2>c!;VvLnTs_>-=Vq(HO4-oo z;yYwb__C81j!a|2Hw`-+lmM*lQJ)wk>`%%-?Y((^i&TgX|I)u9%vGBP{rHlQle}C> ze{&T_MaZXfKM&zuX-haE{hscPWxPNq^O$ZrN(BsPuZ{u#D5iR$qmfY4QIHUFH+Qr? z;%M)lC^?$}S73JcQsOCl1JM}nJ2@HLDabV$CE4MT`rd+TWN4s*h{mpnw7p^*?f2;b z99i{T5-#|TC|`k~Po&FstCIF(K6>DIUjihgfYcY)A|~&MDmLS~KgL*)7nQy~ex*Yr zLg_w0RrN}ha_6y{bd!w*NY?X=l9GFqL09(VQZq&0-Uf{ORXkPfPv zNvW1{cy|FSL708k>5Sw)ku5~IkS+|q^!+36M7|@DHq4cRs%KMNV&~~`$JeI<8z7N) ziZlaTRP13C6~8;F?D5})vswPnT@tCd@fLu#1MpGr5O}f zz@1YgK7kRC^xouPzPhJ1h$y^v^^i%#G1Bmsxv6+!i6MUdtquWy3}g8xk~Jj>Yit9u z`ONXOSuuVCL>&AC$BS;6EVdM4-J;bhJ5ycm2AxoJy{^~JRYcjCJ7va*2`u`B{CrA# zhL|;k?;<5=>}ImkB59Wji7RgNO3=dyc;YN`hvYW(W$VH%Ont2l@ybn8Q-`DPBS%Bg z>v{A1(PmI&t7>5>PaEW5+*}M4LM5?FN@^IDN2)?#aFb$VaIsqCT>PKdQM<7qlk^gh^63$BluLU(EQ>~W4)Ql7G;i3kHpN4K>i`@* z548TUXO?G1vAz#$+Zo?Xrih=2*GZaDKewY}zzVjL)a96TN3QXzy3k23H8!Uzp(-CK zTL4xo>ln>q17$*{)XE()gAGdhj{Vshp?~KS= zz*5BeSr8tXxQ5Y`CK{D2fvh*TfBS$)ZCAeTB3{JyIkTJix8~Ra0vnBSn}h0br}xfI zCtK~?KK(3A2Mf)_FUZt`tYV6aK{2$>ZylpmD8$jP8&y|>cCae?a7AjHfO5)n82sO zgKUr7JQiaPSC8sJWQBkeOCUzO12SsQ#|K0B^moh*wC3}V&<)^!Zx7XG9%&AtSAw8w zkM@T;I=gt@Vh!+74!vGixot6H&?%jby0#cJ3toQ5-<|iLfGyzNq|_1tWv3Ypy)QM!t$3 zCh%38%O^GUcs~;U``!n{dT>&GFNPU zsi!7pU3yA=_`;ECltMV_l4<*a6t5N7+LHI6u8sA4#gj($0>&N@LBW5OdI(pWm?##V zVdqsObznVN!&=Av{Yl-49+pAC*TcT9i6!DTLFKOO+qqpmTILU%RCrQs?e;*lEHbZC z9U0|!)8=~OB~fR--+`|z*r>5uRVC{bO@f*)X6u8A6|9Tu)uPvoTHmMHJy0ANIHdc_ z)v5@DU9N_MF6@=$dTmU^P)3s(@3>2X*Frd+WO-nJi(*QmUMZ<-nPjHOjB@g`d3A5# zh|t0ks~@S65-HTr##B1iLPI^p{HoQ>?Bvdx*$rb3fB(@$)!^WSyyXzs5hiyCex{b} zW1F7meTk-RJkQvb345R}UN3IytesSxF!$m{rdfe=}yMh9Kd_ zlbFv8%;OdcBo;_;;BoKXShN#SrZZg6^xab$p&J>t>p4U!QOQnon9mJ9@Z(^1|B0$--RS z#mNR_^*cREHI$T>#W35nzl!R3=Y~~Y)(2xOZpU(|RP^l0$$PSjM1)`}8pq8d^R=D&xQaK}WiiJ446G>>58V{H{&-6*j z@D%Up=##Z^n3so@VSh3n$z+f!Neg{vfs>7E6YT)ZC-7H$c4>)bl?9Xdq;SkmeaF&c z4(~nbB=Os6Nm3{Eu$XJ-gPf{o=ud9!o?@gK1#Oh=l0||=en_W4cocZ*M@G&axJ|He1$p7UA;&sK664k7wz(!mkAHsD>*~F- zf3F0NrxV1?JiBgN#7JE{m70waxp`Rk(WdR=tji9Cf@7O1B^x#Br%!Axjsnc9fQhZ)z+xvJit41rycgU<65~{ z+0=bfA=s{XcwT~a^wNf}&I6^MMIjdyeOv9W4aP63%T%5dzfnyS{m1RXhq|IpA?Ot$ zXi&t0*2~PnW-3l#2WM6@u#?4~jog3BWKc%<#Erh}VZ#iD$hJyuwU`Np6j#S#z@GWuP9Ug$#m-l#X06XbpiEj&VX^hQ_h-+9TorMR~ z6R#fBhX8H_ylo+HWdXFvzNa{7bk3Gn5RRlry_)9p_49xo3^0uv?_OwC(r64?mCcfYp#Zf?So0{G-dwgYp_zpjQN;-4@KBUCzY8;a`t~y4s&d zMuNC4E<5f3Byi2-b6a{UgLE=9K+0o8ptKwckG3wNStd}*g&9<+eQcJ-@OpkurM4@s zkEt6<*M4XM8w1jL- z3FGPvXmsamVdk;x)XIa`s7UiT*}MJB$PqdfE`rh!H{w_6?A_jNwYRQT`c>;ch;G>GR{HI=lmo2ylP$M zRc7;g*V`cKMtW@JdH8m-V3O(B_3bzp{)EcyAe*6l_`nJhbD$1?EjmuWT6~Ke`ic#X z&dErOJrVr!h(tI}UItND;+alCnTST+NGWISwV#Wdm&eIQigCq!TZx@sdR?PJ-q3oa zv>B0#vxiAsC0z#G9rYpG<`0;s+yRWC#8((!H)0mMxR4{p6BfTLFAm4-K-zCfPu5Jm zoSIrh?=HX14{jaosGe-kemmPVg-BOD{V3thU zSGW=Rc&%i^!J665&IM^v1-&p?=jdpchtLT}3|@8v!c=I3yln^I`wJIQbVo{>QhjMcNFgJX8&F?rLY)2!E6E>oBG zPu7MipAW|dztwCNOPt{TDiVVwhiGD}Wp`cT6=p1yLhX()$Yv0Hh=T4*vf%_3r1|10_X75;bW{uwSp@e}+{A^#Qq_b~S}TIJbKXtVzWJrxCbsC)c2!h{0o Mg2u)=n%{o?KY37~>Hq)$ literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch2.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9ed74ec2a5cedd89b0be49d6b51f41bbe3e647ab GIT binary patch literal 10367 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 literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601091.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601091.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9d5162d51e3a515e86f83e69ec0a5a80473c1c87 GIT binary patch literal 10095 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 str: - # 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z - dt = datetime.now() - # print(dt) - return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z" - - -class BioyondCellWorkstation(BioyondWorkstation): - """ - 集成 Bioyond LIMS 的工作站示例, - 覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) → - 运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) → - 查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28) - """ - - def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs): - - # 使用统一配置,支持自定义覆盖, 从 config.py 加载完整配置 - self.bioyond_config ={ - **API_CONFIG, - "material_type_mappings": MATERIAL_TYPE_MAPPINGS, - "warehouse_mapping": WAREHOUSE_MAPPING, - "debug_mode": False - } - - # "material_type_mappings": MATERIAL_TYPE_MAPPINGS - # "warehouse_mapping": WAREHOUSE_MAPPING - if deck is None and config: - deck = config.get('deck') - # print(self.bioyond_config) - self.debug_mode = self.bioyond_config["debug_mode"] - self.http_service_started = self.debug_mode - self._device_id = "bioyond_cell_workstation" # 默认值,后续会从_ros_node获取 - super().__init__(bioyond_config=config, deck=deck) - self.update_push_ip() #直接修改奔耀端的报送ip地址 - logger.info("已更新奔耀端推送 IP 地址") - - # 启动 HTTP 服务线程 - t = threading.Thread(target=self._start_http_service, daemon=True, name="unilab_http") - t.start() - logger.info("HTTP 服务线程已启动") - # 等到任务报送成功 - self.order_finish_event = threading.Event() - self.last_order_status = None - self.last_order_code = None - logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})") - - @property - def device_id(self): - """获取设备ID,优先从_ros_node获取,否则返回默认值""" - if hasattr(self, '_ros_node') and self._ros_node is not None: - return getattr(self._ros_node, 'device_id', self._device_id) - return self._device_id - - def _start_http_service(self): - """启动 HTTP 服务""" - host = self.bioyond_config.get("HTTP_host", "") - port = self.bioyond_config.get("HTTP_port", None) - try: - self.service = WorkstationHTTPService(self, host=host, port=port) - self.service.start() - self.http_service_started = True - logger.info(f"WorkstationHTTPService 成功启动: {host}:{port}") - while True: - time.sleep(1) #一直挂着,直到进程退出 - except Exception as e: - self.http_service_started = False - logger.error(f"启动 WorkstationHTTPService 失败: {e}", exc_info=True) - - - # http报送服务,返回数据部分 - def process_step_finish_report(self, report_request): - stepId = report_request.data.get("stepId") - logger.info(f"步骤完成: stepId: {stepId}, stepName:{report_request.data.get('stepName')}") - return report_request.data.get('executionStatus') - - def process_sample_finish_report(self, report_request): - logger.info(f"通量完成: {report_request.data.get('sampleId')}") - return {"status": "received"} - - def process_order_finish_report(self, report_request, used_materials=None): - order_code = report_request.data.get("orderCode") - status = report_request.data.get("status") - logger.info(f"report_request: {report_request}") - logger.info(f"任务完成: {order_code}, status={status}") - - # 保存完整报文 - self.last_order_report = report_request.data - # 如果是当前等待的订单,触发事件 - if self.last_order_code == order_code: - self.order_finish_event.set() - - return {"status": "received"} - - def wait_for_order_finish(self, order_code: str, timeout: int = 36000) -> Dict[str, Any]: - """ - 等待指定 orderCode 的 /report/order_finish 报送。 - Args: - order_code: 任务编号 - timeout: 超时时间(秒) - Returns: - 完整的报送数据 + 状态判断结果 - """ - if not order_code: - logger.error("wait_for_order_finish() 被调用,但 order_code 为空!") - return {"status": "error", "message": "empty order_code"} - - self.last_order_code = order_code - self.last_order_report = None - self.order_finish_event.clear() - - logger.info(f"等待任务完成报送: orderCode={order_code} (timeout={timeout}s)") - - if not self.order_finish_event.wait(timeout=timeout): - logger.error(f"等待任务超时: orderCode={order_code}") - return {"status": "timeout", "orderCode": order_code} - - # 报送数据匹配验证 - report = self.last_order_report or {} - report_code = report.get("orderCode") - status = str(report.get("status", "")) - - if report_code != order_code: - logger.warning(f"收到的报送 orderCode 不匹配: {report_code} ≠ {order_code}") - return {"status": "mismatch", "report": report} - - if status == "30": - logger.info(f"任务成功完成 (orderCode={order_code})") - return {"status": "success", "report": report} - elif status == "-11": - logger.error(f"任务异常停止 (orderCode={order_code})") - return {"status": "abnormal_stop", "report": report} - elif status == "-12": - logger.warning(f"任务人工停止 (orderCode={order_code})") - return {"status": "manual_stop", "report": report} - else: - logger.warning(f"任务未知状态 ({status}) (orderCode={order_code})") - return {"status": f"unknown_{status}", "report": report} - - - # -------------------- 基础HTTP封装 -------------------- - def _url(self, path: str) -> str: - return f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}" - - def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]: - """LIMS API:大多数接口用 {apiKey/requestTime,data} 包装""" - payload = { - "apiKey": self.bioyond_config["api_key"], - "requestTime": _iso_local_now_ms() - } - if data is not None: - payload["data"] = data - - if self.debug_mode: - # 模拟返回,不发真实请求 - logger.info(f"[DEBUG] POST {path} with payload={payload}") - - return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"} - - try: - logger.info(json.dumps(payload, ensure_ascii=False)) - response = requests.post( - self._url(path), - json=payload, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"} - ) # 拼接网址+post bioyond接口 - response.raise_for_status() - return response.json() - except Exception as e: - logger.info(f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}") - logger.error(f"POST {path} 失败: {e}") - return {"error": str(e)} - - def _put_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]: - """LIMS API:PUT {apiKey/requestTime,data} 包装""" - payload = { - "apiKey": self.bioyond_config["api_key"], - "requestTime": _iso_local_now_ms() - } - if data is not None: - payload["data"] = data - - if self.debug_mode: - logger.info(f"[DEBUG] PUT {path} with payload={payload}") - return {"debug_mode": True, "url": self._url(path), "payload": payload, "status": "ok"} - - try: - response = requests.put( - self._url(path), - json=payload, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"} - ) - response.raise_for_status() - return response.json() - except Exception as e: - logger.info(f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}") - logger.error(f"PUT {path} 失败: {e}") - return {"error": str(e)} - - # -------------------- 3.36 更新推送 IP 地址 -------------------- - def update_push_ip(self, ip: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]: - """ - 3.36 更新推送 IP 地址接口(PUT) - URL: /api/lims/order/ip-config - 请求体:{ apiKey, requestTime, data: { ip, port } } - """ - target_ip = ip or self.bioyond_config.get("HTTP_host", "") - target_port = int(port or self.bioyond_config.get("HTTP_port", 0)) - data = {"ip": target_ip, "port": target_port} - - # 固定接口路径,不做其他路径兼容 - path = "/api/lims/order/ip-config" - return self._put_lims(path, data) - - # -------------------- 单点接口封装 -------------------- - # 2.17 入库物料(单个) - def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]: - return self._post_lims("/api/lims/storage/inbound", { - "materialId": material_id, - "locationId": location_id - }) - - # 2.18 批量入库(多个) - def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]: - """ - items = [{"materialId": "...", "locationId": "..."}, ...] - """ - return self._post_lims("/api/lims/storage/batch-inbound", items) - - - def auto_feeding4to3( - self, - # ★ 修改点:默认模板路径 - xlsx_path: Optional[str] = "/Users/sml/work/Unilab/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx", - # ---------------- WH4 - 加样头面 (Z=1, 12个点位) ---------------- - WH4_x1_y1_z1_1_materialName: str = "", WH4_x1_y1_z1_1_quantity: float = 0.0, - WH4_x2_y1_z1_2_materialName: str = "", WH4_x2_y1_z1_2_quantity: float = 0.0, - WH4_x3_y1_z1_3_materialName: str = "", WH4_x3_y1_z1_3_quantity: float = 0.0, - WH4_x4_y1_z1_4_materialName: str = "", WH4_x4_y1_z1_4_quantity: float = 0.0, - WH4_x5_y1_z1_5_materialName: str = "", WH4_x5_y1_z1_5_quantity: float = 0.0, - WH4_x1_y2_z1_6_materialName: str = "", WH4_x1_y2_z1_6_quantity: float = 0.0, - WH4_x2_y2_z1_7_materialName: str = "", WH4_x2_y2_z1_7_quantity: float = 0.0, - WH4_x3_y2_z1_8_materialName: str = "", WH4_x3_y2_z1_8_quantity: float = 0.0, - WH4_x4_y2_z1_9_materialName: str = "", WH4_x4_y2_z1_9_quantity: float = 0.0, - WH4_x5_y2_z1_10_materialName: str = "", WH4_x5_y2_z1_10_quantity: float = 0.0, - WH4_x1_y3_z1_11_materialName: str = "", WH4_x1_y3_z1_11_quantity: float = 0.0, - WH4_x2_y3_z1_12_materialName: str = "", WH4_x2_y3_z1_12_quantity: float = 0.0, - - # ---------------- WH4 - 原液瓶面 (Z=2, 9个点位) ---------------- - WH4_x1_y1_z2_1_materialName: str = "", WH4_x1_y1_z2_1_quantity: float = 0.0, WH4_x1_y1_z2_1_materialType: str = "", WH4_x1_y1_z2_1_targetWH: str = "", - WH4_x2_y1_z2_2_materialName: str = "", WH4_x2_y1_z2_2_quantity: float = 0.0, WH4_x2_y1_z2_2_materialType: str = "", WH4_x2_y1_z2_2_targetWH: str = "", - WH4_x3_y1_z2_3_materialName: str = "", WH4_x3_y1_z2_3_quantity: float = 0.0, WH4_x3_y1_z2_3_materialType: str = "", WH4_x3_y1_z2_3_targetWH: str = "", - WH4_x1_y2_z2_4_materialName: str = "", WH4_x1_y2_z2_4_quantity: float = 0.0, WH4_x1_y2_z2_4_materialType: str = "", WH4_x1_y2_z2_4_targetWH: str = "", - WH4_x2_y2_z2_5_materialName: str = "", WH4_x2_y2_z2_5_quantity: float = 0.0, WH4_x2_y2_z2_5_materialType: str = "", WH4_x2_y2_z2_5_targetWH: str = "", - WH4_x3_y2_z2_6_materialName: str = "", WH4_x3_y2_z2_6_quantity: float = 0.0, WH4_x3_y2_z2_6_materialType: str = "", WH4_x3_y2_z2_6_targetWH: str = "", - WH4_x1_y3_z2_7_materialName: str = "", WH4_x1_y3_z2_7_quantity: float = 0.0, WH4_x1_y3_z2_7_materialType: str = "", WH4_x1_y3_z2_7_targetWH: str = "", - WH4_x2_y3_z2_8_materialName: str = "", WH4_x2_y3_z2_8_quantity: float = 0.0, WH4_x2_y3_z2_8_materialType: str = "", WH4_x2_y3_z2_8_targetWH: str = "", - WH4_x3_y3_z2_9_materialName: str = "", WH4_x3_y3_z2_9_quantity: float = 0.0, WH4_x3_y3_z2_9_materialType: str = "", WH4_x3_y3_z2_9_targetWH: str = "", - - # ---------------- WH3 - 人工堆栈 (Z=3, 15个点位) ---------------- - WH3_x1_y1_z3_1_materialType: str = "", WH3_x1_y1_z3_1_materialId: str = "", WH3_x1_y1_z3_1_quantity: float = 0, - WH3_x2_y1_z3_2_materialType: str = "", WH3_x2_y1_z3_2_materialId: str = "", WH3_x2_y1_z3_2_quantity: float = 0, - WH3_x3_y1_z3_3_materialType: str = "", WH3_x3_y1_z3_3_materialId: str = "", WH3_x3_y1_z3_3_quantity: float = 0, - WH3_x1_y2_z3_4_materialType: str = "", WH3_x1_y2_z3_4_materialId: str = "", WH3_x1_y2_z3_4_quantity: float = 0, - WH3_x2_y2_z3_5_materialType: str = "", WH3_x2_y2_z3_5_materialId: str = "", WH3_x2_y2_z3_5_quantity: float = 0, - WH3_x3_y2_z3_6_materialType: str = "", WH3_x3_y2_z3_6_materialId: str = "", WH3_x3_y2_z3_6_quantity: float = 0, - WH3_x1_y3_z3_7_materialType: str = "", WH3_x1_y3_z3_7_materialId: str = "", WH3_x1_y3_z3_7_quantity: float = 0, - WH3_x2_y3_z3_8_materialType: str = "", WH3_x2_y3_z3_8_materialId: str = "", WH3_x2_y3_z3_8_quantity: float = 0, - WH3_x3_y3_z3_9_materialType: str = "", WH3_x3_y3_z3_9_materialId: str = "", WH3_x3_y3_z3_9_quantity: float = 0, - WH3_x1_y4_z3_10_materialType: str = "", WH3_x1_y4_z3_10_materialId: str = "", WH3_x1_y4_z3_10_quantity: float = 0, - WH3_x2_y4_z3_11_materialType: str = "", WH3_x2_y4_z3_11_materialId: str = "", WH3_x2_y4_z3_11_quantity: float = 0, - WH3_x3_y4_z3_12_materialType: str = "", WH3_x3_y4_z3_12_materialId: str = "", WH3_x3_y4_z3_12_quantity: float = 0, - WH3_x1_y5_z3_13_materialType: str = "", WH3_x1_y5_z3_13_materialId: str = "", WH3_x1_y5_z3_13_quantity: float = 0, - WH3_x2_y5_z3_14_materialType: str = "", WH3_x2_y5_z3_14_materialId: str = "", WH3_x2_y5_z3_14_quantity: float = 0, - WH3_x3_y5_z3_15_materialType: str = "", WH3_x3_y5_z3_15_materialId: str = "", WH3_x3_y5_z3_15_quantity: float = 0, - ): - """ - 自动化上料(支持两种模式) - - Excel 路径存在 → 从 Excel 模板解析 - - Excel 路径不存在 → 使用手动参数 - """ - items: List[Dict[str, Any]] = [] - - # ---------- 模式 1: Excel 导入 ---------- - if xlsx_path: - path = Path(__file__).parent / Path(xlsx_path) - if path.exists(): # ★ 修改点:路径存在才加载 - try: - df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl") - except Exception as e: - raise RuntimeError(f"读取 Excel 失败:{e}") - - # 四号手套箱加样头面 - for _, row in df.iloc[1:13, 2:7].iterrows(): - if pd.notna(row[5]): - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), - "materialName": str(row[5]).strip(), - "quantity": float(row[6]) if pd.notna(row[6]) else 0.0, - }) - # 四号手套箱原液瓶面 - for _, row in df.iloc[14:23, 2:9].iterrows(): - if pd.notna(row[5]): - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), - "materialName": str(row[5]).strip(), - "quantity": float(row[6]) if pd.notna(row[6]) else 0.0, - "materialType": str(row[7]).strip() if pd.notna(row[7]) else "", - "targetWH": str(row[8]).strip() if pd.notna(row[8]) else "", - }) - # 三号手套箱人工堆栈 - for _, row in df.iloc[25:40, 2:7].iterrows(): - if pd.notna(row[5]) or pd.notna(row[6]): - items.append({ - "sourceWHName": "三号手套箱人工堆栈", - "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), - "materialType": str(row[5]).strip() if pd.notna(row[5]) else "", - "materialId": str(row[6]).strip() if pd.notna(row[6]) else "", - "quantity": 1 - }) - else: - logger.warning(f"未找到 Excel 文件 {xlsx_path},自动切换到手动参数模式。") - - # ---------- 模式 2: 手动填写 ---------- - if not items: - params = locals() - for name, value in params.items(): - if name.startswith("四号手套箱堆栈") and "materialName" in name and value: - idx = name.split("_") - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(idx[1][1:]), "posY": int(idx[2][1:]), "posZ": int(idx[3][1:]), - "materialName": value, - "quantity": float(params.get(name.replace("materialName", "quantity"), 0.0)) - }) - elif name.startswith("四号手套箱堆栈") and "materialType" in name and (value or params.get(name.replace("materialType", "materialName"), "")): - idx = name.split("_") - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(idx[1][1:]), "posY": int(idx[2][1:]), "posZ": int(idx[3][1:]), - "materialName": params.get(name.replace("materialType", "materialName"), ""), - "quantity": float(params.get(name.replace("materialType", "quantity"), 0.0)), - "materialType": value, - "targetWH": params.get(name.replace("materialType", "targetWH"), ""), - }) - elif name.startswith("三号手套箱人工堆栈") and "materialType" in name and (value or params.get(name.replace("materialType", "materialId"), "")): - idx = name.split("_") - items.append({ - "sourceWHName": "三号手套箱人工堆栈", - "posX": int(idx[1][1:]), "posY": int(idx[2][1:]), "posZ": int(idx[3][1:]), - "materialType": value, - "materialId": params.get(name.replace("materialType", "materialId"), ""), - "quantity": int(params.get(name.replace("materialType", "quantity"), 1)), - }) - - if not items: - logger.warning("没有有效的上料条目,已跳过提交。") - return {"code": 0, "message": "no valid items", "data": []} - logger.info(items) - response = self._post_lims("/api/lims/order/auto-feeding4to3", items) - - # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") - if not order_code: - logger.error("上料任务未返回有效 orderCode!") - return response - # 等待完成报送 - result = self.wait_for_order_finish(order_code) - print("\n" + "="*60) - print("实验记录本结果auto_feeding4to3") - print("="*60) - print(json.dumps(result, indent=2, ensure_ascii=False)) - print("="*60 + "\n") - return result - - def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]: - """ - 3.31 自动化下料(Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound) - """ - path = Path(xlsx_path) - if not path.exists(): - raise FileNotFoundError(f"未找到 Excel 文件:{path}") - - try: - df = pd.read_excel(path, sheet_name=0, engine="openpyxl") - except Exception as e: - raise RuntimeError(f"读取 Excel 失败:{e}") - - def pick(names: List[str]) -> Optional[str]: - for n in names: - if n in df.columns: - return n - return None - - c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"]) - c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"]) - c_qty = pick(["数量", "quantity"]) - c_x = pick(["x", "X", "posX", "坐标X"]) - c_y = pick(["y", "Y", "posY", "坐标Y"]) - c_z = pick(["z", "Z", "posZ", "坐标Z"]) - - required = [c_loc, c_wh, c_qty, c_x, c_y, c_z] - if any(c is None for c in required): - raise KeyError("Excel 缺少必要列:locationId/warehouseId/数量/x/y/z(支持多别名,至少要能匹配到)。") - - def as_int(v, d=0): - try: - if pd.isna(v): return d - return int(v) - except Exception: - try: - return int(float(v)) - except Exception: - return d - - def as_float(v, d=0.0): - try: - if pd.isna(v): return d - return float(v) - except Exception: - return d - - def as_str(v, d=""): - if v is None or (isinstance(v, float) and pd.isna(v)): return d - s = str(v).strip() - return s if s else d - - items: List[Dict[str, Any]] = [] - for _, row in df.iterrows(): - items.append({ - "locationId": as_str(row[c_loc]), - "warehouseId": as_str(row[c_wh]), - "quantity": as_float(row[c_qty]), - "x": as_int(row[c_x]), - "y": as_int(row[c_y]), - "z": as_int(row[c_z]), - }) - - response = self._post_lims("/api/lims/storage/auto-batch-out-bound", items) - self.wait_for_response_orders(response, "auto_batch_outbound_from_xlsx") - return response - - # 2.14 新建实验 - def create_orders(self, xlsx_path: str) -> Dict[str, Any]: - """ - 从 Excel 解析并创建实验(2.14) - 约定: - - batchId = Excel 文件名(不含扩展名) - - 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列) - - totalMass 自动计算为所有物料质量之和 - - createTime 缺失或为空时自动填充为当前日期(YYYY/M/D) - """ - default_path = Path("/Users/sml/work/Unilab/unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025092701.xlsx") - path = Path(xlsx_path) if xlsx_path else default_path - print(f"[create_orders] 使用 Excel 路径: {path}") - if path != default_path: - print("[create_orders] 来源: 调用方传入自定义路径") - else: - print("[create_orders] 来源: 使用默认模板路径") - - if not path.exists(): - print(f"[create_orders] ⚠️ Excel 文件不存在: {path}") - raise FileNotFoundError(f"未找到 Excel 文件:{path}") - - try: - df = pd.read_excel(path, sheet_name=0, engine="openpyxl") - except Exception as e: - raise RuntimeError(f"读取 Excel 失败:{e}") - print(f"[create_orders] Excel 读取成功,行数: {len(df)}, 列: {list(df.columns)}") - - # 列名容错:返回可选列名,找不到则返回 None - def _pick(col_names: List[str]) -> Optional[str]: - for c in col_names: - if c in df.columns: - return c - return None - - col_order_name = _pick(["配方ID", "orderName", "订单编号"]) - col_create_time = _pick(["创建日期", "createTime"]) - col_bottle_type = _pick(["配液瓶类型", "bottleType"]) - col_mix_time = _pick(["混匀时间(s)", "mixTime"]) - col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"]) - col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"]) - col_cond = _pick(["电导测试分液体积", "conductivityInfo"]) - col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"]) - print("[create_orders] 列匹配结果:", { - "order_name": col_order_name, - "create_time": col_create_time, - "bottle_type": col_bottle_type, - "mix_time": col_mix_time, - "load": col_load, - "pouch": col_pouch, - "conductivity": col_cond, - "conductivity_bottle_count": col_cond_cnt, - }) - - # 物料列:所有以 (g) 结尾 - material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")] - print(f"[create_orders] 识别到的物料列: {material_cols}") - if not material_cols: - raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。") - - batch_id = path.stem - - def _to_ymd_slash(v) -> str: - # 统一为 "YYYY/M/D";为空或解析失败则用当前日期 - if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "": - ts = datetime.now() - else: - try: - ts = pd.to_datetime(v) - except Exception: - ts = datetime.now() - return f"{ts.year}/{ts.month}/{ts.day}" - - def _as_int(val, default=0) -> int: - try: - if pd.isna(val): - return default - return int(val) - except Exception: - return default - - def _as_float(val, default=0.0) -> float: - try: - if pd.isna(val): - return default - return float(val) - except Exception: - return default - - def _as_str(val, default="") -> str: - if val is None or (isinstance(val, float) and pd.isna(val)): - return default - s = str(val).strip() - return s if s else default - - orders: List[Dict[str, Any]] = [] - - for idx, row in df.iterrows(): - mats: List[Dict[str, Any]] = [] - total_mass = 0.0 - - for mcol in material_cols: - val = row.get(mcol, None) - if val is None or (isinstance(val, float) and pd.isna(val)): - continue - try: - mass = float(val) - except Exception: - continue - if mass > 0: - mats.append({"name": mcol.replace("(g)", ""), "mass": mass}) - total_mass += mass - else: - if mass < 0: - print(f"[create_orders] 第 {idx+1} 行物料 {mcol} 数值为负数: {mass}") - - order_data = { - "batchId": batch_id, - "orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}", - "createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None), - "bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶", - "mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0, - "loadSheddingInfo": _as_float(row[col_load]) if col_load else 0.0, - "pouchCellInfo": _as_float(row[col_pouch]) if col_pouch else 0, - "conductivityInfo": _as_float(row[col_cond]) if col_cond else 0, - "conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0, - "materialInfos": mats, - "totalMass": round(total_mass, 4) # 自动汇总 - } - print(f"[create_orders] 第 {idx+1} 行解析结果: orderName={order_data['orderName']}, " - f"loadShedding={order_data['loadSheddingInfo']}, pouchCell={order_data['pouchCellInfo']}, " - f"conductivity={order_data['conductivityInfo']}, totalMass={order_data['totalMass']}, " - f"material_count={len(mats)}") - - if order_data["totalMass"] <= 0: - print(f"[create_orders] ⚠️ 第 {idx+1} 行总质量 <= 0,可能导致 LIMS 校验失败") - if not mats: - print(f"[create_orders] ⚠️ 第 {idx+1} 行未找到有效物料") - - orders.append(order_data) - print("================================================") - print("orders:", orders) - - print(f"[create_orders] 即将提交订单数量: {len(orders)}") - response = self._post_lims("/api/lims/order/orders", orders) - print(f"[create_orders] 接口返回: {response}") - # 等待任务报送成功 - data_list = response.get("data", []) - if data_list: - order_code = data_list[0].get("orderCode") - else: - order_code = None - - if not order_code: - logger.error("上料任务未返回有效 orderCode!") - return response - # 等待完成报送 - result = self.wait_for_order_finish(order_code) - print("实验记录本========================create_orders========================") - print(result) - print("========================") - return result - - # 2.7 启动调度 - def scheduler_start(self) -> Dict[str, Any]: - return self._post_lims("/api/lims/scheduler/start") - # 3.10 停止调度 - def scheduler_stop(self) -> Dict[str, Any]: - - """ - 停止调度 (3.10) - 请求体只包含 apiKey 和 requestTime - """ - return self._post_lims("/api/lims/scheduler/stop") - # 2.9 继续调度 - # 2.9 继续调度 - def scheduler_continue(self) -> Dict[str, Any]: - """ - 继续调度 (2.9) - 请求体只包含 apiKey 和 requestTime - """ - return self._post_lims("/api/lims/scheduler/continue") - def scheduler_reset(self) -> Dict[str, Any]: - """ - 复位调度 (2.11) - 请求体只包含 apiKey 和 requestTime - """ - return self._post_lims("/api/lims/scheduler/reset") - - - # 2.24 物料变更推送 - def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]: - """ - material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等) - """ - return self._post_report_raw("/report/material_change", material_obj) - - # 2.32 3-2-1 物料转运 - def transfer_3_to_2_to_1(self, - # source_wh_id: Optional[str] = None, - source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b', - source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]: - payload: Dict[str, Any] = { - "sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z - } - if source_wh_id: - payload["sourceWHID"] = source_wh_id - - response = self._post_lims("/api/lims/order/transfer-task3To2To1", payload) - # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") - if not order_code: - logger.error("上料任务未返回有效 orderCode!") - return response - # 等待完成报送 - result = self.wait_for_order_finish(order_code) - return result - - # 3.35 1→2 物料转运 - def transfer_1_to_2(self) -> Dict[str, Any]: - """ - 1→2 物料转运 - URL: /api/lims/order/transfer-task1To2 - 只需要 apiKey 和 requestTime - """ - response = self._post_lims("/api/lims/order/transfer-task1To2") - # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") - if not order_code: - logger.error("上料任务未返回有效 orderCode!") - return response - # 等待完成报送 - result = self.wait_for_order_finish(order_code) - return result - - # 2.5 批量查询实验报告(post过滤关键字查询) - def order_list_v2(self, - timeType: str = "", - beginTime: str = "", - endTime: str = "", - status: str = "", # 60表示正在运行,80表示完成,90表示失败 - filter: str = "", - skipCount: int = 0, - pageCount: int = 1, # 显示多少页数据 - sorting: str = "") -> Dict[str, Any]: - """ - 批量查询实验报告的详细信息 (2.5) - URL: /api/lims/order/order-list - 参数默认值和接口文档保持一致 - """ - data: Dict[str, Any] = { - "timeType": timeType, - "beginTime": beginTime, - "endTime": endTime, - "status": status, - "filter": filter, - "skipCount": skipCount, - "pageCount": pageCount, - "sorting": sorting - } - return self._post_lims("/api/lims/order/order-list", data) - - # 一直post执行bioyond接口查询任务状态 - def wait_for_transfer_task(self, timeout: int = 3000, interval: int = 5, filter_text: Optional[str] = None) -> bool: - """ - 轮询查询物料转移任务是否成功完成 (status=80) - - timeout: 最大等待秒数 (默认600秒) - - interval: 轮询间隔秒数 (默认3秒) - 返回 True 表示找到并成功完成,False 表示超时未找到 - """ - now = datetime.now() - beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") - endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ") - print(beginTime, endTime) - - deadline = time.time() + timeout - - while time.time() < deadline: - result = self.order_list_v2( - timeType="", - beginTime=beginTime, - endTime=endTime, - status="", - filter=filter_text, - skipCount=0, - pageCount=1, - sorting="" - ) - print(result) - - items = result.get("data", {}).get("items", []) - for item in items: - name = item.get("name", "") - status = item.get("status") - # 改成用 filter_text 判断 - if (not filter_text or filter_text in name) and status == 80: - logger.info(f"硬件转移动作完成: {name}, status={status}") - return True - - logger.info(f"等待中: {name}, status={status}") - time.sleep(interval) - - logger.warning("超时未找到成功的物料转移任务") - return False - - def create_materials(self, mappings: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - 将 SOLID_LIQUID_MAPPINGS 中的所有物料逐个 POST 到 /api/lims/storage/material - """ - results = [] - - for name, data in mappings.items(): - data = { - "typeId": data["typeId"], - "code": data.get("code", ""), - "barCode": data.get("barCode", ""), - "name": data["name"], - "unit": data.get("unit", "g"), - "parameters": data.get("parameters", ""), - "quantity": data.get("quantity", ""), - "warningQuantity": data.get("warningQuantity", ""), - "details": data.get("details", []) - } - - logger.info(f"正在创建第 {i}/{total} 个固体物料: {name}") - result = self._post_lims("/api/lims/storage/material", material_data) - - if result and result.get("code") == 1: - # data 字段可能是字符串(物料ID)或字典(包含id字段) - data = result.get("data") - if isinstance(data, str): - # data 直接是物料ID字符串 - material_id = data - elif isinstance(data, dict): - # data 是字典,包含id字段 - material_id = data.get("id") - else: - material_id = None - - if material_id: - created_materials.append({ - "name": name, - "materialId": material_id, - "typeId": type_id - }) - logger.info(f"✓ 成功创建物料: {name}, ID: {material_id}") - else: - logger.error(f"✗ 创建物料失败: {name}, 未返回ID") - logger.error(f" 响应数据: {result}") - else: - error_msg = result.get("error") or result.get("message", "未知错误") - logger.error(f"✗ 创建物料失败: {name}") - logger.error(f" 错误信息: {error_msg}") - logger.error(f" 完整响应: {result}") - - # 避免请求过快 - time.sleep(0.3) - - logger.info(f"物料创建完成,成功创建 {len(created_materials)}/{total} 个固体物料") - return created_materials - - def _sync_materials_safe(self) -> bool: - """仅使用 BioyondResourceSynchronizer 执行同步(与 station.py 保持一致)。""" - if hasattr(self, 'resource_synchronizer') and self.resource_synchronizer: - try: - return bool(self.resource_synchronizer.sync_from_external()) - except Exception as e: - logger.error(f"同步失败: {e}") - return False - logger.warning("资源同步器未初始化") - return False - - def _load_warehouse_locations(self, warehouse_name: str) -> tuple[List[str], List[str]]: - """从配置加载仓库位置信息 - - Args: - warehouse_name: 仓库名称 - - Returns: - (location_ids, position_names) 元组 - """ - warehouse_mapping = self.bioyond_config.get("warehouse_mapping", WAREHOUSE_MAPPING) - - if warehouse_name not in warehouse_mapping: - raise ValueError(f"配置中未找到仓库: {warehouse_name}。可用: {list(warehouse_mapping.keys())}") - - site_uuids = warehouse_mapping[warehouse_name].get("site_uuids", {}) - if not site_uuids: - raise ValueError(f"仓库 {warehouse_name} 没有配置位置") - - # 按顺序获取位置ID和名称 - location_ids = [] - position_names = [] - for key in sorted(site_uuids.keys()): - location_ids.append(site_uuids[key]) - position_names.append(key) - - return location_ids, position_names - - - def create_and_inbound_materials( - self, - material_names: Optional[List[str]] = None, - type_id: str = "3a190ca0-b2f6-9aeb-8067-547e72c11469", - warehouse_name: str = "粉末加样头堆栈" - ) -> Dict[str, Any]: - """ - 传参与默认列表方式创建物料并入库(不使用CSV)。 - - Args: - material_names: 物料名称列表;默认使用 [LiPF6, LiDFOB, DTD, LiFSI, LiPO2F2] - type_id: 物料类型ID - warehouse_name: 目标仓库名(用于取位置信息) - - Returns: - 执行结果字典 - """ - logger.info("=" * 60) - logger.info(f"开始执行:从参数创建物料并批量入库到 {warehouse_name}") - logger.info("=" * 60) - - try: - # 1) 准备物料名称(默认值) - default_materials = ["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"] - mat_names = [m.strip() for m in (material_names or default_materials) if str(m).strip()] - if not mat_names: - return {"success": False, "error": "物料名称列表为空"} - - # 2) 加载仓库位置信息 - all_location_ids, position_names = self._load_warehouse_locations(warehouse_name) - logger.info(f"✓ 加载 {len(all_location_ids)} 个位置 ({position_names[0]} ~ {position_names[-1]})") - - # 限制数量不超过可用位置 - if len(mat_names) > len(all_location_ids): - logger.warning(f"物料数量超出位置数量,仅处理前 {len(all_location_ids)} 个") - mat_names = mat_names[:len(all_location_ids)] - - # 3) 创建物料 - logger.info(f"\n【步骤1/3】创建 {len(mat_names)} 个固体物料...") - created_materials = self.create_solid_materials(mat_names, type_id) - if not created_materials: - return {"success": False, "error": "没有成功创建任何物料"} - - # 4) 批量入库 - logger.info(f"\n【步骤2/3】批量入库物料...") - location_ids = all_location_ids[:len(created_materials)] - selected_positions = position_names[:len(created_materials)] - - inbound_items = [ - {"materialId": mat["materialId"], "locationId": loc_id} - for mat, loc_id in zip(created_materials, location_ids) - ] - - for material, position in zip(created_materials, selected_positions): - logger.info(f" - {material['name']} → {position}") - - result = self.storage_batch_inbound(inbound_items) - if result.get("code") != 1: - logger.error(f"✗ 批量入库失败: {result}") - return {"success": False, "error": "批量入库失败", "created_materials": created_materials, "inbound_result": result} - - logger.info("✓ 批量入库成功") - - # 5) 同步 - logger.info(f"\n【步骤3/3】同步物料数据...") - if self._sync_materials_safe(): - logger.info("✓ 物料数据同步完成") - else: - logger.warning("⚠ 物料数据同步未完成(可忽略,不影响已创建与入库的数据)") - - logger.info("\n" + "=" * 60) - logger.info("流程完成") - logger.info("=" * 60 + "\n") - - return { - "success": True, - "created_materials": created_materials, - "inbound_result": result, - "total_created": len(created_materials), - "total_inbound": len(inbound_items), - "warehouse": warehouse_name, - "positions": selected_positions - } - - except Exception as e: - logger.error(f"✗ 执行失败: {e}") - return {"success": False, "error": str(e)} - - def create_material( - self, - material_name: str, - type_id: str, - warehouse_name: str, - location_name_or_id: Optional[str] = None - ) -> Dict[str, Any]: - """创建单个物料并可选入库。 - Args: - material_name: 物料名称(会优先匹配配置模板)。 - type_id: 物料类型 ID(若为空则尝试从配置推断)。 - warehouse_name: 需要入库的仓库名称;若为空则仅创建不入库。 - location_name_or_id: 具体库位名称(如 A01)或库位 UUID,由用户指定。 - Returns: - 包含创建结果、物料ID以及入库结果的字典。 - """ - material_name = (material_name or "").strip() - - resolved_type_id = (type_id or "").strip() - # 优先从 SOLID_LIQUID_MAPPINGS 中获取模板数据 - template = SOLID_LIQUID_MAPPINGS.get(material_name) - if not template: - raise ValueError(f"在配置中未找到物料 {material_name} 的模板,请检查 SOLID_LIQUID_MAPPINGS。") - material_data: Dict[str, Any] - material_data = deepcopy(template) - # 最终确保 typeId 为调用方传入的值 - if resolved_type_id: - material_data["typeId"] = resolved_type_id - material_data["name"] = material_name - # 生成唯一编码 - def _generate_code(prefix: str) -> str: - normalized = re.sub(r"\W+", "_", prefix) - normalized = normalized.strip("_") or "material" - return f"{normalized}_{datetime.now().strftime('%Y%m%d%H%M%S')}" - if not material_data.get("code"): - material_data["code"] = _generate_code(material_name) - if not material_data.get("barCode"): - material_data["barCode"] = "" - # 处理数量字段类型 - def _to_number(value: Any, default: float = 0.0) -> float: - try: - if value is None: - return default - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str) and value.strip() == "": - return default - return float(value) - except (TypeError, ValueError): - return default - material_data["quantity"] = _to_number(material_data.get("quantity"), 1.0) - material_data["warningQuantity"] = _to_number(material_data.get("warningQuantity"), 0.0) - unit = material_data.get("unit") or "个" - material_data["unit"] = unit - if not material_data.get("parameters"): - material_data["parameters"] = json.dumps({"unit": unit}, ensure_ascii=False) - # 补充子物料信息 - details = material_data.get("details") or [] - if not isinstance(details, list): - logger.warning("details 字段不是列表,已忽略。") - details = [] - else: - for idx, detail in enumerate(details, start=1): - if not isinstance(detail, dict): - continue - if not detail.get("code"): - detail["code"] = f"{material_data['code']}_{idx:02d}" - if not detail.get("name"): - detail["name"] = f"{material_name}_detail_{idx:02d}" - if not detail.get("unit"): - detail["unit"] = unit - if not detail.get("parameters"): - detail["parameters"] = json.dumps({"unit": detail.get("unit", unit)}, ensure_ascii=False) - if "quantity" in detail: - detail["quantity"] = _to_number(detail.get("quantity"), 1.0) - material_data["details"] = details - create_result = self._post_lims("/api/lims/storage/material", material_data) - # 解析创建结果中的物料 ID - material_id: Optional[str] = None - if isinstance(create_result, dict): - data_field = create_result.get("data") - if isinstance(data_field, str): - material_id = data_field - elif isinstance(data_field, dict): - material_id = data_field.get("id") or data_field.get("materialId") - inbound_result: Optional[Dict[str, Any]] = None - location_id: Optional[str] = None - # 按用户指定位置入库 - if warehouse_name and material_id and location_name_or_id: - try: - location_ids, position_names = self._load_warehouse_locations(warehouse_name) - position_to_id = {name: loc_id for name, loc_id in zip(position_names, location_ids)} - target_location_id = position_to_id.get(location_name_or_id, location_name_or_id) - if target_location_id: - location_id = target_location_id - inbound_result = self.storage_inbound(material_id, target_location_id) - else: - inbound_result = {"error": f"未找到匹配的库位: {location_name_or_id}"} - except Exception as exc: - logger.error(f"获取仓库 {warehouse_name} 位置失败: {exc}") - inbound_result = {"error": str(exc)} - return { - "success": bool(isinstance(create_result, dict) and create_result.get("code") == 1 and material_id), - "material_name": material_name, - "material_id": material_id, - "warehouse": warehouse_name, - "location_id": location_id, - "location_name_or_id": location_name_or_id, - "create_result": create_result, - "inbound_result": inbound_result, - } - def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: ResourcePLR, parent_resource: ResourcePLR): - # ROS2DeviceNode.run_async_func(self._ros_node.resource_tree_transfer, True, **{ - # "old_parent": old_parent, - # "plr_resource": plr_resource, - # "parent_resource": parent_resource, - # }) - print("resource_tree_transfer", plr_resource, parent_resource) - if hasattr(plr_resource, "unilabos_extra") and plr_resource.unilabos_extra: - if "update_resource_site" in plr_resource.unilabos_extra: - site = plr_resource.unilabos_extra["update_resource_site"] - plr_model = plr_resource.model - board_type = None - for key, (moudle_name,moudle_uuid) in MATERIAL_TYPE_MAPPINGS.items(): - if plr_model == moudle_name: - board_type = key - break - if board_type is None: - pass - bottle1 = plr_resource.children[0] - - bottle_moudle = bottle1.model - bottle_type = None - for key, (moudle_name, moudle_uuid) in MATERIAL_TYPE_MAPPINGS.items(): - if bottle_moudle == moudle_name: - bottle_type = key - break - - # 从 parent_resource 获取仓库名称 - warehouse_name = parent_resource.name if parent_resource else "手动堆栈" - logger.info(f"拖拽上料: {plr_resource.name} -> {warehouse_name} / {site}") - - self.create_sample(plr_resource.name, board_type, bottle_type, site, warehouse_name) - return - self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}") - - def create_sample( - self, - name: str, - board_type: str, - bottle_type: str, - location_code: str, - warehouse_name: str = "手动堆栈" - ) -> Dict[str, Any]: - """创建配液板物料并自动入库。 - Args: - name: 物料名称 - board_type: 板类型,如 "5ml分液瓶板"、"配液瓶(小)板" - bottle_type: 瓶类型,如 "5ml分液瓶"、"配液瓶(小)" - location_code: 库位编号,例如 "A01" - warehouse_name: 仓库名称,默认为 "手动堆栈",支持 "自动堆栈-左"、"自动堆栈-右" 等 - """ - carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] - bottle_type_id = MATERIAL_TYPE_MAPPINGS[bottle_type][1] - - # 从指定仓库获取库位UUID - if warehouse_name not in WAREHOUSE_MAPPING: - logger.error(f"未找到仓库: {warehouse_name},回退到手动堆栈") - warehouse_name = "手动堆栈" - - if location_code not in WAREHOUSE_MAPPING[warehouse_name]["site_uuids"]: - logger.error(f"仓库 {warehouse_name} 中未找到库位 {location_code}") - raise ValueError(f"库位 {location_code} 在仓库 {warehouse_name} 中不存在") - - location_id = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"][location_code] - logger.info(f"创建样品入库: {name} -> {warehouse_name}/{location_code} (UUID: {location_id})") - - # 新建小瓶 - details = [] - for y in range(1, 5): - for x in range(1, 3): - details.append({ - "typeId": bottle_type_id, - "code": "", - "name": str(bottle_type) + str(x) + str(y), - "quantity": "1", - "x": x, - "y": y, - "z": 1, - "unit": "个", - "parameters": json.dumps({"unit": "个"}, ensure_ascii=False), - }) - - data = { - "typeId": carrier_type_id, - "code": "", - "barCode": "", - "name": name, - "unit": "块", - "parameters": json.dumps({"unit": "块"}, ensure_ascii=False), - "quantity": "1", - "details": details, - } - # print("xxx:",data) - create_result = self._post_lims("/api/lims/storage/material", data) - sample_uuid = create_result.get("data") - - final_result = self._post_lims("/api/lims/storage/inbound", { - "materialId": sample_uuid, - "locationId": location_id, - }) - return final_result - - - - -if __name__ == "__main__": - lab_registry.setup() - deck = BIOYOND_YB_Deck(setup=True) - ws = BioyondCellWorkstation(deck=deck) - # ws.create_sample(name="test", board_type="配液瓶(小)板", bottle_type="配液瓶(小)", location_code="B01") - # logger.info(ws.scheduler_stop()) - # logger.info(ws.scheduler_start()) - - # 继续后续流程 - logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 - # # # 使用正斜杠或 Path 对象来指定文件路径 - # excel_path = Path("unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025092701.xlsx") - # logger.info(ws.create_orders(excel_path)) - # logger.info(ws.transfer_3_to_2_to_1()) - - # logger.info(ws.transfer_1_to_2()) - # logger.info(ws.scheduler_start()) - - - while True: - time.sleep(1) - # re=ws.scheduler_stop() - # re = ws.transfer_3_to_2_to_1() - - # print(re) - # logger.info("调度启动完成") - - # ws.scheduler_continue() - # 3.30 上料:读取模板 Excel 自动解析并 POST - # r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板.xlsx") - # ws.wait_for_transfer_task(filter_text="物料转移任务") - # logger.info("4号箱向3号箱转运物料转移任务已完成") - - # ws.scheduler_start() - # print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items - - # # 新建实验 - # response = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092701.xlsx") - # logger.info(response) - # data_list = response.get("data", []) - # order_name = data_list[0].get("orderName", "") - - # ws.wait_for_transfer_task(filter_text=order_name) - # ws.wait_for_transfer_task(filter_text='DP20250927001') - # logger.info("3号站内实验完成") - # # ws.scheduler_start() - # # print(res) - # ws.transfer_3_to_2_to_1() - # ws.wait_for_transfer_task(filter_text="物料转移任务") - # logger.info("3号站向2号站向1号站转移任务完成") - # r321 = self.wait_for_transfer_task() - #1号站启动 - # ws.transfer_1_to_2() - # ws.wait_for_transfer_task(filter_text="物料转移任务") - # logger.info("1号站向2号站转移任务完成") - # logger.info("全流程结束") - - # 3.31 下料:同理 - # r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx") - # print(r2["payload"]["data"]) diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index 5d10f40..f718fb9 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -161,6 +161,95 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.warning(f"任务未知状态 ({status}) (orderCode={order_code})") return {"status": f"unknown_{status}", "report": report} + def get_material_info(self, material_id: str) -> Dict[str, Any]: + """查询物料详细信息(物料详情接口) + + Args: + material_id: 物料 ID (GUID) + + Returns: + 物料详情,包含 name, typeName, locations 等 + """ + result = self._post_lims("/api/lims/storage/material-info", material_id) + return result.get("data", {}) + + def _process_order_reagents(self, report: Dict[str, Any]) -> Dict[str, Any]: + """处理订单完成报文中的试剂数据,计算质量比 + + Args: + report: 订单完成推送的 report 数据 + + Returns: + { + "real_mass_ratio": {"试剂A": 0.6, "试剂B": 0.4}, + "target_mass_ratio": {"试剂A": 0.6, "试剂B": 0.4}, + "reagent_details": [...] # 详细数据 + } + """ + used_materials = report.get("usedMaterials", []) + + # 1. 筛选试剂(typemode="2",注意是小写且是字符串) + reagents = [m for m in used_materials if str(m.get("typemode")) == "2"] + + if not reagents: + logger.warning("订单完成报文中没有试剂(typeMode=2)") + return { + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "reagent_details": [] + } + + # 2. 查询试剂名称 + reagent_data = [] + for reagent in reagents: + material_id = reagent.get("materialId") + if not material_id: + continue + + try: + info = self.get_material_info(material_id) + name = info.get("name", f"Unknown_{material_id[:8]}") + real_qty = float(reagent.get("realQuantity", 0.0)) + used_qty = float(reagent.get("usedQuantity", 0.0)) + + reagent_data.append({ + "name": name, + "material_id": material_id, + "real_quantity": real_qty, + "used_quantity": used_qty + }) + logger.info(f"试剂: {name}, 目标={used_qty}g, 实际={real_qty}g") + except Exception as e: + logger.error(f"查询物料信息失败: {material_id}, {e}") + continue + + if not reagent_data: + return { + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "reagent_details": [] + } + + # 3. 计算质量比 + def calculate_mass_ratio(items: List[Dict], key: str) -> Dict[str, float]: + total = sum(item[key] for item in items) + if total == 0: + logger.warning(f"总质量为0,无法计算{key}质量比") + return {item["name"]: 0.0 for item in items} + return {item["name"]: round(item[key] / total, 4) for item in items} + + real_mass_ratio = calculate_mass_ratio(reagent_data, "real_quantity") + target_mass_ratio = calculate_mass_ratio(reagent_data, "used_quantity") + + logger.info(f"真实质量比: {real_mass_ratio}") + logger.info(f"目标质量比: {target_mass_ratio}") + + return { + "real_mass_ratio": real_mass_ratio, + "target_mass_ratio": target_mass_ratio, + "reagent_details": reagent_data + } + # -------------------- 基础HTTP封装 -------------------- def _url(self, path: str) -> str: @@ -643,6 +732,21 @@ class BioyondCellWorkstation(BioyondWorkstation): # 提取报文数据 if result.get("status") == "success": report = result.get("report", {}) + + # [新增] 处理试剂数据,计算质量比 + try: + mass_ratios = self._process_order_reagents(report) + report["mass_ratios"] = mass_ratios # 添加到报文中 + logger.info(f"已计算订单 {order_code} 的试剂质量比") + except Exception as e: + logger.error(f"计算试剂质量比失败: {e}") + report["mass_ratios"] = { + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "reagent_details": [], + "error": str(e) + } + all_reports.append(report) print(f"[create_orders] ✓ 订单 {order_code} 完成") else: @@ -672,6 +776,252 @@ class BioyondCellWorkstation(BioyondWorkstation): return final_result + def create_orders_v2(self, xlsx_path: str) -> Dict[str, Any]: + """ + 从 Excel 解析并创建实验(2.14)- V2版本 + 约定: + - batchId = Excel 文件名(不含扩展名) + - 物料列:所有以 "(g)" 结尾(不再读取"总质量(g)"列) + - totalMass 自动计算为所有物料质量之和 + - createTime 缺失或为空时自动填充为当前日期(YYYY/M/D) + """ + default_path = Path("D:\\UniLab\\Uni-Lab-OS\\unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025122301.xlsx") + path = Path(xlsx_path) if xlsx_path else default_path + print(f"[create_orders_v2] 使用 Excel 路径: {path}") + if path != default_path: + print("[create_orders_v2] 来源: 调用方传入自定义路径") + else: + print("[create_orders_v2] 来源: 使用默认模板路径") + + if not path.exists(): + print(f"[create_orders_v2] ⚠️ Excel 文件不存在: {path}") + raise FileNotFoundError(f"未找到 Excel 文件:{path}") + + try: + df = pd.read_excel(path, sheet_name=0, engine="openpyxl") + except Exception as e: + raise RuntimeError(f"读取 Excel 失败:{e}") + print(f"[create_orders_v2] Excel 读取成功,行数: {len(df)}, 列: {list(df.columns)}") + + # 列名容错:返回可选列名,找不到则返回 None + def _pick(col_names: List[str]) -> Optional[str]: + for c in col_names: + if c in df.columns: + return c + return None + + col_order_name = _pick(["配方ID", "orderName", "订单编号"]) + col_create_time = _pick(["创建日期", "createTime"]) + col_bottle_type = _pick(["配液瓶类型", "bottleType"]) + col_mix_time = _pick(["混匀时间(s)", "mixTime"]) + col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"]) + col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"]) + col_cond = _pick(["电导测试分液体积", "conductivityInfo"]) + col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"]) + print("[create_orders_v2] 列匹配结果:", { + "order_name": col_order_name, + "create_time": col_create_time, + "bottle_type": col_bottle_type, + "mix_time": col_mix_time, + "load": col_load, + "pouch": col_pouch, + "conductivity": col_cond, + "conductivity_bottle_count": col_cond_cnt, + }) + + # 物料列:所有以 (g) 结尾 + material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")] + print(f"[create_orders_v2] 识别到的物料列: {material_cols}") + if not material_cols: + raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。") + + batch_id = path.stem + + def _to_ymd_slash(v) -> str: + # 统一为 "YYYY/M/D";为空或解析失败则用当前日期 + if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "": + ts = datetime.now() + else: + try: + ts = pd.to_datetime(v) + except Exception: + ts = datetime.now() + return f"{ts.year}/{ts.month}/{ts.day}" + + def _as_int(val, default=0) -> int: + try: + if pd.isna(val): + return default + return int(val) + except Exception: + return default + + def _as_float(val, default=0.0) -> float: + try: + if pd.isna(val): + return default + return float(val) + except Exception: + return default + + def _as_str(val, default="") -> str: + if val is None or (isinstance(val, float) and pd.isna(val)): + return default + s = str(val).strip() + return s if s else default + + orders: List[Dict[str, Any]] = [] + + for idx, row in df.iterrows(): + mats: List[Dict[str, Any]] = [] + total_mass = 0.0 + + for mcol in material_cols: + val = row.get(mcol, None) + if val is None or (isinstance(val, float) and pd.isna(val)): + continue + try: + mass = float(val) + except Exception: + continue + if mass > 0: + mats.append({"name": mcol.replace("(g)", ""), "mass": mass}) + total_mass += mass + else: + if mass < 0: + print(f"[create_orders_v2] 第 {idx+1} 行物料 {mcol} 数值为负数: {mass}") + + order_data = { + "batchId": batch_id, + "orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}", + "createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None), + "bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶", + "mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0, + "loadSheddingInfo": _as_float(row[col_load]) if col_load else 0.0, + "pouchCellInfo": _as_float(row[col_pouch]) if col_pouch else 0, + "conductivityInfo": _as_float(row[col_cond]) if col_cond else 0, + "conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0, + "materialInfos": mats, + "totalMass": round(total_mass, 4) # 自动汇总 + } + print(f"[create_orders_v2] 第 {idx+1} 行解析结果: orderName={order_data['orderName']}, " + f"loadShedding={order_data['loadSheddingInfo']}, pouchCell={order_data['pouchCellInfo']}, " + f"conductivity={order_data['conductivityInfo']}, totalMass={order_data['totalMass']}, " + f"material_count={len(mats)}") + + if order_data["totalMass"] <= 0: + print(f"[create_orders_v2] ⚠️ 第 {idx+1} 行总质量 <= 0,可能导致 LIMS 校验失败") + if not mats: + print(f"[create_orders_v2] ⚠️ 第 {idx+1} 行未找到有效物料") + + orders.append(order_data) + print("================================================") + print("orders:", orders) + + print(f"[create_orders_v2] 即将提交订单数量: {len(orders)}") + response = self._post_lims("/api/lims/order/orders", orders) + print(f"[create_orders_v2] 接口返回: {response}") + + # 提取所有返回的 orderCode + data_list = response.get("data", []) + if not data_list: + logger.error("创建订单未返回有效数据!") + return response + + # 收集所有 orderCode + order_codes = [] + for order_item in data_list: + code = order_item.get("orderCode") + if code: + order_codes.append(code) + + if not order_codes: + logger.error("未找到任何有效的 orderCode!") + return response + + print(f"[create_orders_v2] 等待 {len(order_codes)} 个订单完成: {order_codes}") + + # ========== 步骤1: 等待所有订单完成并收集报文(不计算质量比)========== + all_reports = [] + for idx, order_code in enumerate(order_codes, 1): + print(f"[create_orders_v2] 正在等待第 {idx}/{len(order_codes)} 个订单: {order_code}") + result = self.wait_for_order_finish(order_code) + + # 提取报文数据 + if result.get("status") == "success": + report = result.get("report", {}) + all_reports.append(report) + print(f"[create_orders_v2] ✓ 订单 {order_code} 完成") + else: + logger.warning(f"订单 {order_code} 状态异常: {result.get('status')}") + # 即使订单失败,也记录下这个结果 + all_reports.append({ + "orderCode": order_code, + "status": result.get("status"), + "error": result.get("message", "未知错误") + }) + + print(f"[create_orders_v2] 所有订单已完成,共收集 {len(all_reports)} 个报文") + + # ========== 步骤2: 统一计算所有订单的质量比 ========== + print(f"[create_orders_v2] 开始统一计算 {len(all_reports)} 个订单的质量比...") + all_mass_ratios = [] # 存储所有订单的质量比,与reports顺序一致 + + for idx, report in enumerate(all_reports, 1): + order_code = report.get("orderCode", "N/A") + print(f"[create_orders_v2] 计算第 {idx}/{len(all_reports)} 个订单 {order_code} 的质量比...") + + # 只为成功完成的订单计算质量比 + if "error" not in report: + try: + mass_ratios = self._process_order_reagents(report) + # 精简输出,只保留核心质量比信息 + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": mass_ratios.get("real_mass_ratio", {}), + "target_mass_ratio": mass_ratios.get("target_mass_ratio", {}) + }) + logger.info(f"✓ 已计算订单 {order_code} 的试剂质量比") + except Exception as e: + logger.error(f"计算订单 {order_code} 质量比失败: {e}") + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "error": str(e) + }) + else: + # 失败的订单不计算质量比 + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "error": "订单未成功完成" + }) + + print(f"[create_orders_v2] 质量比计算完成") + print("实验记录本========================create_orders_v2========================") + + # 返回所有订单的完成报文 + final_result = { + "status": "all_completed", + "total_orders": len(order_codes), + "bottle_count": len(order_codes), # 明确标注瓶数,用于下游check + "reports": all_reports, # 原始订单报文(不含质量比) + "mass_ratios": all_mass_ratios, # 所有质量比统一放在这里 + "original_response": response + } + + print(f"返回报文数量: {len(all_reports)}") + for i, report in enumerate(all_reports, 1): + print(f"报文 {i}: orderCode={report.get('orderCode', 'N/A')}, status={report.get('status', 'N/A')}") + print("========================") + + return final_result + # 2.7 启动调度 def scheduler_start(self) -> Dict[str, Any]: return self._post_lims("/api/lims/scheduler/start") @@ -683,7 +1033,7 @@ class BioyondCellWorkstation(BioyondWorkstation): 请求体只包含 apiKey 和 requestTime """ return self._post_lims("/api/lims/scheduler/stop") - # 2.9 继续调度 + # 2.9 继续调度 def scheduler_continue(self) -> Dict[str, Any]: """ @@ -867,6 +1217,48 @@ class BioyondCellWorkstation(BioyondWorkstation): result = self.wait_for_order_finish(order_code) return result + def transfer_3_to_2(self, + source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b', + source_x: int = 1, + source_y: int = 1, + source_z: int = 1) -> Dict[str, Any]: + """ + 2.34 3-2 物料转运接口 + + 新建从 3 -> 2 的搬运任务 + + Args: + source_wh_id: 来源仓库 Id (默认为3号仓库) + source_x: 来源位置 X 坐标 + source_y: 来源位置 Y 坐标 + source_z: 来源位置 Z 坐标 + + Returns: + dict: 包含任务 orderId 和 orderCode 的响应 + """ + payload: Dict[str, Any] = { + "sourcePosX": source_x, + "sourcePosY": source_y, + "sourcePosZ": source_z + } + if source_wh_id: + payload["sourceWHID"] = source_wh_id + + logger.info(f"[transfer_3_to_2] 开始转运: 仓库={source_wh_id}, 位置=({source_x}, {source_y}, {source_z})") + response = self._post_lims("/api/lims/order/transfer-task3To2", payload) + + # 等待任务报送成功 + order_code = response.get("data", {}).get("orderCode") + if not order_code: + logger.error("[transfer_3_to_2] 转运任务未返回有效 orderCode!") + return response + + logger.info(f"[transfer_3_to_2] 转运任务已创建: {order_code}") + # 等待完成报送 + result = self.wait_for_order_finish(order_code) + logger.info(f"[transfer_3_to_2] 转运任务完成: {order_code}") + return result + # 3.35 1→2 物料转运 def transfer_1_to_2(self) -> Dict[str, Any]: """ @@ -874,14 +1266,28 @@ class BioyondCellWorkstation(BioyondWorkstation): URL: /api/lims/order/transfer-task1To2 只需要 apiKey 和 requestTime """ + logger.info("[transfer_1_to_2] 开始 1→2 物料转运") response = self._post_lims("/api/lims/order/transfer-task1To2") - # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") + logger.info(f"[transfer_1_to_2] API Response: {response}") + + # 等待任务报送成功 - 处理不同的响应格式 + order_code = None + data_field = response.get("data") + + if isinstance(data_field, dict): + order_code = data_field.get("orderCode") + elif isinstance(data_field, str): + # 某些接口可能直接返回 orderCode 字符串 + order_code = data_field + if not order_code: - logger.error("上料任务未返回有效 orderCode!") + logger.error(f"[transfer_1_to_2] 转运任务未返回有效 orderCode!响应: {response}") return response - # 等待完成报送 + + logger.info(f"[transfer_1_to_2] 转运任务已创建: {order_code}") + # 等待完成报送 result = self.wait_for_order_finish(order_code) + logger.info(f"[transfer_1_to_2] 转运任务完成: {order_code}") return result # 2.5 批量查询实验报告(post过滤关键字查询) diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx index 1b0eb9b20ea509e4085d4b7150cbc330f83f316a..88b233daa2fae48e13e53695798580f473a95c90 100644 GIT binary patch delta 3479 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 C_lqO| delta 3471 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 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py new file mode 100644 index 0000000..195e87a --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py @@ -0,0 +1,12 @@ +import pubchempy as pcp + +cas = "21324-40-3" # 示例 +comps = pcp.get_compounds(cas, namespace="name") +if not comps: + raise ValueError("No hit") + +c = comps[0] + +print("Canonical SMILES:", c.canonical_smiles) +print("Isomeric SMILES:", c.isomeric_smiles) +print("MW:", c.molecular_weight) diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 9a70d5c..ec1c90d 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -133,6 +133,46 @@ WAREHOUSE_MAPPING = { "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" } }, + "手动传递窗右": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + "A03": "3a19deae-2c7a-5876-c454-6b7e224ca927", + "B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a", + "C01": "3a19deae-2c7a-32bc-768e-556647e292f3", + "C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4", + "C03": "3a19deae-2c7a-3056-6504-10dc73fbc276", + "D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440", + "D02": "3a19deae-2c7a-61be-601c-b6fb5610499a", + "D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560", + "E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363", + "E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910", + "E03": "3a19deae-2c7a-b163-2219-23df15200311", + } + }, + "手动传递窗左": { + "uuid": "", + "site_uuids": { + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", + "F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679", + "F03": "3a19deae-2c7a-f7c4-12bd-425799425698", + "G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955", + "G02": "3a19deae-2c7a-204e-95ed-1f1950f28343", + "G03": "3a19deae-2c7a-392b-62f1-4907c66343f8", + "H01": "3a19deae-2c7a-5602-e876-d27aca4e3201", + "H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702", + "H03": "3a19deae-2c7a-780b-8965-2e1345f7e834", + "I01": "3a19deae-2c7a-8849-e172-07de14ede928", + "I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0", + "I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4", + "J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6", + "J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205", + "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" + } + }, "4号手套箱内部堆栈": { "uuid": "", "site_uuids": { diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index f47f560..34f11e9 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -20,8 +20,7 @@ from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck from unilabos.resources.graphio import convert_resources_to_type from unilabos.utils.log import logger -from pymodbus.payload import BinaryPayloadDecoder -from pymodbus.constants import Endian +import struct def _decode_float32_correct(registers): @@ -43,13 +42,19 @@ def _decode_float32_correct(registers): return 0.0 try: - # 使用正确的字节序配置 - decoder = BinaryPayloadDecoder.fromRegisters( - registers, - byteorder=Endian.Big, # 字节序始终为Big - wordorder=Endian.Little # 字序为Little (根据PLC配置) - ) - return decoder.decode_32bit_float() + # Word Order: Little - 交换两个寄存器的顺序 + # Byte Order: Big - 每个寄存器内部使用大端字节序 + low_word = registers[0] + high_word = registers[1] + + # 将两个16位寄存器组合成一个32位值 (Little Word Order) + # 使用大端字节序 ('>') 将每个寄存器转换为字节 + byte_string = struct.pack('>HH', high_word, low_word) + + # 将字节字符串解码为浮点数 (大端) + value = struct.unpack('>f', byte_string)[0] + + return value except Exception as e: logger.error(f"解码FLOAT32失败: {e}, registers: {registers}") return 0.0 @@ -632,23 +637,22 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # 读取 STRING 类型数据 code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) - # 处理 bytes 或 string 类型 - if isinstance(code_little, bytes): - code_str = code_little.decode('utf-8', errors='ignore') - elif isinstance(code_little, str): - code_str = code_little - else: - logger.warning(f"电池二维码返回的类型不支持: {type(code_little)}") + # PyModbus 3.x 返回 string 类型 + if not isinstance(code_little, str): + logger.warning(f"电池二维码返回的类型不支持: {type(code_little)}, 值: {repr(code_little)}") return "N/A" - # 取前8个字符 - raw_code = code_str[:8] + # 从字符串末尾查找连续的字母数字字符(反转字符串) + import re + reversed_str = code_little[::-1] + match = re.match(r'^([A-Za-z0-9]+)', reversed_str) - # LITTLE字节序需要每2个字符交换位置 - clean_code = ''.join([raw_code[i+1] + raw_code[i] for i in range(0, len(raw_code), 2)]) + if not match: + logger.warning(f"未找到有效的电池二维码数据,原始字符串: {repr(code_little)}") + return "N/A" - # 去除空字符和空格 - decoded = clean_code.replace('\x00', '').replace('\r', '').replace('\n', '').strip() + # 提取匹配到的字符串(已经是正确顺序) + decoded = match.group(1)[:8] # 只取前8个字符 return decoded if decoded else "N/A" except Exception as e: @@ -663,23 +667,22 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # 读取 STRING 类型数据 code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) - # 处理 bytes 或 string 类型 - if isinstance(code_little, bytes): - code_str = code_little.decode('utf-8', errors='ignore') - elif isinstance(code_little, str): - code_str = code_little - else: - logger.warning(f"电解液二维码返回的类型不支持: {type(code_little)}") + # PyModbus 3.x 返回 string 类型 + if not isinstance(code_little, str): + logger.warning(f"电解液二维码返回的类型不支持: {type(code_little)}, 值: {repr(code_little)}") return "N/A" - # 取前8个字符 - raw_code = code_str[:8] + # 从字符串末尾查找连续的字母数字字符(反转字符串) + import re + reversed_str = code_little[::-1] + match = re.match(r'^([A-Za-z0-9]+)', reversed_str) - # LITTLE字节序需要每2个字符交换位置 - clean_code = ''.join([raw_code[i+1] + raw_code[i] for i in range(0, len(raw_code), 2)]) + if not match: + logger.warning(f"未找到有效的电解液二维码数据,原始字符串: {repr(code_little)}") + return "N/A" - # 去除空字符和空格 - decoded = clean_code.replace('\x00', '').replace('\r', '').replace('\n', '').strip() + # 提取匹配到的字符串(已经是正确顺序) + decoded = match.group(1)[:8] # 只取前8个字符 return decoded if decoded else "N/A" except Exception as e: @@ -940,6 +943,111 @@ class CoinCellAssemblyWorkstation(WorkstationBase): time.sleep(1) #自动按钮置False + def func_sendbottle_allpack_multi( + self, + elec_num, + elec_use_num, + elec_vol: int = 50, + # 电解液双滴模式参数 + dual_drop_mode: bool = False, + dual_drop_first_volume: int = 25, + dual_drop_suction_timing: bool = False, + dual_drop_start_timing: bool = False, + assembly_type: int = 7, + assembly_pressure: int = 4200, + # 来自原 qiming_coin_cell_code 的参数 + fujipian_panshu: int = 0, + fujipian_juzhendianwei: int = 0, + gemopanshu: int = 0, + gemo_juzhendianwei: int = 0, + qiangtou_juzhendianwei: int = 0, + lvbodian: bool = True, + battery_pressure_mode: bool = True, + battery_clean_ignore: bool = False, + file_path: str = "/Users/sml/work" + ) -> Dict[str, Any]: + """ + 发送瓶数+简化组装函数(适用于第二批次及后续批次) + + 合并了发送瓶数和简化组装流程,用于连续批次生产。 + 适用场景:设备已完成初始化和启动,仍在自动模式下运行。 + + Args: + elec_num: 电解液瓶数 + elec_use_num: 每瓶电解液组装的电池数 + elec_vol: 电解液吸液量 (μL) + dual_drop_mode: 电解液添加模式 (False=单次滴液, True=二次滴液) + dual_drop_first_volume: 二次滴液第一次排液体积 (μL) + dual_drop_suction_timing: 二次滴液吸液时机 (False=正常吸液, True=先吸液) + dual_drop_start_timing: 二次滴液开始滴液时机 (False=正极片前, True=正极片后) + assembly_type: 组装类型 (7=不用铝箔垫, 8=使用铝箔垫) + assembly_pressure: 电池压制力 (N) + fujipian_panshu: 负极片盘数 + fujipian_juzhendianwei: 负极片矩阵点位 + gemopanshu: 隔膜盘数 + gemo_juzhendianwei: 隔膜矩阵点位 + qiangtou_juzhendianwei: 枪头盒矩阵点位 + lvbodian: 是否使用铝箔垫片 + battery_pressure_mode: 是否启用压力模式 + battery_clean_ignore: 是否忽略电池清洁 + file_path: 实验记录保存路径 + + Returns: + dict: 包含组装结果的字典 + + 注意: + - 第一次启动需先调用 func_pack_device_init_auto_start_combined() + - 后续批次直接调用此函数即可 + """ + logger.info("=" * 60) + logger.info("开始发送瓶数+简化组装流程...") + logger.info(f"电解液瓶数: {elec_num}, 每瓶电池数: {elec_use_num}") + logger.info("=" * 60) + + # 步骤1: 发送电解液瓶数(触发物料搬运) + logger.info("步骤1/2: 发送电解液瓶数,触发物料搬运...") + try: + self.func_pack_send_bottle_num(elec_num) + logger.info("✓ 瓶数发送完成,物料搬运中...") + except Exception as e: + logger.error(f"发送瓶数失败: {e}") + return { + "success": False, + "error": f"发送瓶数失败: {e}", + "total_batteries": 0, + "batteries": [] + } + + # 步骤2: 执行简化组装流程 + logger.info("步骤2/2: 开始简化组装流程...") + result = self.func_allpack_cmd_simp( + elec_num=elec_num, + elec_use_num=elec_use_num, + elec_vol=elec_vol, + dual_drop_mode=dual_drop_mode, + dual_drop_first_volume=dual_drop_first_volume, + dual_drop_suction_timing=dual_drop_suction_timing, + dual_drop_start_timing=dual_drop_start_timing, + assembly_type=assembly_type, + assembly_pressure=assembly_pressure, + fujipian_panshu=fujipian_panshu, + fujipian_juzhendianwei=fujipian_juzhendianwei, + gemopanshu=gemopanshu, + gemo_juzhendianwei=gemo_juzhendianwei, + qiangtou_juzhendianwei=qiangtou_juzhendianwei, + lvbodian=lvbodian, + battery_pressure_mode=battery_pressure_mode, + battery_clean_ignore=battery_clean_ignore, + file_path=file_path + ) + + logger.info("=" * 60) + logger.info("发送瓶数+简化组装流程完成") + logger.info(f"总组装电池数: {result.get('total_batteries', 0)}") + logger.info("=" * 60) + + return result + # 下发参数 #def func_pack_send_msg_cmd(self, elec_num: int, elec_use_num: int, elec_vol: float, assembly_type: int, assembly_pressure: int) -> bool: diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv deleted file mode 100644 index 3f7b357..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv +++ /dev/null @@ -1,64 +0,0 @@ -Name,DataType,InitValue,Comment,Attribute,DeviceType,Address, -COIL_SYS_START_CMD,BOOL,,,,coil,9010, -COIL_SYS_STOP_CMD,BOOL,,,,coil,9020, -COIL_SYS_RESET_CMD,BOOL,,,,coil,9030, -COIL_SYS_HAND_CMD,BOOL,,,,coil,9040, -COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050, -COIL_SYS_INIT_CMD,BOOL,,,,coil,9060, -COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700, -COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd -COIL_SYS_START_STATUS,BOOL,,,,coil,9210, -COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220, -COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230, -COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240, -COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250, -COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260, -COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500, -COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status -REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000, -REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num -REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol -REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type -REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure -REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num -REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage -REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004, -REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006, -REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008, -REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight -REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time -REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure -REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume -REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num -REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code() -REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code() -REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code() -REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure -REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content -REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content -UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720, -UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520, -REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496, -REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000, -UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730, -UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530, -REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8 -COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340, -REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440, -REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450, -REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480, -REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443, -REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453, -REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,9360,电池压制模式 -,,,,,,, -,BOOL,,视觉对位(false:使用,true:忽略),,coil,9300,视觉对位 -,BOOL,,复检(false:使用,true:忽略),,coil,9310,视觉复检 -,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,9320,手套箱左仓 -,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,9420,手套箱右仓 -,BOOL,,真空检知(false:使用,true:忽略),,coil,9350,真空检知 -,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,9370,滴液模式 -,BOOL,,正极片称重(false:使用,true:忽略),,coil,9380,正极片称重 -,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,9390,正负极反装 -,BOOL,,压制清洁(false:使用,true:忽略),,coil,9400,压制清洁 -,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,9410,负极片摆盘方式 -REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,9460, diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv deleted file mode 100644 index 9212f9b..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv +++ /dev/null @@ -1,159 +0,0 @@ -Name,DataType,Comment,DeviceType,Address,, -COIL_SYS_START_CMD,BOOL,豸,coil,8010,, -COIL_SYS_STOP_CMD,BOOL,豸ֹͣ,coil,8020,, -COIL_SYS_RESET_CMD,BOOL,豸λ,coil,8030,, -COIL_SYS_HAND_CMD,BOOL,豸ֶģʽ,coil,8040,, -COIL_SYS_AUTO_CMD,BOOL,豸Զģʽ,coil,8050,, -COIL_SYS_INIT_CMD,BOOL,豸ʼģʽ,coil,8060,, -COIL_SYS_STOP_STATUS,BOOL,豸ͣ,coil,8220,, -,,,,,, -,BOOL,UNILAB͵Һƿ,coil,8720,, -,BOOL,豸ܵҺƿ,coil,8520,, -REG_MSG_ELECTROLYTE_NUM,WORD,Һʹƿ,hold_register,496,, -,WORD,Ƭ̾λʼλ0,hold_register,440,, -,WORD,Ĥ̾λʼλ0,hold_register,450,, -,WORD,Һƿ_Ͼλʼλ0,hold_register,460,, -,WORD,Һƿ_վλʼλ0,hold_register,430,, -,WORD,g_Һƿ_޸˸׾λʼλ0,hold_register,470,, -,WORD,Һǹͷλʼλ0,hold_register,480,, -,WORD,øƬ,hold_register,443,, -,WORD,øĤ,hold_register,453,, -,,,,,, -COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,UNILAB䷽,coil,8700,, -COIL_REQUEST_REC_MSG_STATUS,BOOL,豸䷽,coil,8500,, -REG_MSG_ELECTROLYTE_USE_NUM,INT16,ƿҺʹô,hold_register,11000,, -REG_MSG_ELECTROLYTE_VOLUME,INT16,Һȡ,hold_register,11004,, -REG_MSG_ASSEMBLY_PRESSURE,INT16,װѹ,hold_register,11008,, -REG_DATA_ELECTROLYTE_CODE,STRING,Һάк,hold_register,10020,, -,BOOL,Ӿλfalse:ʹãtrue:ԣ,coil,8300,, -,BOOL,죨false:ʹãtrue:ԣ,coil,8310,, -,BOOL,_֣false:ʹãtrue:ԣ,coil,8320,, -,BOOL,_Ҳ֣false:ʹãtrue:ԣ,coil,8420,, -,BOOL,еְ̣false:ʹãtrue:ԣ,coil,8330,, -,BOOL,ϣfalse:ʹãtrue:ԣ,coil,8340,, -,BOOL,ռ֪false:ʹãtrue:ԣ,coil,8350,, -,BOOL,ѹģʽfalse:ѹģʽTrue:ģʽ,coil,8360,, -,BOOL,Һģʽfalse:εҺtrue:εҺ,coil,8370,, -,BOOL,Ƭأfalse:ʹãtrue:ԣ,coil,8380,, -,BOOL,Ƭװʽfalse:װtrue:װ,coil,8390,, -,BOOL,ѹࣨfalse:ʹãtrue:ԣ,coil,8400,, -,BOOL,̷̰ʽfalse:ˮƽ̣true:ѵ̣,coil,8410,, -COIL_SYS_UNILAB_INTERACT ,BOOL,Unilabfalse:ʹãtrue:ԣ,coil,8450,, -,BOOL,Եࣨfalse:ʹãtrue:ԣ,colil,8460,, -,,,,,, -COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,UNILAB䷽,coil,8510,, -COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,UNILABܲ,coil,8710,, -REG_DATA_POLE_WEIGHT,FLOAT32,ǰƬ,hold_register,10010,, -REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,ǰŵװʱ,hold_register,10012,, -REG_DATA_ASSEMBLY_PRESSURE,INT16,ǰװѹ,hold_register,10014,, -REG_DATA_ELECTROLYTE_VOLUME,INT16,ǰҺע,hold_register,10016,, -REG_DATA_ASSEMBLY_TYPE,INT16,װƬѵʽ(7/8),hold_register,10018,, -REG_DATA_ELECTROLYTE_CODE,STRING,Һάк,hold_register,10020,, -REG_DATA_COIN_CELL_CODE,STRING,ضάк,hold_register,10030,, -REG_DATA_STACK_VISON_CODE,STRING,϶ѵͼƬ,hold_register,10040,, -REG_DATA_ELECTROLYTE_USE_NUM,INT16,ǰ缫ҺװR,hold_register,10000,, -REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,ǰصѹ,hold_register,10002,, -,INT,еȡϼĴ1-ǡ2-桢3-Ƭ4-Ĥ5-Ƭ6-ƽ桢7-桢8-ǣ,hold_register,10060,, -,,,,,, -,INT,ǰƬʣR,hold_register,10062,PLCַ,1223- -,INT,ǰĤR,hold_register,10064,, -,INT,Һ״̬루R,hold_register,10066,, -,REAL,·ѹOKֵR,hold_register,10068,, -,REAL,·ѹOKֵR,hold_register,10070,, -,INT,ǰװR,hold_register,10072,, -,INT,ǰװR,hold_register,10074,, -,REAL,10mmƬʣR,hold_register,520,HMIַ, -,REAL,12mmƬʣR,hold_register,522,, -,REAL,16mmƬʣR,hold_register,524,, -,REAL,ʣR,hold_register,526,, -,REAL,ʣR,hold_register,528,, -,REAL,ƽʣR,hold_register,530,, -,REAL,ʣR,hold_register,532,, -,REAL,ʣR,hold_register,534,, -,REAL,ƷʣR,hold_register,536,, -,REAL,ƷNGʣR,hold_register,538,, -,,,,,, -,REAL,10mmƬȣW,hold_register,540,, -,REAL,12mmƬȣW,hold_register,542,, -,REAL,16mmƬȣW,hold_register,544,, -,REAL,ȣW,hold_register,546,, -,REAL,ǺȣW,hold_register,548,, -,REAL,ƽȣW,hold_register,550,, -,REAL,øǺȣW,hold_register,552,, -,REAL,õȣW,hold_register,554,, -,REAL,óƷغȣW,hold_register,556,, -,,,,,, -,,,,,, -REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,ѹ,hold_register,10050,, -REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,ˮ,hold_register,10052,, -REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,hold_register,10054,, -,,,,,, -,BOOL,쳣100-ϵͳ쳣,coil,1000,, -,BOOL,쳣101-ͣ,coil,1010,, -,BOOL,쳣111-伱ͣ,coil,1110,, -,BOOL,쳣112-ڹդڵ,coil,1120,, -,BOOL,쳣160-Һǹͷȱ,coil,1600,, -,BOOL,쳣161-ȱ,coil,1610,, -,BOOL,쳣162-ȱ,coil,1620,, -,BOOL,쳣163-Ƭȱ,coil,1630,, -,BOOL,쳣164-Ĥȱ,coil,1640,, -,BOOL,쳣165-Ƭȱ,coil,1650,, -,BOOL,쳣166-ƽȱ,coil,1660,, -,BOOL,쳣167-ȱ,coil,1670,, -,BOOL,쳣168-ȱ,coil,1680,, -,BOOL,쳣169-Ʒ,coil,1690,, -,BOOL,쳣201-ŷ01쳣,coil,2010,, -,BOOL,쳣202-ŷ02쳣,coil,2020,, -,BOOL,쳣203-ŷ03쳣,coil,2030,, -,BOOL,쳣204-ŷ04쳣,coil,2040,, -,BOOL,쳣205-ŷ05쳣,coil,2050,, -,BOOL,쳣206-ŷ06쳣,coil,2060,, -,BOOL,쳣207-ŷ07쳣,coil,2070,, -,BOOL,쳣208-ŷ08쳣,coil,2080,, -,BOOL,쳣209-ŷ09쳣,coil,2090,, -,BOOL,쳣210-ŷ10쳣,coil,2100,, -,BOOL,쳣211-ŷ11쳣,coil,2110,, -,BOOL,쳣212-ŷ12쳣,coil,2120,, -,BOOL,쳣213-ŷ13쳣,coil,2130,, -,BOOL,쳣214-ŷ14쳣,coil,2140,, -,BOOL,쳣250-Ԫ쳣,coil,2500,, -,BOOL,쳣251-ҺǹͨѶ쳣,coil,2510,, -,BOOL,쳣252-Һǹ,coil,2520,, -,BOOL,쳣256-צ쳣,coil,2560,, -,BOOL,쳣262-RBδ֪λ,coil,2620,, -,BOOL,쳣263-RBXYZ,coil,2630,, -,BOOL,쳣264-RBӾ,coil,2640,, -,BOOL,쳣265-RB1#ȡʧ,coil,2650,, -,BOOL,쳣266-RB2#ȡʧ,coil,2660,, -,BOOL,쳣267-RB3#ȡʧ,coil,2670,, -,BOOL,쳣268-RB4#ȡʧ,coil,2680,, -,BOOL,쳣269-RBȡʧ,coil,2690,, -,BOOL,쳣280-RBײ쳣,coil,2800,, -,BOOL,쳣290-ӾϵͳͨѶ쳣,coil,2900,, -,BOOL,쳣291-ӾλNG쳣,coil,2910,, -,BOOL,쳣292-ɨǹͨѶ쳣,coil,2920,, -,BOOL,쳣310-쳣,coil,3100,, -,BOOL,쳣311-쳣,coil,3110,, -,BOOL,쳣312-쳣,coil,3120,, -,BOOL,쳣313-쳣,coil,3130,, -,BOOL,쳣340-·ѹ쳣,coil,3400,, -,BOOL,쳣342-·ѹ쳣,coil,3420,, -,BOOL,쳣344-·ѹѹ쳣,coil,3440,, -,BOOL,쳣350-쳣,coil,3500,, -,BOOL,쳣352-쳣,coil,3520,, -,BOOL,쳣354-ϴ޳쳣,coil,3540,, -,BOOL,쳣356-ϴ޳ѹ쳣,coil,3560,, -,BOOL,쳣360-Һƿλ쳣,coil,3600,, -,BOOL,쳣362-Һǹͷжλ쳣,coil,3620,, -COIL ALARM_364_SERVO_DRIVE_ERROR,BOOL,쳣364-Լƿצ쳣,coil,3640,, -COIL ALARM_367_SERVO_DRIVER_ERROR,BOOL,쳣366-Լƿצ쳣,coil,3660,, -COIL ALARM_370_SERVO_MODULE_ERROR,BOOL,쳣370-ѹģ鴵쳣,coil,3700,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -COIL + ģ/ + д+»߷ָ--boolֵ,,,,,, -REG + ģ/ + д+»߷ָ--ԼĴ,,,,,, diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv deleted file mode 100644 index 8a8c176..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv +++ /dev/null @@ -1,2 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251224_172304,-5.537573695435827e-37,-48.45097351074219,1.372190511464448e+16,3820,30,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv deleted file mode 100644 index d51070e..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv +++ /dev/null @@ -1,2 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251225_105600,5.566961054206384e-37,-53149746331648.0,3271557120.0,3658,10,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv deleted file mode 100644 index 124bff1..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv +++ /dev/null @@ -1,2 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251229_161836,-5.537573695435827e-37,8.919000478163591e+20,-3.806253867691382e-29,3544,20,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv deleted file mode 100644 index 7fefb59..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv +++ /dev/null @@ -1,9 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251230_182319,0.01600000075995922,13.899999618530273,175.0,3836,20,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' -20251230_185306,0.01600000075995922,13.639999389648438,625.0,3819,20,7,deaoR, -20251230_192124,0.0,8.949999809265137,414.0,3803,20,8,deaoR, -20251230_195621,3.8359999656677246,10.069999694824219,205.0,3350,20,8,LG600001,19311909 -20251230_200830,0.7929999828338623,9.34999942779541,18.0,3318,20,8,LG600001,19533419 -20251230_201123,0.0,9.169999122619629,17.0,3269,20,8,LG600001,20054389 -20251230_201410,0.0,9.569999694824219,18.0,3237,20,8,LG600001,YS102704 -20251230_201659,0.0,9.699999809265137,169.0,3318,20,8,LG600001,20112754 diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv deleted file mode 100644 index f9cde47..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv +++ /dev/null @@ -1,3 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20260106_221708,0.03200000151991844,26.26999855041504,18.0,3803,30,7,NoRead88,22000063 -20260106_221957,0.11299999803304672,26.26999855041504,170.0,3787,30,7,LG600001,22124813 diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv new file mode 100644 index 0000000..de333a0 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv @@ -0,0 +1,6 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code +20260110_153356,0.0,23.889999389648438,17.0,3609,40,7,deaoR, +20260110_153640,3.430999994277954,23.719999313354492,162.0,3560,40,7,deaoR, +20260110_162905,0.0,23.920000076293945,772.0,3625,30,7,LG600001,YS103130 +20260110_163526,3.430999994277954,24.010000228881836,234.0,3690,30,7,LG600001,YS102964 +20260110_164530,0.0,23.589998245239258,219.0,3690,30,7,LG600001,YS102857 diff --git a/unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py b/unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py deleted file mode 100644 index adf2082..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py +++ /dev/null @@ -1,536 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""扣式电池组装系统 - 交互式CSV导出演示脚本(增强版) - -此脚本专为交互式使用优化,提供清洁的命令行界面, -禁用了所有调试信息输出,确保用户可以顺畅地输入命令。 - -主要功能: -1. 手动导出设备数据到CSV文件(包含6个关键数据字段) -2. 查看CSV文件内容和导出状态 -3. 兼容原有的电池组装完成状态自动导出功能 -4. 实时查看设备数据和电池数量 - -数据字段: -- timestamp: 时间戳 -- assembly_time: 单颗电池组装时间(秒) -- open_circuit_voltage: 开路电压值(V) -- pole_weight: 正极片称重数据(g) -- battery_qr_code: 电池二维码序列号 -- electrolyte_qr_code: 电解液二维码序列号 - -使用方法: -1. 确保设备已连接并可正常通信 -2. 运行此脚本: python interactive_battery_export_demo.py -3. 使用交互式命令控制导出功能 -""" - -import time -import os -import sys -import logging -import csv -from datetime import datetime -from pathlib import Path - -# 完全禁用所有调试和信息级别的日志输出 -logging.getLogger().setLevel(logging.CRITICAL) -logging.getLogger('pymodbus').setLevel(logging.CRITICAL) -logging.getLogger('unilabos').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging.tcp').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging.base').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging.decoders').setLevel(logging.CRITICAL) - -# 添加当前目录到Python路径,以便正确导入模块 -current_dir = Path(__file__).parent -sys.path.insert(0, str(current_dir.parent.parent.parent)) # 添加unilabos根目录 -sys.path.insert(0, str(current_dir)) # 添加当前目录 - -# 导入扣式电池组装系统 -try: - from unilabos.devices.coin_cell_assembly.coin_cell_assembly_system import Coin_Cell_Assembly -except ImportError: - # 如果上述导入失败,尝试直接导入 - try: - from coin_cell_assembly_system import Coin_Cell_Assembly - except ImportError as e: - print(f"导入错误: {e}") - print("请确保在正确的目录下运行此脚本,或者将unilabos添加到Python路径中") - sys.exit(1) - -def clear_screen(): - """清屏函数""" - os.system('cls' if os.name == 'nt' else 'clear') - -def print_header(): - """打印程序头部信息""" - print("="*60) - print(" 扣式电池组装系统 - 交互式CSV导出控制台") - print("="*60) - print() - -def print_commands(): - """打印可用命令""" - print("可用命令:") - print(" start - 启动电池组装完成状态导出") - print(" stop - 停止导出") - print(" status - 查看导出状态") - print(" data - 查看当前设备数据") - print(" count - 查看当前电池数量") - print(" export - 手动导出当前数据到CSV") - print(" setpath - 设置自定义CSV文件路径") - print(" view - 查看CSV文件内容") - print(" force - 强制继续CSV导出(即使设备停止)") - print(" detail - 显示详细设备状态") - print(" clear - 清屏") - print(" help - 显示帮助信息") - print(" quit - 退出程序") - print("-"*60) - -def print_status_info(device, csv_file_path): - """打印状态信息""" - try: - status = device.get_csv_export_status() - is_running = status.get('running', False) - export_file = status.get('file_path', None) - thread_alive = status.get('thread_alive', False) - device_status = status.get('device_status', 'N/A') - battery_count = status.get('battery_count', 'N/A') - - print(f"导出状态: {'运行中' if is_running else '已停止'}") - print(f"导出文件: {export_file if export_file else 'N/A'}") - print(f"线程状态: {'活跃' if thread_alive else '非活跃'}") - print(f"设备状态: {device_status}") - print(f"电池计数: {battery_count}") - - # 检查手动导出的CSV文件 - if os.path.exists(csv_file_path): - file_size = os.path.getsize(csv_file_path) - print(f"手动导出文件: {csv_file_path} ({file_size} 字节)") - else: - print(f"手动导出文件: {csv_file_path} (不存在)") - - # 显示设备运行状态 - try: - print("\n=== 设备运行状态 ===") - print(f"系统启动状态: {device.sys_start_status}") - print(f"系统停止状态: {device.sys_stop_status}") - print(f"自动模式状态: {device.sys_auto_status}") - print(f"手动模式状态: {device.sys_hand_status}") - except Exception as e: - print(f"获取设备运行状态失败: {e}") - - except Exception as e: - print(f"获取状态失败: {e}") - -def collect_device_data(device): - """收集设备的六个关键数据""" - try: - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - # 读取各项数据,添加错误处理和重试机制 - try: - assembly_time = device.data_assembly_time # 单颗电池组装时间(秒) - # 确保返回的是数值类型 - if isinstance(assembly_time, (list, tuple)) and len(assembly_time) > 0: - assembly_time = float(assembly_time[0]) - else: - assembly_time = float(assembly_time) - except Exception as e: - print(f"读取组装时间失败: {e}") - assembly_time = 0.0 - - try: - open_circuit_voltage = device.data_open_circuit_voltage # 开路电压值(V) - # 确保返回的是数值类型 - if isinstance(open_circuit_voltage, (list, tuple)) and len(open_circuit_voltage) > 0: - open_circuit_voltage = float(open_circuit_voltage[0]) - else: - open_circuit_voltage = float(open_circuit_voltage) - except Exception as e: - print(f"读取开路电压失败: {e}") - open_circuit_voltage = 0.0 - - try: - pole_weight = device.data_pole_weight # 正极片称重数据(g) - # 确保返回的是数值类型 - if isinstance(pole_weight, (list, tuple)) and len(pole_weight) > 0: - pole_weight = float(pole_weight[0]) - else: - pole_weight = float(pole_weight) - except Exception as e: - print(f"读取正极片重量失败: {e}") - pole_weight = 0.0 - - try: - assembly_pressure = device.data_assembly_pressure # 电池压制力(N) - # 确保返回的是数值类型 - if isinstance(assembly_pressure, (list, tuple)) and len(assembly_pressure) > 0: - assembly_pressure = int(assembly_pressure[0]) - else: - assembly_pressure = int(assembly_pressure) - except Exception as e: - print(f"读取压制力失败: {e}") - assembly_pressure = 0 - - try: - battery_qr_code = device.data_coin_cell_code # 电池二维码序列号 - # 处理字符串类型数据 - if isinstance(battery_qr_code, str): - battery_qr_code = battery_qr_code.strip() - else: - battery_qr_code = str(battery_qr_code) - except Exception as e: - print(f"读取电池二维码失败: {e}") - battery_qr_code = "N/A" - - try: - electrolyte_qr_code = device.data_electrolyte_code # 电解液二维码序列号 - # 处理字符串类型数据 - if isinstance(electrolyte_qr_code, str): - electrolyte_qr_code = electrolyte_qr_code.strip() - else: - electrolyte_qr_code = str(electrolyte_qr_code) - except Exception as e: - print(f"读取电解液二维码失败: {e}") - electrolyte_qr_code = "N/A" - - # 获取电池数量 - try: - battery_count = device.data_assembly_coin_cell_num - # 确保返回的是数值类型 - if isinstance(battery_count, (list, tuple)) and len(battery_count) > 0: - battery_count = int(battery_count[0]) - else: - battery_count = int(battery_count) - except Exception as e: - print(f"读取电池数量失败: {e}") - battery_count = 0 - - return { - 'Timestamp': timestamp, - 'Battery_Count': battery_count, - 'Assembly_Time': assembly_time, - 'Open_Circuit_Voltage': open_circuit_voltage, - 'Pole_Weight': pole_weight, - 'Assembly_Pressure': assembly_pressure, - 'Battery_Code': battery_qr_code, - 'Electrolyte_Code': electrolyte_qr_code - } - except Exception as e: - print(f"收集数据时出错: {e}") - return None - -def export_to_csv(data, csv_file_path): - """将数据导出到CSV文件""" - try: - # 检查文件是否存在,如果不存在则创建并写入表头 - file_exists = os.path.exists(csv_file_path) - - # 确保目录存在 - csv_dir = os.path.dirname(csv_file_path) - if csv_dir: - os.makedirs(csv_dir, exist_ok=True) - - # 确保数值字段为正确的数值类型,避免前导单引号问题 - processed_data = data.copy() - - # 处理数值字段,确保它们是数值类型而不是字符串,增强错误处理 - numeric_fields = ['Battery_Count', 'Assembly_Time', 'Open_Circuit_Voltage', 'Pole_Weight', 'Assembly_Pressure'] - for field in numeric_fields: - if field in processed_data: - try: - value = processed_data[field] - # 处理可能的列表或元组类型 - if isinstance(value, (list, tuple)) and len(value) > 0: - value = value[0] - - if field == 'Battery_Count' or field == 'Assembly_Pressure': - processed_data[field] = int(float(value)) # 先转float再转int,处理字符串数字 - else: - processed_data[field] = float(value) - except (ValueError, TypeError, IndexError) as e: - print(f"字段 {field} 类型转换失败: {e}, 使用默认值") - processed_data[field] = 0 if field == 'Battery_Count' else 0.0 - - # 处理字符串字段 - for field in ['Battery_Code', 'Electrolyte_Code']: - if field in processed_data: - try: - value = processed_data[field] - if isinstance(value, (list, tuple)) and len(value) > 0: - value = value[0] - processed_data[field] = str(value).strip() - except Exception as e: - print(f"字段 {field} 处理失败: {e}, 使用默认值") - processed_data[field] = "N/A" - - with open(csv_file_path, 'a', newline='', encoding='utf-8') as csvfile: - fieldnames = ['Timestamp', 'Battery_Count', 'Assembly_Time', 'Open_Circuit_Voltage', - 'Pole_Weight', 'Assembly_Pressure', 'Battery_QR_Code', 'Electrolyte_QR_Code'] - writer = csv.DictWriter(csvfile, fieldnames=fieldnames, quoting=csv.QUOTE_MINIMAL) - - # 如果文件不存在,写入表头 - if not file_exists: - writer.writeheader() - print(f"创建新的CSV文件: {csv_file_path}") - - # 写入数据 - writer.writerow(processed_data) - print(f"数据已导出到: {csv_file_path}") - - return True - except Exception as e: - print(f"导出CSV时出错: {e}") - return False - -def view_csv_content(csv_file_path, lines=10): - """查看CSV文件内容""" - try: - if not os.path.exists(csv_file_path): - print("CSV文件不存在") - return - - with open(csv_file_path, 'r', encoding='utf-8') as csvfile: - content = csvfile.readlines() - - if not content: - print("CSV文件为空") - return - - print(f"CSV文件内容 (显示最后{min(lines, len(content))}行):") - print("-" * 80) - - # 显示表头 - if len(content) > 0: - print(content[0].strip()) - print("-" * 80) - - # 显示最后几行数据 - start_line = max(1, len(content) - lines + 1) - for i in range(start_line, len(content)): - print(content[i].strip()) - - print("-" * 80) - print(f"总共 {len(content)-1} 条数据记录") - - except Exception as e: - print(f"读取CSV文件时出错: {e}") - -def interactive_demo(): - """ - 交互式演示模式(优化版) - """ - clear_screen() - print_header() - - print("正在初始化设备连接...") - print("设备地址: 192.168.1.20:502") - print("正在尝试连接...") - - try: - device = Coin_Cell_Assembly(address="192.168.1.20", port="502") - print("✓ 设备连接成功") - - # 测试设备数据读取 - print("正在测试设备数据读取...") - try: - test_count = device.data_assembly_coin_cell_num - print(f"✓ 当前电池数量: {test_count}") - except Exception as e: - print(f"⚠ 数据读取测试失败: {e}") - print("设备连接正常,但数据读取可能存在问题") - - except Exception as e: - print(f"✗ 设备连接失败: {e}") - print("请检查以下项目:") - print("1. 设备是否已开机并正常运行") - print("2. 网络连接是否正常") - print("3. 设备IP地址是否为192.168.1.20") - print("4. Modbus服务是否在端口502上运行") - input("按回车键退出...") - return - - csv_file_path = "battery_data_export.csv" - print(f"CSV文件路径: {os.path.abspath(csv_file_path)}") - print() - print("功能说明:") - print("- 支持手动导出当前设备数据到CSV文件") - print("- 包含六个关键数据: 组装时间、开路电压、正极片重量、电池码、电解液码") - print("- 电池码和电解液码可能显示为N/A(当二维码读取失败时)") - print("- 支持查看CSV文件内容和导出状态") - print("- 兼容原有的电池组装完成状态自动导出功能") - print() - - print_commands() - - while True: - try: - command = input("\n请输入命令 > ").strip().lower() - - if command == "start": - print("启动电池组装完成状态导出...") - try: - success, message = device.start_battery_completion_export(csv_file_path) - if success: - print(f"✓ {message}") - print("系统正在监控电池组装完成状态...") - else: - print(f"✗ {message}") - except Exception as e: - print(f"启动导出时出错: {e}") - - elif command == "stop": - print("停止导出...") - try: - success, message = device.stop_csv_export() - if success: - print(f"✓ {message}") - else: - print(f"✗ {message}") - except Exception as e: - print(f"停止导出时出错: {e}") - - elif command == "force": - print("强制继续CSV导出...") - try: - success, message = device.force_continue_csv_export() - if success: - print(f"✓ {message}") - print("注意: CSV导出将继续监控数据变化,即使设备处于停止状态") - else: - print(f"✗ {message}") - except AttributeError: - print("✗ 当前版本不支持强制继续功能") - except Exception as e: - print(f"✗ 强制继续失败: {e}") - - elif command == "detail": - print("=== 详细设备状态 ===") - print_status_info(device, csv_file_path) - - elif command == "status": - print_status_info(device, csv_file_path) - - elif command == "data": - print("读取当前设备数据...") - try: - data = collect_device_data(device) - if data: - print("\n=== 当前设备数据 ===") - print(f"时间戳: {data['Timestamp']}") - print(f"电池数量: {data['Battery_Count']}") - print(f"单颗电池组装时间: {data['Assembly_Time']:.2f} 秒") - print(f"开路电压值: {data['Open_Circuit_Voltage']:.4f} V") - print(f"正极片称重数据: {data['Pole_Weight']:.4f} g") - print(f"电池压制力: {data['Assembly_Pressure']} N") - print(f"电池二维码序列号: {data['Battery_Code']}") - print(f"电解液二维码序列号: {data['Electrolyte_Code']}") - print("===================") - else: - print("无法获取设备数据") - except Exception as e: - print(f"读取数据时出错: {e}") - - elif command == "count": - print("读取当前电池数量...") - try: - count = device.data_assembly_coin_cell_num - print(f"当前已完成电池数量: {count}") - except Exception as e: - print(f"读取电池数量时出错: {e}") - - elif command == "export": - print("正在收集设备数据并导出到CSV...") - data = collect_device_data(device) - if data: - print(f"收集到数据: 电池数量={data.get('Battery_Count', 'N/A')}, 组装时间={data.get('Assembly_Time', 'N/A')}s") - if export_to_csv(data, csv_file_path): - print("✓ 数据已成功导出到CSV文件") - print(f"导出数据: 时间={data['Timestamp']}, 电池数量={data['Battery_Count']}, 组装时间={data['Assembly_Time']}秒, " - f"电压={data['Open_Circuit_Voltage']}V, 重量={data['Pole_Weight']}g, 压制力={data['Assembly_Pressure']}N") - print(f"电池码={data['Battery_Code']}, 电解液码={data['Electrolyte_Code']}") - else: - print("✗ 导出失败") - else: - print("✗ 数据收集失败,无法导出!请检查设备连接状态。") - # 尝试重新连接设备 - try: - if hasattr(device, 'connect'): - device.connect() - print("尝试重新连接设备...") - except Exception as e: - print(f"重新连接失败: {e}") - - elif command == "setpath": - print("设置自定义CSV文件路径") - print(f"当前CSV文件路径: {csv_file_path}") - new_path = input("请输入新的CSV文件路径(包含文件名,如: D:/data/my_battery_data.csv): ").strip() - if new_path: - try: - # 确保目录存在 - new_dir = os.path.dirname(new_path) - if new_dir and not os.path.exists(new_dir): - os.makedirs(new_dir, exist_ok=True) - print(f"✓ 已创建目录: {new_dir}") - - csv_file_path = new_path - print(f"✓ CSV文件路径已更新为: {os.path.abspath(csv_file_path)}") - - # 检查文件是否存在 - if os.path.exists(csv_file_path): - file_size = os.path.getsize(csv_file_path) - print(f"文件已存在,大小: {file_size} 字节") - else: - print("文件不存在,将在首次导出时创建") - except Exception as e: - print(f"✗ 设置路径失败: {e}") - else: - print("路径不能为空") - - elif command == "view": - print("查看CSV文件内容...") - view_csv_content(csv_file_path) - - elif command == "clear": - clear_screen() - print_header() - print_commands() - - elif command == "help": - print_commands() - - elif command == "quit" or command == "exit": - print("正在退出...") - # 停止导出 - try: - device.stop_csv_export() - print("✓ 导出已停止") - except: - pass - print("程序已退出") - break - - elif command == "": - # 空命令,不做任何操作 - continue - - else: - print(f"未知命令: {command}") - print("输入 'help' 查看可用命令") - - except KeyboardInterrupt: - print("\n\n检测到 Ctrl+C,正在退出...") - try: - device.stop_csv_export() - print("✓ 导出已停止") - except: - pass - print("程序已退出") - break - except Exception as e: - print(f"执行命令时出错: {e}") - -if __name__ == '__main__': - interactive_demo() \ No newline at end of file diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index 09690f1..15d1bd5 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -178,6 +178,38 @@ bioyond_cell: title: create_orders参数 type: object type: UniLabJsonCommand + auto-create_orders_v2: + feedback: {} + goal: {} + goal_default: + xlsx_path: null + handles: + output: + - data_key: total_orders + data_source: executor + data_type: integer + handler_key: bottle_count + io_type: sink + label: 配液瓶数 + placeholder_keys: {} + result: {} + schema: + description: 从Excel解析并创建实验(V2版本) + properties: + feedback: {} + goal: + properties: + xlsx_path: + type: string + required: + - xlsx_path + type: object + result: {} + required: + - goal + title: create_orders_v2参数 + type: object + type: UniLabJsonCommand auto-create_sample: feedback: {} goal: {} @@ -594,6 +626,47 @@ bioyond_cell: title: transfer_1_to_2参数 type: object type: UniLabJsonCommand + auto-transfer_3_to_2: + feedback: {} + goal: {} + goal_default: + source_wh_id: 3a19debc-84b4-0359-e2d4-b3beea49348b + source_x: 1 + source_y: 1 + source_z: 1 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 3-2 物料转运,从3号位置转运到2号位置 + properties: + feedback: {} + goal: + properties: + source_wh_id: + default: 3a19debc-84b4-0359-e2d4-b3beea49348b + description: 来源仓库ID + type: string + source_x: + default: 1 + description: 来源位置X坐标 + type: integer + source_y: + default: 1 + description: 来源位置Y坐标 + type: integer + source_z: + default: 1 + description: 来源位置Z坐标 + type: integer + required: [] + type: object + result: {} + required: + - goal + title: transfer_3_to_2参数 + type: object + type: UniLabJsonCommand auto-transfer_3_to_2_to_1: feedback: {} goal: {} diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index e2cb91a..426dbca 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -194,7 +194,7 @@ coincellassemblyworkstation_device: type: string fujipian_juzhendianwei: default: 0 - description: 负极片矩阵点位 + description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) type: integer fujipian_panshu: default: 0 @@ -202,7 +202,7 @@ coincellassemblyworkstation_device: type: integer gemo_juzhendianwei: default: 0 - description: 隔膜矩阵点位 + description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) type: integer gemopanshu: default: 0 @@ -214,7 +214,7 @@ coincellassemblyworkstation_device: type: boolean qiangtou_juzhendianwei: default: 0 - description: 枪头盒矩阵点位 + description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) type: integer required: - elec_num @@ -493,6 +493,125 @@ coincellassemblyworkstation_device: title: func_read_data_and_output参数 type: object type: UniLabJsonCommand + auto-func_sendbottle_allpack_multi: + feedback: {} + goal: {} + goal_default: + assembly_pressure: 4200 + assembly_type: 7 + battery_clean_ignore: false + battery_pressure_mode: true + dual_drop_first_volume: 25 + dual_drop_mode: false + dual_drop_start_timing: false + dual_drop_suction_timing: false + elec_num: null + elec_use_num: null + elec_vol: 50 + file_path: /Users/sml/work + fujipian_juzhendianwei: 0 + fujipian_panshu: 0 + gemo_juzhendianwei: 0 + gemopanshu: 0 + lvbodian: true + qiangtou_juzhendianwei: 0 + handles: + input: + - data_key: elec_num + data_source: workflow + data_type: integer + handler_key: bottle_count + io_type: source + label: 配液瓶数 + required: true + placeholder_keys: {} + result: {} + schema: + description: 发送瓶数+简化组装函数(适用于第二批次及后续批次),合并了发送瓶数和简化组装流程 + properties: + feedback: {} + goal: + properties: + assembly_pressure: + default: 4200 + description: 电池压制力(N) + type: integer + assembly_type: + default: 7 + description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫) + type: integer + battery_clean_ignore: + default: false + description: 是否忽略电池清洁步骤 + type: boolean + battery_pressure_mode: + default: true + description: 是否启用压力模式 + type: boolean + dual_drop_first_volume: + default: 25 + description: 二次滴液第一次排液体积(μL) + type: integer + dual_drop_mode: + default: false + description: 电解液添加模式(false=单次滴液, true=二次滴液) + type: boolean + dual_drop_start_timing: + default: false + description: 二次滴液开始滴液时机(false=正极片前, true=正极片后) + type: boolean + dual_drop_suction_timing: + default: false + description: 二次滴液吸液时机(false=正常吸液, true=先吸液) + type: boolean + elec_num: + description: 电解液瓶数,如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数 + type: string + elec_use_num: + description: 每瓶电解液组装电池数 + type: string + elec_vol: + default: 50 + description: 电解液吸液量(μL) + type: integer + file_path: + default: /Users/sml/work + description: 实验记录保存路径 + type: string + fujipian_juzhendianwei: + default: 0 + description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + type: integer + fujipian_panshu: + default: 0 + description: 负极片盘数 + type: integer + gemo_juzhendianwei: + default: 0 + description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + type: integer + gemopanshu: + default: 0 + description: 隔膜盘数 + type: integer + lvbodian: + default: true + description: 是否使用铝箔垫片 + type: boolean + qiangtou_juzhendianwei: + default: 0 + description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + type: integer + required: + - elec_num + - elec_use_num + type: object + result: {} + required: + - goal + title: func_sendbottle_allpack_multi参数 + type: object + type: UniLabJsonCommand auto-func_stop_read_data: feedback: {} goal: {} diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 77ac533..e69de29 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -1,5794 +0,0 @@ -virtual_centrifuge: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - centrifuge: - feedback: - current_speed: current_speed - current_status: status - current_temp: current_temp - progress: progress - goal: - speed: speed - temp: temp - time: time - vessel: vessel - goal_default: - speed: 0.0 - temp: 0.0 - time: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_speed: - type: number - current_status: - type: string - current_temp: - type: number - progress: - type: number - required: - - progress - - current_speed - - current_temp - - current_status - title: Centrifuge_Feedback - type: object - goal: - properties: - speed: - type: number - temp: - type: number - time: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - speed - - time - - temp - title: Centrifuge_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Centrifuge_Result - type: object - required: - - goal - title: Centrifuge - type: object - type: Centrifuge - module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge - status_types: - centrifuge_state: str - current_speed: float - current_temp: float - max_speed: float - max_temp: float - message: str - min_temp: float - progress: float - status: str - target_speed: float - target_temp: float - time_remaining: float - type: python - config_info: [] - description: Virtual Centrifuge for CentrifugeProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: transport - description: 需要离心的样品容器 - handler_key: centrifuge - io_type: target - label: centrifuge - side: NORTH - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - centrifuge_state: - type: string - current_speed: - type: number - current_temp: - type: number - max_speed: - type: number - max_temp: - type: number - message: - type: string - min_temp: - type: number - progress: - type: number - status: - type: string - target_speed: - type: number - target_temp: - type: number - time_remaining: - type: number - required: - - status - - centrifuge_state - - current_speed - - target_speed - - current_temp - - target_temp - - max_speed - - max_temp - - min_temp - - time_remaining - - progress - - message - type: object - version: 1.0.0 -virtual_column: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - run_column: - feedback: - current_status: current_status - processed_volume: processed_volume - progress: progress - goal: - column: column - from_vessel: from_vessel - to_vessel: to_vessel - goal_default: - column: '' - from_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - pct1: '' - pct2: '' - ratio: '' - rf: '' - solvent1: '' - solvent2: '' - to_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: current_status - return_info: current_status - success: success - schema: - description: '' - properties: - feedback: - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: RunColumn_Feedback - type: object - goal: - properties: - column: - type: string - from_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: from_vessel - type: object - pct1: - type: string - pct2: - type: string - ratio: - type: string - rf: - type: string - solvent1: - type: string - solvent2: - type: string - to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: to_vessel - type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio - title: RunColumn_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: RunColumn_Result - type: object - required: - - goal - title: RunColumn - type: object - type: RunColumn - module: unilabos.devices.virtual.virtual_column:VirtualColumn - status_types: - column_diameter: float - column_length: float - column_state: str - current_flow_rate: float - current_phase: str - current_status: str - final_volume: float - max_flow_rate: float - processed_volume: float - progress: float - status: str - type: python - config_info: [] - description: Virtual Column Chromatography Device for RunColumn Protocol Testing - handles: - - data_key: from_vessel - data_source: handle - data_type: transport - description: 样品输入口 - handler_key: columnin - io_type: target - label: columnin - side: WEST - - data_key: to_vessel - data_source: handle - data_type: transport - description: 产物输出口 - handler_key: columnout - io_type: source - label: columnout - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - column_diameter: - type: number - column_length: - type: number - column_state: - type: string - current_flow_rate: - type: number - current_phase: - type: string - current_status: - type: string - final_volume: - type: number - max_flow_rate: - type: number - processed_volume: - type: number - progress: - type: number - status: - type: string - required: - - status - - column_state - - current_flow_rate - - max_flow_rate - - column_length - - column_diameter - - processed_volume - - progress - - current_status - - current_phase - - final_volume - type: object - version: 1.0.0 -virtual_filter: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - filter: - feedback: - current_status: current_status - current_temp: current_temp - filtered_volume: filtered_volume - progress: progress - goal: - continue_heatchill: continue_heatchill - filtrate_vessel: filtrate_vessel - stir: stir - stir_speed: stir_speed - temp: temp - vessel: vessel - volume: volume - goal_default: - continue_heatchill: false - filtrate_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - volume: 0.0 - handles: {} - result: - message: message - return_info: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - current_temp: - type: number - filtered_volume: - type: number - progress: - type: number - required: - - progress - - current_temp - - filtered_volume - - current_status - title: Filter_Feedback - type: object - goal: - properties: - continue_heatchill: - type: boolean - filtrate_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: filtrate_vessel - type: object - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - volume: - type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume - title: Filter_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Filter_Result - type: object - required: - - goal - title: Filter - type: object - type: Filter - module: unilabos.devices.virtual.virtual_filter:VirtualFilter - status_types: - current_status: str - current_temp: float - filtered_volume: float - max_stir_speed: float - max_temp: float - max_volume: float - message: str - progress: float - status: str - type: python - config_info: [] - description: Virtual Filter for FilterProtocol Testing - handles: - - data_key: vessel_in - data_source: handle - data_type: transport - description: 需要过滤的样品容器 - handler_key: filterin - io_type: target - label: filter_in - side: NORTH - - data_key: filtrate_out - data_source: handle - data_type: transport - description: 滤液出口 - handler_key: filtrateout - io_type: source - label: filtrate_out - side: SOUTH - - data_key: retentate_out - data_source: handle - data_type: transport - description: 滤渣/固体出口 - handler_key: retentateout - io_type: source - label: retentate_out - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - current_status: - type: string - current_temp: - type: number - filtered_volume: - type: number - max_stir_speed: - type: number - max_temp: - type: number - max_volume: - type: number - message: - type: string - progress: - type: number - status: - type: string - required: - - status - - progress - - current_temp - - current_status - - filtered_volume - - message - - max_temp - - max_stir_speed - - max_volume - type: object - version: 1.0.0 -virtual_gas_source: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-is_open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_open的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_open参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - module: unilabos.devices.virtual.virtual_gas_source:VirtualGasSource - status_types: - status: str - type: python - config_info: [] - description: Virtual gas source - handles: - - data_key: fluid_out - data_source: executor - data_type: fluid - description: 气源出气口 - handler_key: gassource - io_type: source - label: gassource - side: SOUTH - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - status: - type: string - required: - - status - type: object - version: 1.0.0 -virtual_heatchill: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - heat_chill: - feedback: - status: status - goal: - purpose: purpose - stir: stir - stir_speed: stir_speed - temp: temp - time: time - vessel: vessel - goal_default: - pressure: '' - purpose: '' - reflux_solvent: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - temp_spec: '' - time: '' - time_spec: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChill_Feedback - type: object - goal: - properties: - pressure: - type: string - purpose: - type: string - reflux_solvent: - type: string - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - temp_spec: - type: string - time: - type: string - time_spec: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose - title: HeatChill_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: HeatChill_Result - type: object - required: - - goal - title: HeatChill - type: object - type: HeatChill - heat_chill_start: - feedback: - status: status - goal: - purpose: purpose - temp: temp - vessel: vessel - goal_default: - purpose: '' - temp: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChillStart_Feedback - type: object - goal: - properties: - purpose: - type: string - temp: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - temp - - purpose - title: HeatChillStart_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: HeatChillStart_Result - type: object - required: - - goal - title: HeatChillStart - type: object - type: HeatChillStart - heat_chill_stop: - feedback: - status: status - goal: - vessel: vessel - goal_default: - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChillStop_Feedback - type: object - goal: - properties: - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - title: HeatChillStop_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: HeatChillStop_Result - type: object - required: - - goal - title: HeatChillStop - type: object - type: HeatChillStop - module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill - status_types: - is_stirring: bool - max_stir_speed: float - max_temp: float - min_temp: float - operation_mode: str - progress: float - remaining_time: float - status: str - stir_speed: float - type: python - config_info: [] - description: Virtual HeatChill for HeatChillProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: mechanical - description: 加热/冷却器的物理连接口 - handler_key: heatchill - io_type: source - label: heatchill - side: NORTH - icon: Heater.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - is_stirring: - type: boolean - max_stir_speed: - type: number - max_temp: - type: number - min_temp: - type: number - operation_mode: - type: string - progress: - type: number - remaining_time: - type: number - status: - type: string - stir_speed: - type: number - required: - - status - - operation_mode - - is_stirring - - stir_speed - - remaining_time - - progress - - max_temp - - min_temp - - max_stir_speed - type: object - version: 1.0.0 -virtual_multiway_valve: - category: - - virtual_device - class: - action_value_mappings: - auto-close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: close的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: close参数 - type: object - type: UniLabJsonCommand - auto-is_at_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: is_at_port参数 - type: object - type: UniLabJsonCommand - auto-is_at_position: - feedback: {} - goal: {} - goal_default: - position: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_position的参数schema - properties: - feedback: {} - goal: - properties: - position: - type: integer - required: - - position - type: object - result: {} - required: - - goal - title: is_at_position参数 - type: object - type: UniLabJsonCommand - auto-is_at_pump_position: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_pump_position的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_at_pump_position参数 - type: object - type: UniLabJsonCommand - auto-open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: open参数 - type: object - type: UniLabJsonCommand - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommand - auto-set_to_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_to_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: set_to_port参数 - type: object - type: UniLabJsonCommand - auto-set_to_pump_position: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_to_pump_position的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: set_to_pump_position参数 - type: object - type: UniLabJsonCommand - auto-switch_between_pump_and_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: switch_between_pump_and_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: switch_between_pump_and_port参数 - type: object - type: UniLabJsonCommand - set_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - set_valve_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve - status_types: - current_port: str - current_position: int - flow_path: str - status: str - target_position: int - valve_position: int - valve_state: str - type: python - config_info: [] - description: Virtual 8-Way Valve for flow direction control - handles: - - data_key: fluid_in - data_source: handle - data_type: fluid - description: 八通阀门进液口 - handler_key: transferpump - io_type: target - label: transferpump - side: NORTH - - data_key: fluid_port_1 - data_source: executor - data_type: fluid - description: 八通阀门端口1 - handler_key: '1' - io_type: source - label: '1' - side: NORTH - - data_key: fluid_port_2 - data_source: executor - data_type: fluid - description: 八通阀门端口2 - handler_key: '2' - io_type: source - label: '2' - side: EAST - - data_key: fluid_port_3 - data_source: executor - data_type: fluid - description: 八通阀门端口3 - handler_key: '3' - io_type: source - label: '3' - side: EAST - - data_key: fluid_port_4 - data_source: executor - data_type: fluid - description: 八通阀门端口4 - handler_key: '4' - io_type: source - label: '4' - side: SOUTH - - data_key: fluid_port_5 - data_source: executor - data_type: fluid - description: 八通阀门端口5 - handler_key: '5' - io_type: source - label: '5' - side: SOUTH - - data_key: fluid_port_6 - data_source: executor - data_type: fluid - description: 八通阀门端口6 - handler_key: '6' - io_type: source - label: '6' - side: WEST - - data_key: fluid_port_7 - data_source: executor - data_type: fluid - description: 八通阀门端口7 - handler_key: '7' - io_type: source - label: '7' - side: WEST - - data_key: fluid_port_8 - data_source: executor - data_type: fluid - description: 八通阀门端口8-特殊输入 - handler_key: '8' - io_type: target - label: '8' - side: WEST - - data_key: fluid_port_8 - data_source: executor - data_type: fluid - description: 八通阀门端口8 - handler_key: '8' - io_type: source - label: '8' - side: NORTH - icon: EightPipeline.webp - init_param_schema: - config: - properties: - port: - default: VIRTUAL - type: string - positions: - default: 8 - type: integer - required: [] - type: object - data: - properties: - current_port: - type: string - current_position: - type: integer - flow_path: - type: string - status: - type: string - target_position: - type: integer - valve_position: - type: integer - valve_state: - type: string - required: - - status - - valve_state - - current_position - - target_position - - current_port - - valve_position - - flow_path - type: object - version: 1.0.0 -virtual_rotavap: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - evaporate: - feedback: - current_device: current_device - status: status - goal: - pressure: pressure - stir_speed: stir_speed - temp: temp - time: time - vessel: vessel - goal_default: - pressure: 0.0 - solvent: '' - stir_speed: 0.0 - temp: 0.0 - time: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_device: - type: string - status: - type: string - time_remaining: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: time_remaining - type: object - time_spent: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: time_spent - type: object - required: - - status - - current_device - - time_spent - - time_remaining - title: Evaporate_Feedback - type: object - goal: - properties: - pressure: - type: number - solvent: - type: string - stir_speed: - type: number - temp: - type: number - time: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent - title: Evaporate_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: Evaporate_Result - type: object - required: - - goal - title: Evaporate - type: object - type: Evaporate - module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap - status_types: - current_temp: float - evaporated_volume: float - max_rotation_speed: float - max_temp: float - message: str - progress: float - remaining_time: float - rotation_speed: float - rotavap_state: str - status: str - vacuum_pressure: float - type: python - config_info: [] - description: Virtual Rotary Evaporator for EvaporateProtocol Testing - handles: - - data_key: vessel_in - data_source: handle - data_type: fluid - description: 样品连接口 - handler_key: samplein - io_type: target - label: sample_in - side: NORTH - - data_key: product_out - data_source: handle - data_type: fluid - description: 浓缩产物出口 - handler_key: productout - io_type: source - label: product_out - side: SOUTH - - data_key: solvent_out - data_source: handle - data_type: fluid - description: 冷凝溶剂出口 - handler_key: solventout - io_type: source - label: solvent_out - side: EAST - icon: Rotaryevaporator.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - current_temp: - type: number - evaporated_volume: - type: number - max_rotation_speed: - type: number - max_temp: - type: number - message: - type: string - progress: - type: number - remaining_time: - type: number - rotation_speed: - type: number - rotavap_state: - type: string - status: - type: string - vacuum_pressure: - type: number - required: - - status - - rotavap_state - - current_temp - - rotation_speed - - vacuum_pressure - - evaporated_volume - - progress - - message - - max_temp - - max_rotation_speed - - remaining_time - type: object - version: 1.0.0 -virtual_separator: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - separate: - feedback: - current_status: status - progress: progress - goal: - from_vessel: from_vessel - product_phase: product_phase - purpose: purpose - repeats: repeats - separation_vessel: separation_vessel - settling_time: settling_time - solvent: solvent - solvent_volume: solvent_volume - stir_speed: stir_speed - stir_time: stir_time - through: through - to_vessel: to_vessel - waste_phase_to_vessel: waste_phase_to_vessel - goal_default: - from_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - product_phase: '' - product_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - purpose: '' - repeats: 0 - separation_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - settling_time: 0.0 - solvent: '' - solvent_volume: '' - stir_speed: 0.0 - stir_time: 0.0 - through: '' - to_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - volume: '' - waste_phase_to_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - waste_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: Separate_Feedback - type: object - goal: - properties: - from_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: from_vessel - type: object - product_phase: - type: string - product_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: product_vessel - type: object - purpose: - type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - separation_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: separation_vessel - type: object - settling_time: - type: number - solvent: - type: string - solvent_volume: - type: string - stir_speed: - type: number - stir_time: - type: number - through: - type: string - to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: to_vessel - type: object - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - volume: - type: string - waste_phase_to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: waste_phase_to_vessel - type: object - waste_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: waste_vessel - type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time - title: Separate_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Separate_Result - type: object - required: - - goal - title: Separate - type: object - type: Separate - module: unilabos.devices.virtual.virtual_separator:VirtualSeparator - status_types: - has_phases: bool - message: str - phase_separation: bool - progress: float - separator_state: str - settling_time: float - status: str - stir_speed: float - volume: float - type: python - config_info: [] - description: Virtual Separator for SeparateProtocol Testing - handles: - - data_key: from_vessel - data_source: handle - data_type: fluid - description: 需要分离的混合液体输入口 - handler_key: separatorin - io_type: target - label: separator_in - side: NORTH - - data_key: bottom_outlet - data_source: executor - data_type: fluid - description: 下相(重相)液体输出口 - handler_key: bottomphaseout - io_type: source - label: bottom_phase_out - side: SOUTH - - data_key: mechanical_port - data_source: handle - data_type: mechanical - description: 用于连接搅拌器等机械设备的接口 - handler_key: bind - io_type: target - label: bind - side: WEST - icon: Separator.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - has_phases: - type: boolean - message: - type: string - phase_separation: - type: boolean - progress: - type: number - separator_state: - type: string - settling_time: - type: number - status: - type: string - stir_speed: - type: number - volume: - type: number - required: - - status - - separator_state - - volume - - has_phases - - phase_separation - - stir_speed - - settling_time - - progress - - message - type: object - version: 1.0.0 -virtual_solenoid_valve: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommandAsync - auto-toggle: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: toggle的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: toggle参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: - command: CLOSED - goal_default: {} - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - set_valve_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - message: message - success: success - valve_position: valve_position - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve - status_types: - is_open: bool - status: str - valve_position: str - valve_state: str - type: python - config_info: [] - description: Virtual Solenoid Valve for simple on/off flow control - handles: - - data_key: fluid_port_in - data_source: handle - data_type: fluid - description: 电磁阀的进液口 - handler_key: in - io_type: target - label: in - side: NORTH - - data_key: fluid_port_out - data_source: handle - data_type: fluid - description: 电磁阀的出液口 - handler_key: out - io_type: source - label: out - side: SOUTH - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - is_open: - type: boolean - status: - type: string - valve_position: - type: string - valve_state: - type: string - required: - - status - - valve_state - - is_open - - valve_position - type: object - version: 1.0.0 -virtual_solid_dispenser: - category: - - virtual_device - class: - action_value_mappings: - add_solid: - feedback: - current_status: status - progress: progress - goal: - equiv: equiv - event: event - mass: mass - mol: mol - purpose: purpose - rate_spec: rate_spec - ratio: ratio - reagent: reagent - vessel: vessel - goal_default: - amount: '' - equiv: '' - event: '' - mass: '' - mol: '' - purpose: '' - rate_spec: '' - ratio: '' - reagent: '' - stir: false - stir_speed: 0.0 - time: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - viscous: false - volume: '' - handles: {} - result: - message: message - return_info: return_info - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - required: - - progress - - current_status - title: Add_Feedback - type: object - goal: - properties: - amount: - type: string - equiv: - type: string - event: - type: string - mass: - type: string - mol: - type: string - purpose: - type: string - rate_spec: - type: string - ratio: - type: string - reagent: - type: string - stir: - type: boolean - stir_speed: - type: number - time: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - viscous: - type: boolean - volume: - type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio - title: Add_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Add_Result - type: object - required: - - goal - title: Add - type: object - type: Add - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-find_solid_reagent_bottle: - feedback: {} - goal: {} - goal_default: - reagent_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - reagent_name: - type: string - required: - - reagent_name - type: object - result: {} - required: - - goal - title: find_solid_reagent_bottle参数 - type: object - type: UniLabJsonCommand - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-parse_mass_string: - feedback: {} - goal: {} - goal_default: - mass_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mass_str: - type: string - required: - - mass_str - type: object - result: {} - required: - - goal - title: parse_mass_string参数 - type: object - type: UniLabJsonCommand - auto-parse_mol_string: - feedback: {} - goal: {} - goal_default: - mol_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mol_str: - type: string - required: - - mol_str - type: object - result: {} - required: - - goal - title: parse_mol_string参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser - status_types: - current_reagent: str - dispensed_amount: float - status: str - total_operations: int - type: python - config_info: [] - description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and - molar additions - handles: - - data_key: solid_out - data_source: executor - data_type: resource - description: 固体试剂输出口 - handler_key: SolidOut - io_type: source - label: SolidOut - side: SOUTH - - data_key: solid_in - data_source: handle - data_type: resource - description: 固体试剂输入口(连接试剂瓶) - handler_key: SolidIn - io_type: target - label: SolidIn - side: NORTH - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_reagent: - type: string - dispensed_amount: - type: number - status: - type: string - total_operations: - type: integer - required: - - status - - current_reagent - - dispensed_amount - - total_operations - type: object - version: 1.0.0 -virtual_stirrer: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - start_stir: - feedback: - status: status - goal: - purpose: purpose - stir_speed: stir_speed - vessel: vessel - goal_default: - purpose: '' - stir_speed: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - current_speed: - type: number - current_status: - type: string - progress: - type: number - required: - - progress - - current_speed - - current_status - title: StartStir_Feedback - type: object - goal: - properties: - purpose: - type: string - stir_speed: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - stir_speed - - purpose - title: StartStir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: StartStir_Result - type: object - required: - - goal - title: StartStir - type: object - type: StartStir - stir: - feedback: - status: status - goal: - settling_time: settling_time - stir_speed: stir_speed - stir_time: stir_time - goal_default: - event: '' - settling_time: '' - stir_speed: 0.0 - stir_time: 0.0 - time: '' - time_spec: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: Stir_Feedback - type: object - goal: - properties: - event: - type: string - settling_time: - type: string - stir_speed: - type: number - stir_time: - type: number - time: - type: string - time_spec: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time - title: Stir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Stir_Result - type: object - required: - - goal - title: Stir - type: object - type: Stir - stop_stir: - feedback: - status: status - goal: - vessel: vessel - goal_default: - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - required: - - progress - - current_status - title: StopStir_Feedback - type: object - goal: - properties: - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - title: StopStir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: StopStir_Result - type: object - required: - - goal - title: StopStir - type: object - type: StopStir - module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer - status_types: - current_speed: float - current_vessel: str - device_info: dict - is_stirring: bool - max_speed: float - min_speed: float - operation_mode: str - remaining_time: float - status: str - type: python - config_info: [] - description: Virtual Stirrer for StirProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: mechanical - description: 搅拌器的机械连接口 - handler_key: stirrer - io_type: source - label: stirrer - side: NORTH - icon: Stirrer.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_speed: - type: number - current_vessel: - type: string - device_info: - type: object - is_stirring: - type: boolean - max_speed: - type: number - min_speed: - type: number - operation_mode: - type: string - remaining_time: - type: number - status: - type: string - required: - - status - - operation_mode - - current_vessel - - current_speed - - is_stirring - - remaining_time - - max_speed - - min_speed - - device_info - type: object - version: 1.0.0 -virtual_transfer_pump: - category: - - virtual_device - class: - action_value_mappings: - auto-aspirate: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: aspirate的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: aspirate参数 - type: object - type: UniLabJsonCommandAsync - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-dispense: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: dispense的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: dispense参数 - type: object - type: UniLabJsonCommandAsync - auto-empty_syringe: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: empty_syringe的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: [] - type: object - result: {} - required: - - goal - title: empty_syringe参数 - type: object - type: UniLabJsonCommandAsync - auto-fill_syringe: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: fill_syringe的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: [] - type: object - result: {} - required: - - goal - title: fill_syringe参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_empty: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_empty的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_empty参数 - type: object - type: UniLabJsonCommand - auto-is_full: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_full的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_full参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-pull_plunger: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: pull_plunger的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: pull_plunger参数 - type: object - type: UniLabJsonCommandAsync - auto-push_plunger: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: push_plunger的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: push_plunger参数 - type: object - type: UniLabJsonCommandAsync - auto-set_max_velocity: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_max_velocity的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: - - velocity - type: object - result: {} - required: - - goal - title: set_max_velocity参数 - type: object - type: UniLabJsonCommand - auto-stop_operation: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: stop_operation的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: stop_operation参数 - type: object - type: UniLabJsonCommandAsync - set_position: - feedback: - current_position: current_position - progress: progress - status: status - goal: - max_velocity: max_velocity - position: position - goal_default: - max_velocity: 0.0 - position: 0.0 - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_position: - type: number - progress: - type: number - status: - type: string - required: - - status - - current_position - - progress - title: SetPumpPosition_Feedback - type: object - goal: - properties: - max_velocity: - type: number - position: - type: number - required: - - position - - max_velocity - title: SetPumpPosition_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - - message - title: SetPumpPosition_Result - type: object - required: - - goal - title: SetPumpPosition - type: object - type: SetPumpPosition - transfer: - feedback: - current_status: current_status - progress: progress - transferred_volume: transferred_volume - goal: - amount: amount - from_vessel: from_vessel - rinsing_repeats: rinsing_repeats - rinsing_solvent: rinsing_solvent - rinsing_volume: rinsing_volume - solid: solid - time: time - to_vessel: to_vessel - viscous: viscous - volume: volume - goal_default: - amount: '' - from_vessel: '' - rinsing_repeats: 0 - rinsing_solvent: '' - rinsing_volume: 0.0 - solid: false - time: 0.0 - to_vessel: '' - viscous: false - volume: 0.0 - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - transferred_volume: - type: number - required: - - progress - - transferred_volume - - current_status - title: Transfer_Feedback - type: object - goal: - properties: - amount: - type: string - from_vessel: - type: string - rinsing_repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - rinsing_solvent: - type: string - rinsing_volume: - type: number - solid: - type: boolean - time: - type: number - to_vessel: - type: string - viscous: - type: boolean - volume: - type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid - title: Transfer_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Transfer_Result - type: object - required: - - goal - title: Transfer - type: object - type: Transfer - module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump - status_types: - current_volume: float - max_velocity: float - position: float - remaining_capacity: float - status: str - transfer_rate: float - type: python - config_info: [] - description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) - handles: - - data_key: fluid_port - data_source: handle - data_type: fluid - description: 注射器式转移泵的连接口 - handler_key: transferpump - io_type: source - label: transferpump - side: SOUTH - icon: Pump.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_volume: - type: number - max_velocity: - type: number - position: - type: number - remaining_capacity: - type: number - status: - type: string - transfer_rate: - type: number - required: - - status - - position - - current_volume - - max_velocity - - transfer_rate - - remaining_capacity - type: object - version: 1.0.0 -virtual_vacuum_pump: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-is_open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_open的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_open参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - module: unilabos.devices.virtual.virtual_vacuum_pump:VirtualVacuumPump - status_types: - status: str - type: python - config_info: [] - description: Virtual vacuum pump - handles: - - data_key: fluid_in - data_source: handle - data_type: fluid - description: 真空泵进气口 - handler_key: vacuumpump - io_type: source - label: vacuumpump - side: SOUTH - icon: Vacuum.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - status: - type: string - required: - - status - type: object - version: 1.0.0 diff --git a/unilabos/resources/bioyond/README_WAREHOUSE.md b/unilabos/resources/bioyond/README_WAREHOUSE.md new file mode 100644 index 0000000..6d1a62e --- /dev/null +++ b/unilabos/resources/bioyond/README_WAREHOUSE.md @@ -0,0 +1,548 @@ +# Bioyond 仓库系统开发指南 + +本文档详细说明 Bioyond 仓库(Warehouse)系统的架构、配置和使用方法,帮助开发者快速理解和维护仓库相关代码。 + +## 📚 目录 + +- [系统架构](#系统架构) +- [核心概念](#核心概念) +- [三层映射关系](#三层映射关系) +- [warehouse_factory 详解](#warehouse_factory-详解) +- [创建新仓库](#创建新仓库) +- [常见问题](#常见问题) +- [调试技巧](#调试技巧) + +--- + +## 系统架构 + +Bioyond 仓库系统采用**三层架构**,实现从前端显示到后端 API 的完整映射: + +``` +┌─────────────────────────────────────────────────────────┐ +│ 前端显示层 (YB_warehouses.py) │ +│ - warehouse_factory 自动生成库位网格 │ +│ - 生成库位名称:A01, B02, C03... │ +│ - 存储在 WareHouse.sites 字典中 │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Deck 布局层 (decks.py) │ +│ - 定义仓库在 Deck 上的物理位置 │ +│ - 组织多个仓库形成完整布局 │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ UUID 映射层 (config.py) │ +│ - 将库位名称映射到 Bioyond 系统 UUID │ +│ - 用于 API 调用时的物料入库操作 │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心概念 + +### 仓库(Warehouse) + +仓库是一个**三维网格**,用于存放物料。由以下参数定义: + +- **num_items_x**: 列数(X 轴) +- **num_items_y**: 行数(Y 轴) +- **num_items_z**: 层数(Z 轴) + +例如:`5行×3列×1层` = 5×3×1 = 15个库位 + +### 库位(Site) + +库位是仓库中的单个存储位置,由**字母行+数字列**命名: + +- **字母行**:A, B, C, D, E, F...(对应 Y 轴) +- **数字列**:01, 02, 03, 04...(对应 X 轴或 Z 轴) + +示例:`A01`, `B02`, `C03` + +### 布局模式(Layout) + +控制库位的排序和 Y 坐标计算: + +| 模式 | 说明 | 生成顺序 | Y 坐标计算 | 显示效果 | +|------|------|----------|-----------|---------| +| `col-major` | 列优先(默认) | A01, B01, C01, A02... | `dy + (num_y - row - 1) * item_dy` | A 可能在下 | +| `row-major` | 行优先 | A01, A02, A03, B01... | `dy + row * item_dy` | **A 在上** ✓ | + +**重要:** 使用 `row-major` 可以避免上下颠倒问题! + +--- + +## 三层映射关系 + +### 示例:手动传递窗右(A01-E03) + +#### 1️⃣ 前端显示层 - [`YB_warehouses.py`](YB_warehouses.py) + +```python +def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse: + """创建 5行×3列×1层 仓库""" + return warehouse_factory( + name=name, + num_items_x=3, # 3列 + num_items_y=5, # 5行 + num_items_z=1, # 1层 + row_offset=row_offset, + layout="row-major", + ) +``` + +**自动生成的库位:** A01, A02, A03, B01, B02, B03, ..., E01, E02, E03 + +#### 2️⃣ Deck 布局层 - [`decks.py`](decks.py) + +```python +self.warehouses = { + "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), +} +self.warehouse_locations = { + "手动传递窗右": Coordinate(4160.0, 877.0, 0.0), +} +``` + +**作用:** +- 创建仓库实例 +- 设置在 Deck 上的物理坐标 + +#### 3️⃣ UUID 映射层 - [`config.py`](../../devices/workstation/bioyond_studio/config.py) + +```python +WAREHOUSE_MAPPING = { + "手动传递窗右": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + # ... 其他库位 + } + } +} +``` + +**作用:** +- 用户拖拽物料到"手动传递窗右"的"A01"位置时 +- 系统查找 `WAREHOUSE_MAPPING["手动传递窗右"]["site_uuids"]["A01"]` +- 获取 UUID `"3a19deae-2c7a-36f5-5e41-02c5b66feaea"` +- 调用 Bioyond API 将物料入库到该 UUID 位置 + +--- + +## 实际配置案例 + +### 案例:手动传递窗左/右的完整配置 + +本案例展示如何为"手动传递窗右"和"手动传递窗左"建立完整的三层映射。 + +#### 背景需求 +- **手动传递窗右**: 需要 A01-E03(5行×3列=15个库位) +- **手动传递窗左**: 需要 F01-J03(5行×3列=15个库位) +- 这两个仓库共享同一个物理堆栈的 UUID("手动堆栈") + +#### 实施步骤 + +**1️⃣ 修复前端布局** - [`YB_warehouses.py`](YB_warehouses.py) + +```python +# 创建新的 5×3×1 仓库函数(之前是错误的 1×3×3) +def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse: + """创建5行×3列×1层仓库,支持行偏移生成不同字母行""" + return warehouse_factory( + name=name, + num_items_x=3, # 3列 + num_items_y=5, # 5行 ← 修正 + num_items_z=1, # 1层 ← 修正 + row_offset=row_offset, # ← 支持 F-J 行 + layout="row-major", # ← 避免上下颠倒 + ) +``` + +**2️⃣ 更新 Deck 配置** - [`decks.py`](decks.py) + +```python +from unilabos.resources.bioyond.YB_warehouses import ( + bioyond_warehouse_5x3x1, # 新增导入 +) + +class BIOYOND_YB_Deck(Deck): + def setup(self) -> None: + self.warehouses = { + # 修改前: bioyond_warehouse_1x3x3 (错误尺寸) + # 修改后: bioyond_warehouse_5x3x1 (正确尺寸) + "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03 + "手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03 + } +``` + +**3️⃣ 添加 UUID 映射** - [`config.py`](../../devices/workstation/bioyond_studio/config.py) + +```python +WAREHOUSE_MAPPING = { + # 保持原有的"手动堆栈"配置不变(A01-J03共30个库位) + "手动堆栈": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + # ... A02-E03 共15个 + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", + # ... F02-J03 共15个 + } + }, + + # [新增] 手动传递窗右 - 复用"手动堆栈"的 A01-E03 UUID + "手动传递窗右": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", # ← 与手动堆栈A01相同 + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + "A03": "3a19deae-2c7a-5876-c454-6b7e224ca927", + "B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a", + "C01": "3a19deae-2c7a-32bc-768e-556647e292f3", + "C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4", + "C03": "3a19deae-2c7a-3056-6504-10dc73fbc276", + "D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440", + "D02": "3a19deae-2c7a-61be-601c-b6fb5610499a", + "D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560", + "E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363", + "E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910", + "E03": "3a19deae-2c7a-b163-2219-23df15200311", + } + }, + + # [新增] 手动传递窗左 - 复用"手动堆栈"的 F01-J03 UUID + "手动传递窗左": { + "uuid": "", + "site_uuids": { + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", # ← 与手动堆栈F01相同 + "F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679", + "F03": "3a19deae-2c7a-f7c4-12bd-425799425698", + "G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955", + "G02": "3a19deae-2c7a-204e-95ed-1f1950f28343", + "G03": "3a19deae-2c7a-392b-62f1-4907c66343f8", + "H01": "3a19deae-2c7a-5602-e876-d27aca4e3201", + "H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702", + "H03": "3a19deae-2c7a-780b-8965-2e1345f7e834", + "I01": "3a19deae-2c7a-8849-e172-07de14ede928", + "I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0", + "I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4", + "J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6", + "J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205", + "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" + } + }, +} +``` + +#### 关键要点 + +1. **UUID 可以复用**: 三个仓库(手动堆栈、手动传递窗右、手动传递窗左)可以共享相同的物理库位 UUID +2. **库位名称必须匹配**: 前端生成的库位名称(如 F01)必须与 config.py 中的键名完全一致 +3. **row_offset 的妙用**: + - `row_offset=0` → 生成 A-E 行 + - `row_offset=5` → 生成 F-J 行(跳过前5个字母) + +#### 验证结果 + +配置完成后,拖拽测试: + +| 拖拽位置 | 前端库位 | 查找路径 | UUID | 结果 | +|---------|---------|---------|------|------| +| 手动传递窗右/A01 | A01 | `WAREHOUSE_MAPPING["手动传递窗右"]["site_uuids"]["A01"]` | `3a19...eaea` | ✅ 正确入库 | +| 手动传递窗左/F01 | F01 | `WAREHOUSE_MAPPING["手动传递窗左"]["site_uuids"]["F01"]` | `3a19...c4a` | ✅ 正确入库 | +| 手动堆栈/A01 | A01 | `WAREHOUSE_MAPPING["手动堆栈"]["site_uuids"]["A01"]` | `3a19...eaea` | ✅ 仍然正常 | + + +--- + +## warehouse_factory 详解 + +### 函数签名 + +```python +def warehouse_factory( + name: str, + num_items_x: int = 1, # 列数 + num_items_y: int = 4, # 行数 + num_items_z: int = 4, # 层数 + dx: float = 137.0, # X 起始偏移 + dy: float = 96.0, # Y 起始偏移 + dz: float = 120.0, # Z 起始偏移 + item_dx: float = 10.0, # X 间距 + item_dy: float = 10.0, # Y 间距 + item_dz: float = 10.0, # Z 间距 + col_offset: int = 0, # 列偏移(影响数字) + row_offset: int = 0, # 行偏移(影响字母) + layout: str = "col-major", # 布局模式 +) -> WareHouse: +``` + +### 参数说明 + +#### 尺寸参数 +- **num_items_x, y, z**: 定义仓库的网格尺寸 +- **注意**: 当 `num_items_z > 1` 时,Z 轴会被映射为数字列 + +#### 位置参数 +- **dx, dy, dz**: 第一个库位的起始坐标 +- **item_dx, dy, dz**: 库位之间的间距 + +#### 偏移参数 +- **col_offset**: 列起始偏移,用于生成 A05-D08 等命名 + ```python + col_offset=4 # 生成 A05, A06, A07, A08 + ``` + +- **row_offset**: 行起始偏移,用于生成 F01-J03 等命名 + ```python + row_offset=5 # 生成 F01, F02, F03(跳过 A-E) + ``` + +#### 布局参数 +- **layout**: + - `"col-major"`: 列优先(默认),可能导致上下颠倒 + - `"row-major"`: 行优先,**推荐使用**,A 显示在上 + +### 库位生成逻辑 + +```python +# row-major 模式(推荐) +keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" + for j in range(num_y) + for i in range(num_x)] + +# 示例:num_y=2, num_x=3, row_offset=0, col_offset=0 +# 生成:A01, A02, A03, B01, B02, B03 +``` + +### Y 坐标计算 + +```python +if layout == "row-major": + # A 在上(Y 较小) + y = dy + row * item_dy +else: + # A 在下(Y 较大)- 不推荐 + y = dy + (num_items_y - row - 1) * item_dy +``` + +--- + +## 创建新仓库 + +### 步骤 1: 在 YB_warehouses.py 中创建函数 + +```python +def bioyond_warehouse_3x4x1(name: str) -> WareHouse: + """创建 3行×4列×1层 仓库 + + 布局: + A01 | A02 | A03 | A04 + B01 | B02 | B03 | B04 + C01 | C02 | C03 | C04 + """ + return warehouse_factory( + name=name, + num_items_x=4, # 4列 + num_items_y=3, # 3行 + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=120.0, + item_dz=120.0, + category="warehouse", + layout="row-major", # ⭐ 推荐使用 + ) +``` + +### 步骤 2: 在 decks.py 中使用 + +```python +# 1. 导入函数 +from unilabos.resources.bioyond.YB_warehouses import ( + bioyond_warehouse_3x4x1, # 新增 +) + +# 2. 在 setup() 中添加 +self.warehouses = { + "我的新仓库": bioyond_warehouse_3x4x1("我的新仓库"), +} +self.warehouse_locations = { + "我的新仓库": Coordinate(100.0, 200.0, 0.0), +} +``` + +### 步骤 3: 在 config.py 中配置 UUID(可选) + +```python +WAREHOUSE_MAPPING = { + "我的新仓库": { + "uuid": "", + "site_uuids": { + "A01": "从 Bioyond 系统获取的 UUID", + "A02": "从 Bioyond 系统获取的 UUID", + # ... 其他 11 个库位 + } + } +} +``` + +**注意:** 如果不需要拖拽入库功能,可跳过此步骤。 + +--- + +## 常见问题 + +### Q1: 为什么库位显示上下颠倒(C 在上,A 在下)? + +**原因:** 使用了默认的 `col-major` 布局。 + +**解决:** 在 `warehouse_factory` 中添加 `layout="row-major"` + +```python +return warehouse_factory( + ... + layout="row-major", # ← 添加这行 +) +``` + +### Q2: 我需要 1×3×3 还是 3×3×1? + +**判断方法:** +- **1×3×3**: 1列×3行×3**层**(垂直堆叠,有高度) +- **3×3×1**: 3行×3列×1**层**(平面网格) + +**推荐:** 大多数情况使用 `X×Y×1`(平面网格)更直观。 + +### Q3: 如何生成 F01-J03 而非 A01-E03? + +**方法:** 使用 `row_offset` 参数 + +```python +bioyond_warehouse_5x3x1("仓库名", row_offset=5) +# row_offset=5 跳过 A-E,从 F 开始 +``` + +### Q4: 拖拽物料后找不到 UUID 怎么办? + +**检查清单:** +1. `config.py` 中是否有该仓库的配置? +2. 仓库名称是否完全匹配? +3. 库位名称(如 A01)是否在 `site_uuids` 中? + +**示例错误:** +```python +# decks.py +"手动传递窗右": bioyond_warehouse_5x3x1(...) + +# config.py - ❌ 名称不匹配 +"手动传递窗": { ... } # 缺少"右"字 +``` + +### Q5: 库位重叠怎么办? + +**原因:** 间距(`item_dx/dy/dz`)太小。 + +**解决:** 增大间距参数 + +```python +item_dx=150.0, # 增大 X 间距 +item_dy=130.0, # 增大 Y 间距 +``` + +--- + +## 调试技巧 + +### 1. 查看生成的库位 + +```python +warehouse = bioyond_warehouse_5x3x1("测试仓库") +print(list(warehouse.sites.keys())) +# 输出:['A01', 'A02', 'A03', 'B01', 'B02', ...] +``` + +### 2. 检查库位坐标 + +```python +for name, site in warehouse.sites.items(): + print(f"{name}: {site.location}") +# 输出: +# A01: Coordinate(x=10.0, y=10.0, z=120.0) +# A02: Coordinate(x=147.0, y=10.0, z=120.0) +# ... +``` + +### 3. 验证 UUID 映射 + +```python +from unilabos.devices.workstation.bioyond_studio.config import WAREHOUSE_MAPPING + +warehouse_name = "手动传递窗右" +location_code = "A01" + +if warehouse_name in WAREHOUSE_MAPPING: + uuid = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"].get(location_code) + print(f"{warehouse_name}/{location_code} → {uuid}") +else: + print(f"❌ 未找到仓库: {warehouse_name}") +``` + +--- + +## 文件关系图 + +``` +unilabos/ +├── resources/ +│ ├── warehouse.py # warehouse_factory 核心实现 +│ └── bioyond/ +│ ├── YB_warehouses.py # ⭐ 仓库函数定义 +│ ├── decks.py # ⭐ Deck 布局配置 +│ └── README_WAREHOUSE.md # 📖 本文档 +└── devices/ + └── workstation/ + └── bioyond_studio/ + ├── config.py # ⭐ UUID 映射配置 + └── bioyond_cell/ + └── bioyond_cell_workstation.py # 业务逻辑 +``` + +--- + +## 版本历史 + +- **v1.1** (2026-01-08): 补充实际配置案例 + - 添加"手动传递窗右"和"手动传递窗左"的完整配置示例 + - 展示 UUID 复用的实际应用 + - 说明三个仓库共享物理堆栈的配置方法 + +- **v1.0** (2026-01-07): 初始版本 + - 新增 `row_offset` 参数支持 + - 创建 `bioyond_warehouse_5x3x1` 和 `bioyond_warehouse_2x2x1` + - 修复多个仓库的上下颠倒问题 + +--- + +## 相关资源 + +- [warehouse.py](../warehouse.py) - 核心工厂函数实现 +- [YB_warehouses.py](YB_warehouses.py) - 所有仓库定义 +- [decks.py](decks.py) - Deck 布局配置 +- [config.py](../../devices/workstation/bioyond_studio/config.py) - UUID 映射 + +--- + +**维护者:** Uni-Lab-OS 开发团队 +**最后更新:** 2026-01-07 diff --git a/unilabos/resources/bioyond/YB_warehouses.py b/unilabos/resources/bioyond/YB_warehouses.py index ae9e473..d39c0e8 100644 --- a/unilabos/resources/bioyond/YB_warehouses.py +++ b/unilabos/resources/bioyond/YB_warehouses.py @@ -166,7 +166,14 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse: ) def bioyond_warehouse_1x2x2(name: str) -> WareHouse: - """创建BioYond 1x2x2仓库""" + """创建BioYond 1x2x2仓库(1列×2行×2层)- 旧版本,已弃用 + + 布局(2层): + 层1: A01 + B01 + 层2: A02 + B02 + """ return warehouse_factory( name=name, num_items_x=1, @@ -179,8 +186,32 @@ def bioyond_warehouse_1x2x2(name: str) -> WareHouse: item_dy=96.0, item_dz=120.0, category="warehouse", + layout="row-major", # 使用行优先避免上下颠倒 ) +def bioyond_warehouse_2x2x1(name: str) -> WareHouse: + """创建BioYond 2x2x1仓库(2行×2列×1层) + + 布局: + A01 | A02 + B01 | B02 + """ + return warehouse_factory( + name=name, + num_items_x=2, # 2列 + num_items_y=2, # 2行 + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + layout="row-major", # 使用行优先避免上下颠倒 + ) + + def bioyond_warehouse_10x1x1(name: str) -> WareHouse: """创建BioYond 10x1x1仓库""" return warehouse_factory( @@ -208,11 +239,61 @@ def bioyond_warehouse_1x3x3(name: str) -> WareHouse: dy=10.0, dz=10.0, item_dx=137.0, - item_dy=96.0, + item_dy=120.0, # 增大Y方向间距以避免重叠 item_dz=120.0, category="warehouse", ) +def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse: + """创建BioYond 5x3x1仓库(5行×3列×1层) + + 标准布局(row_offset=0): + A01 | A02 | A03 + B01 | B02 | B03 + C01 | C02 | C03 + D01 | D02 | D03 + E01 | E02 | E03 + + 带偏移布局(row_offset=5): + F01 | F02 | F03 + G01 | G02 | G03 + H01 | H02 | H03 + I01 | I02 | I03 + J01 | J02 | J03 + """ + return warehouse_factory( + name=name, + num_items_x=3, # 3列 + num_items_y=5, # 5行 + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=120.0, + item_dz=120.0, + category="warehouse", + col_offset=0, + row_offset=row_offset, # 支持行偏移 + layout="row-major", # 使用行优先避免颠倒 + ) + + +def bioyond_warehouse_3x3x1(name: str) -> WareHouse: + """创建BioYond 3x3x1仓库""" + return warehouse_factory( + name=name, + num_items_x=3, + num_items_y=3, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) def bioyond_warehouse_2x1x3(name: str) -> WareHouse: """创建BioYond 2x1x3仓库""" return warehouse_factory( diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 67e6d79..f56b2ba 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -8,7 +8,9 @@ from unilabos.resources.bioyond.YB_warehouses import ( bioyond_warehouse_reagent_stack, # 新增:试剂堆栈 (A1-B4) bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, + bioyond_warehouse_2x2x1, # 新增:321和43窗口 (2行×2列) bioyond_warehouse_1x3x3, + bioyond_warehouse_5x3x1, # 新增:手动传递窗仓库 (5行×3列) bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, @@ -115,10 +117,10 @@ class BIOYOND_YB_Deck(Deck): def setup(self) -> None: # 添加仓库 self.warehouses = { - "321窗口": bioyond_warehouse_1x2x2("321窗口"), - "43窗口": bioyond_warehouse_1x2x2("43窗口"), - "手动传递窗左": bioyond_warehouse_1x3x3("手动传递窗左"), - "手动传递窗右": bioyond_warehouse_1x3x3("手动传递窗右"), + "321窗口": bioyond_warehouse_2x2x1("321窗口"), # 2行×2列 + "43窗口": bioyond_warehouse_2x2x1("43窗口"), # 2行×2列 + "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03 + "手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03 "加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"), "加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"), diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 4dcda6d..3dbd6ad 100644 --- a/unilabos/resources/warehouse.py +++ b/unilabos/resources/warehouse.py @@ -27,6 +27,7 @@ def warehouse_factory( category: str = "warehouse", model: Optional[str] = None, col_offset: int = 0, # 列起始偏移量,用于生成A05-D08等命名 + row_offset: int = 0, # 行起始偏移量,用于生成F01-J03等命名 layout: str = "col-major", # 新增:排序方式,"col-major"=列优先,"row-major"=行优先 ): # 创建位置坐标 @@ -65,10 +66,10 @@ def warehouse_factory( if layout == "row-major": # 行优先顺序: A01,A02,A03,A04, B01,B02,B03,B04 # locations[0] 对应 row=0, y最大(前端顶部)→ 应该是 A01 - keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)] + keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)] else: # 列优先顺序: A01,B01,C01,D01, A02,B02,C02,D02 - keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)] + keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)] sites = {i: site for i, site in zip(keys, _sites.values())}