diff --git a/test/resources/test copy.json b/test/resources/test copy.json new file mode 100644 index 0000000..f9e9aa0 --- /dev/null +++ b/test/resources/test copy.json @@ -0,0 +1,99 @@ + { + "typeId": "3a190c8b-3284-af78-d29f-9a69463ad047", + "code": "", + "barCode": "", + "name": "test", + "unit": "", + "parameters": "{}", + "quantity": "", + "details": [ + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)11", + "quantity": "1", + "x": 1, + "y": 1, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)21", + "quantity": "1", + "x": 2, + "y": 1, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)12", + "quantity": "1", + "x": 1, + "y": 2, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)22", + "quantity": "1", + "x": 2, + "y": 2, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)13", + "quantity": "1", + "x": 1, + "y": 3, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)23", + "quantity": "1", + "x": 2, + "y": 3, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)14", + "quantity": "1", + "x": 1, + "y": 4, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)24", + "quantity": "1", + "x": 2, + "y": 4, + "z": 1, + "unit": "", + "parameters": "{}" + } + ] + } \ No newline at end of file diff --git a/test/resources/test.json b/test/resources/test.json new file mode 100644 index 0000000..ee1be0f --- /dev/null +++ b/test/resources/test.json @@ -0,0 +1,114 @@ +[ + { + "id": "3a1d4b7e-4bdc-16bf-7169-f60350d03c7e", + "typeName": "配液瓶(小)板", + "code": "0001-00088", + "barCode": "", + "name": "test1", + "quantity": 1.0, + "lockQuantity": 0.0, + "unit": "块", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "whid": "3a19deae-2c79-05a3-9c76-8e6760424841", + "whName": "手动堆栈", + "code": "4", + "x": 2, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [ + { + "id": "3a1d4b7e-4bdc-12e8-4d26-dddc77b03f63", + "detailMaterialId": "3a1d4b7e-4bdc-4e9e-8a3c-e9ba4a26457e", + "code": null, + "name": "test1", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 2, + "z": 1, + "associateId": null, + "typeName": "配液瓶(小)", + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb" + }, + { + "id": "3a1d4b7e-4bdc-35b6-22d4-e6f3235e1c27", + "detailMaterialId": "3a1d4b7e-4bdc-ce0f-1fbb-b88de76fce98", + "code": null, + "name": "test1", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 1, + "z": 1, + "associateId": null, + "typeName": "配液瓶(小)", + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb" + } + ] + }, + { + "id": "3a1d4b7e-ee61-ae87-9cd0-31c7e6621b18", + "typeName": "5ml分液瓶板", + "code": "0010-00089", + "barCode": "", + "name": "test2", + "quantity": 1.0, + "lockQuantity": 0.0, + "unit": "块", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "whid": "3a19deae-2c79-05a3-9c76-8e6760424841", + "whName": "手动堆栈", + "code": "5", + "x": 2, + "y": 2, + "z": 1, + "quantity": 0 + } + ], + "detail": [ + { + "id": "3a1d4b7e-ee61-8fb3-9a39-2c2841c3c8d0", + "detailMaterialId": "3a1d4b7e-ee61-305c-fe30-2620017ca1bd", + "code": null, + "name": "test2", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 1, + "z": 1, + "associateId": null, + "typeName": "5ml分液瓶", + "typeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4" + }, + { + "id": "3a1d4b7e-ee61-ef5f-a7d1-f9399a4d3145", + "detailMaterialId": "3a1d4b7e-ee61-2f1d-6969-202ad3cbe226", + "code": null, + "name": "test2", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 2, + "z": 1, + "associateId": null, + "typeName": "5ml分液瓶", + "typeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4" + } + ] + } +] \ No newline at end of file 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 5ce49e0..092a87f 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 @@ -253,7 +253,7 @@ class BioyondCellWorkstation(BioyondWorkstation): def auto_feeding4to3( self, # ★ 修改点:默认模板路径 - xlsx_path: Optional[str] = "/Users/calvincao/Desktop/work/uni-lab-all/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx", + xlsx_path: Optional[str] = "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, @@ -630,7 +630,12 @@ class BioyondCellWorkstation(BioyondWorkstation): response = self._post_lims("/api/lims/order/orders", orders) print(response) # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") + 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 @@ -963,13 +968,179 @@ class BioyondCellWorkstation(BioyondWorkstation): 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 create_sample( + self, + name: str, + board_type: str, + bottle_type: str, + location_code: str + ) -> Dict[str, Any]: + """创建配液板物料并自动入库。 + Args: + material_name: 物料名称,支持 "5ml分液瓶板"/"5ml分液瓶"、"配液瓶(小)板"/"配液瓶(小)"。 + quantity: 主物料与明细的数量,默认 1。 + location_code: 库位编号,例如 "A01",将自动映射为 "手动堆栈" 下的 UUID。 + """ + carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] + bottle_type_id = MATERIAL_TYPE_MAPPINGS[bottle_type][1] + location_id = WAREHOUSE_MAPPING["手动堆栈"]["site_uuids"][location_code] + + # 新建小瓶 + 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() ws = BioyondCellWorkstation() + ws.create_sample(name="test", board_type="配液瓶(小)板", bottle_type="配液瓶(小)", location_code="B01") # logger.info(ws.scheduler_stop()) # logger.info(ws.scheduler_start()) @@ -981,7 +1152,7 @@ if __name__ == "__main__": # 继续后续流程 # logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 - # # 使用正斜杠或 Path 对象来指定文件路径 + # # # 使用正斜杠或 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()) 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 844fc84..abaf145 100644 Binary files a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 2eb3dbb..577833f 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -16,7 +16,7 @@ API_CONFIG = { "report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), # HTTP 服务配置 - "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.210"), # HTTP服务监听地址,监听计算机飞连ip地址 + "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.33.174"), # HTTP服务监听地址,监听计算机飞连ip地址 "HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), "debug_mode": False,# 调试模式 } diff --git a/unilabos/devices/workstation/coin_cell_assembly.zip b/unilabos/devices/workstation/coin_cell_assembly.zip new file mode 100644 index 0000000..b95b7f4 Binary files /dev/null and b/unilabos/devices/workstation/coin_cell_assembly.zip differ diff --git a/unilabos/registry/devices/laiyu_liquid.yaml b/unilabos/registry/devices/laiyu_liquid.yaml index 98201a7..64c0c18 100644 --- a/unilabos/registry/devices/laiyu_liquid.yaml +++ b/unilabos/registry/devices/laiyu_liquid.yaml @@ -1361,8 +1361,7 @@ laiyu_liquid: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -1492,11 +1491,9 @@ laiyu_liquid: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b21ccd7..99c9233 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -3994,8 +3994,7 @@ liquid_handler: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -4151,11 +4150,9 @@ liquid_handler: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -5015,8 +5012,7 @@ liquid_handler.biomek: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -5159,11 +5155,9 @@ liquid_handler.biomek: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -7807,8 +7801,7 @@ liquid_handler.prcxi: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -7937,11 +7930,9 @@ liquid_handler.prcxi: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648