mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-15 13:44:39 +00:00
fix camera & workstation & warehouse & reaction station driver
This commit is contained in:
@@ -5,10 +5,16 @@
|
|||||||
"name": "reaction_station_bioyond",
|
"name": "reaction_station_bioyond",
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"children": [
|
"children": [
|
||||||
"Bioyond_Deck"
|
"Bioyond_Deck",
|
||||||
|
"reactor_1",
|
||||||
|
"reactor_2",
|
||||||
|
"reactor_3",
|
||||||
|
"reactor_4",
|
||||||
|
"reactor_5"
|
||||||
],
|
],
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.bioyond",
|
"class": "reaction_station.bioyond",
|
||||||
|
"position": {"x": 0, "y": 3800, "z": 0},
|
||||||
"config": {
|
"config": {
|
||||||
"config": {
|
"config": {
|
||||||
"api_key": "DE9BDDA0",
|
"api_key": "DE9BDDA0",
|
||||||
@@ -64,6 +70,61 @@
|
|||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "reactor_1",
|
||||||
|
"name": "reactor_1",
|
||||||
|
"children": [],
|
||||||
|
"parent": "reaction_station_bioyond",
|
||||||
|
"type": "device",
|
||||||
|
"class": "reaction_station.reactor",
|
||||||
|
"position": {"x": 1150, "y": 380, "z": 0},
|
||||||
|
"config": {},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactor_2",
|
||||||
|
"name": "reactor_2",
|
||||||
|
"children": [],
|
||||||
|
"parent": "reaction_station_bioyond",
|
||||||
|
"type": "device",
|
||||||
|
"class": "reaction_station.reactor",
|
||||||
|
"position": {"x": 1365, "y": 380, "z": 0},
|
||||||
|
"config": {},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactor_3",
|
||||||
|
"name": "reactor_3",
|
||||||
|
"children": [],
|
||||||
|
"parent": "reaction_station_bioyond",
|
||||||
|
"type": "device",
|
||||||
|
"class": "reaction_station.reactor",
|
||||||
|
"position": {"x": 1580, "y": 380, "z": 0},
|
||||||
|
"config": {},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactor_4",
|
||||||
|
"name": "reactor_4",
|
||||||
|
"children": [],
|
||||||
|
"parent": "reaction_station_bioyond",
|
||||||
|
"type": "device",
|
||||||
|
"class": "reaction_station.reactor",
|
||||||
|
"position": {"x": 1790, "y": 380, "z": 0},
|
||||||
|
"config": {},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactor_5",
|
||||||
|
"name": "reactor_5",
|
||||||
|
"children": [],
|
||||||
|
"parent": "reaction_station_bioyond",
|
||||||
|
"type": "device",
|
||||||
|
"class": "reaction_station.reactor",
|
||||||
|
"position": {"x": 2010, "y": 380, "z": 0},
|
||||||
|
"config": {},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "Bioyond_Deck",
|
"id": "Bioyond_Deck",
|
||||||
"name": "Bioyond_Deck",
|
"name": "Bioyond_Deck",
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
|
import time
|
||||||
import requests
|
import requests
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
|
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||||
WORKFLOW_STEP_IDS,
|
WORKFLOW_STEP_IDS,
|
||||||
WORKFLOW_TO_SECTION_MAP,
|
WORKFLOW_TO_SECTION_MAP,
|
||||||
@@ -10,6 +14,37 @@ from unilabos.devices.workstation.bioyond_studio.config import (
|
|||||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class BioyondReactor:
|
||||||
|
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
|
||||||
|
self.in_temperature = 0.0
|
||||||
|
self.out_temperature = 0.0
|
||||||
|
self.pt100_temperature = 0.0
|
||||||
|
self.sensor_average_temperature = 0.0
|
||||||
|
self.target_temperature = 0.0
|
||||||
|
self.setting_temperature = 0.0
|
||||||
|
self.viscosity = 0.0
|
||||||
|
self.average_viscosity = 0.0
|
||||||
|
self.speed = 0.0
|
||||||
|
self.force = 0.0
|
||||||
|
|
||||||
|
def update_metrics(self, payload: Dict[str, Any]):
|
||||||
|
def _f(v):
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
self.target_temperature = _f(payload.get("targetTemperature"))
|
||||||
|
self.setting_temperature = _f(payload.get("settingTemperature"))
|
||||||
|
self.in_temperature = _f(payload.get("inTemperature"))
|
||||||
|
self.out_temperature = _f(payload.get("outTemperature"))
|
||||||
|
self.pt100_temperature = _f(payload.get("pt100Temperature"))
|
||||||
|
self.sensor_average_temperature = _f(payload.get("sensorAverageTemperature"))
|
||||||
|
self.speed = _f(payload.get("speed"))
|
||||||
|
self.force = _f(payload.get("force"))
|
||||||
|
self.viscosity = _f(payload.get("viscosity"))
|
||||||
|
self.average_viscosity = _f(payload.get("averageViscosity"))
|
||||||
|
|
||||||
|
|
||||||
class BioyondReactionStation(BioyondWorkstation):
|
class BioyondReactionStation(BioyondWorkstation):
|
||||||
"""Bioyond反应站类
|
"""Bioyond反应站类
|
||||||
|
|
||||||
@@ -37,6 +72,19 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
||||||
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
||||||
|
|
||||||
|
self.in_temperature = 0.0
|
||||||
|
self.out_temperature = 0.0
|
||||||
|
self.pt100_temperature = 0.0
|
||||||
|
self.sensor_average_temperature = 0.0
|
||||||
|
self.target_temperature = 0.0
|
||||||
|
self.setting_temperature = 0.0
|
||||||
|
self.viscosity = 0.0
|
||||||
|
self.average_viscosity = 0.0
|
||||||
|
self.speed = 0.0
|
||||||
|
self.force = 0.0
|
||||||
|
|
||||||
|
self._frame_to_reactor_id = {1: "reactor_1", 2: "reactor_2", 3: "reactor_3", 4: "reactor_4", 5: "reactor_5"}
|
||||||
|
|
||||||
# ==================== 工作流方法 ====================
|
# ==================== 工作流方法 ====================
|
||||||
|
|
||||||
def reactor_taken_out(self):
|
def reactor_taken_out(self):
|
||||||
@@ -291,22 +339,39 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def liquid_feeding_titration(
|
def liquid_feeding_titration(
|
||||||
self,
|
self,
|
||||||
volume_formula: str,
|
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
titration_type: str = "1",
|
volume_formula: str = None,
|
||||||
|
x_value: str = None,
|
||||||
|
feeding_order_data: str = None,
|
||||||
|
extracted_actuals: str = None,
|
||||||
|
titration_type: str = "2",
|
||||||
time: str = "90",
|
time: str = "90",
|
||||||
torque_variation: int = 2,
|
torque_variation: int = 2,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料(滴定)
|
"""液体进料(滴定)
|
||||||
|
|
||||||
|
支持两种模式:
|
||||||
|
1. 直接提供 volume_formula (传统方式)
|
||||||
|
2. 自动计算公式: 提供 x_value, feeding_order_data, extracted_actuals (新方式)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume_formula: 分液公式(μL)
|
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
titration_type: 是否滴定(1=否, 2=是)
|
volume_formula: 分液公式(μL),如果提供则直接使用,否则自动计算
|
||||||
|
x_value: 手工输入的x值,格式如 "1-2-3"
|
||||||
|
feeding_order_data: feeding_order JSON字符串或对象,用于获取m二酐值
|
||||||
|
extracted_actuals: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume
|
||||||
|
titration_type: 是否滴定(1=否, 2=是),默认2
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
|
|
||||||
|
自动公式模板: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
|
||||||
|
其中:
|
||||||
|
- m二酐滴定 = actualTargetWeigh (从extracted_actuals获取)
|
||||||
|
- V二酐滴定 = actualVolume (从extracted_actuals获取)
|
||||||
|
- x = x_value (手工输入)
|
||||||
|
- m二酐 = feeding_order中type为"main_anhydride"的amount值
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
@@ -316,6 +381,84 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
|
|
||||||
|
# 如果没有直接提供volume_formula,则自动计算
|
||||||
|
if not volume_formula and x_value and feeding_order_data and extracted_actuals:
|
||||||
|
# 1. 解析 feeding_order_data 获取 m二酐
|
||||||
|
if isinstance(feeding_order_data, str):
|
||||||
|
try:
|
||||||
|
feeding_order_data = json.loads(feeding_order_data)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"feeding_order_data JSON解析失败: {str(e)}")
|
||||||
|
|
||||||
|
# 支持两种格式:
|
||||||
|
# 格式1: 直接是数组 [{...}, {...}]
|
||||||
|
# 格式2: 对象包裹 {"feeding_order": [{...}, {...}]}
|
||||||
|
if isinstance(feeding_order_data, list):
|
||||||
|
feeding_order_list = feeding_order_data
|
||||||
|
elif isinstance(feeding_order_data, dict):
|
||||||
|
feeding_order_list = feeding_order_data.get("feeding_order", [])
|
||||||
|
else:
|
||||||
|
raise ValueError("feeding_order_data 必须是数组或包含feeding_order的字典")
|
||||||
|
|
||||||
|
# 从feeding_order中找到main_anhydride的amount
|
||||||
|
m_anhydride = None
|
||||||
|
for item in feeding_order_list:
|
||||||
|
if item.get("type") == "main_anhydride":
|
||||||
|
m_anhydride = item.get("amount")
|
||||||
|
break
|
||||||
|
|
||||||
|
if m_anhydride is None:
|
||||||
|
raise ValueError("在feeding_order中未找到type为'main_anhydride'的条目")
|
||||||
|
|
||||||
|
# 2. 解析 extracted_actuals 获取 actualTargetWeigh 和 actualVolume
|
||||||
|
if isinstance(extracted_actuals, str):
|
||||||
|
try:
|
||||||
|
extracted_actuals_obj = json.loads(extracted_actuals)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"extracted_actuals JSON解析失败: {str(e)}")
|
||||||
|
else:
|
||||||
|
extracted_actuals_obj = extracted_actuals
|
||||||
|
|
||||||
|
# 获取actuals数组
|
||||||
|
actuals_list = extracted_actuals_obj.get("actuals", [])
|
||||||
|
if not actuals_list:
|
||||||
|
# actuals为空,无法自动生成公式,回退到手动模式
|
||||||
|
print(f"警告: extracted_actuals中actuals数组为空,无法自动生成公式,请手动提供volume_formula")
|
||||||
|
volume_formula = None # 清空,触发后续的错误检查
|
||||||
|
else:
|
||||||
|
# 根据assign_material_name匹配对应的actual数据
|
||||||
|
# 假设order_code中包含物料名称
|
||||||
|
matched_actual = None
|
||||||
|
for actual in actuals_list:
|
||||||
|
order_code = actual.get("order_code", "")
|
||||||
|
# 简单匹配:如果order_code包含物料名称
|
||||||
|
if assign_material_name in order_code:
|
||||||
|
matched_actual = actual
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果没有匹配到,使用第一个
|
||||||
|
if not matched_actual and actuals_list:
|
||||||
|
matched_actual = actuals_list[0]
|
||||||
|
|
||||||
|
if not matched_actual:
|
||||||
|
raise ValueError("无法从extracted_actuals中获取实际加料量数据")
|
||||||
|
|
||||||
|
m_anhydride_titration = matched_actual.get("actualTargetWeigh") # m二酐滴定
|
||||||
|
v_anhydride_titration = matched_actual.get("actualVolume") # V二酐滴定
|
||||||
|
|
||||||
|
if m_anhydride_titration is None or v_anhydride_titration is None:
|
||||||
|
raise ValueError(f"实际加料量数据不完整: actualTargetWeigh={m_anhydride_titration}, actualVolume={v_anhydride_titration}")
|
||||||
|
|
||||||
|
# 3. 构建公式: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
|
||||||
|
# x_value 格式如 "{{1-2-3}}",保留完整格式(包括花括号)直接替换到公式中
|
||||||
|
volume_formula = f"1000*({m_anhydride}-{x_value})*{v_anhydride_titration}/{m_anhydride_titration}"
|
||||||
|
|
||||||
|
print(f"自动生成滴定公式: {volume_formula}")
|
||||||
|
print(f" m二酐={m_anhydride}, x={x_value}, V二酐滴定={v_anhydride_titration}, m二酐滴定={m_anhydride_titration}")
|
||||||
|
|
||||||
|
elif not volume_formula:
|
||||||
|
raise ValueError("必须提供 volume_formula 或 (x_value + feeding_order_data + extracted_actuals)")
|
||||||
|
|
||||||
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
||||||
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
||||||
|
|
||||||
@@ -343,6 +486,286 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
|
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
|
||||||
|
data = report.get('data') if isinstance(report, dict) else None
|
||||||
|
actual_target_weigh = None
|
||||||
|
actual_volume = None
|
||||||
|
if data:
|
||||||
|
extra = data.get('extraProperties') or {}
|
||||||
|
if isinstance(extra, dict):
|
||||||
|
for v in extra.values():
|
||||||
|
obj = None
|
||||||
|
try:
|
||||||
|
obj = json.loads(v) if isinstance(v, str) else v
|
||||||
|
except Exception:
|
||||||
|
obj = None
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
tw = obj.get('targetWeigh')
|
||||||
|
vol = obj.get('volume')
|
||||||
|
if tw is not None:
|
||||||
|
try:
|
||||||
|
actual_target_weigh = float(tw)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if vol is not None:
|
||||||
|
try:
|
||||||
|
actual_volume = float(vol)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
'actualTargetWeigh': actual_target_weigh,
|
||||||
|
'actualVolume': actual_volume
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_actuals_from_batch_reports(self, batch_reports_result: str) -> dict:
|
||||||
|
print(f"[DEBUG] extract_actuals 收到原始数据: {batch_reports_result[:500]}...") # 打印前500字符
|
||||||
|
try:
|
||||||
|
obj = json.loads(batch_reports_result) if isinstance(batch_reports_result, str) else batch_reports_result
|
||||||
|
if isinstance(obj, dict) and "return_info" in obj:
|
||||||
|
inner = obj["return_info"]
|
||||||
|
obj = json.loads(inner) if isinstance(inner, str) else inner
|
||||||
|
reports = obj.get("reports", []) if isinstance(obj, dict) else []
|
||||||
|
print(f"[DEBUG] 解析后的 reports 数组长度: {len(reports)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DEBUG] 解析异常: {e}")
|
||||||
|
reports = []
|
||||||
|
|
||||||
|
actuals = []
|
||||||
|
for i, r in enumerate(reports):
|
||||||
|
print(f"[DEBUG] 处理 report[{i}]: order_code={r.get('order_code')}, has_extracted={r.get('extracted') is not None}, has_report={r.get('report') is not None}")
|
||||||
|
order_code = r.get("order_code")
|
||||||
|
order_id = r.get("order_id")
|
||||||
|
ex = r.get("extracted")
|
||||||
|
if isinstance(ex, dict) and (ex.get("actualTargetWeigh") is not None or ex.get("actualVolume") is not None):
|
||||||
|
print(f"[DEBUG] 从 extracted 字段提取: actualTargetWeigh={ex.get('actualTargetWeigh')}, actualVolume={ex.get('actualVolume')}")
|
||||||
|
actuals.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
"actualTargetWeigh": ex.get("actualTargetWeigh"),
|
||||||
|
"actualVolume": ex.get("actualVolume")
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
report = r.get("report")
|
||||||
|
vals = self._extract_actuals_from_report(report) if report else {"actualTargetWeigh": None, "actualVolume": None}
|
||||||
|
print(f"[DEBUG] 从 report 字段提取: {vals}")
|
||||||
|
actuals.append({
|
||||||
|
"order_code": order_code,
|
||||||
|
"order_id": order_id,
|
||||||
|
**vals
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"[DEBUG] 最终提取的 actuals 数组长度: {len(actuals)}")
|
||||||
|
result = {
|
||||||
|
"return_info": json.dumps({"actuals": actuals}, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
print(f"[DEBUG] 返回结果: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def process_temperature_cutoff_report(self, report_request) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
data = report_request.data
|
||||||
|
def _f(v):
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
self.target_temperature = _f(data.get("targetTemperature"))
|
||||||
|
self.setting_temperature = _f(data.get("settingTemperature"))
|
||||||
|
self.in_temperature = _f(data.get("inTemperature"))
|
||||||
|
self.out_temperature = _f(data.get("outTemperature"))
|
||||||
|
self.pt100_temperature = _f(data.get("pt100Temperature"))
|
||||||
|
self.sensor_average_temperature = _f(data.get("sensorAverageTemperature"))
|
||||||
|
self.speed = _f(data.get("speed"))
|
||||||
|
self.force = _f(data.get("force"))
|
||||||
|
self.viscosity = _f(data.get("viscosity"))
|
||||||
|
self.average_viscosity = _f(data.get("averageViscosity"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
||||||
|
props = [
|
||||||
|
"in_temperature","out_temperature","pt100_temperature","sensor_average_temperature",
|
||||||
|
"target_temperature","setting_temperature","viscosity","average_viscosity",
|
||||||
|
"speed","force"
|
||||||
|
]
|
||||||
|
for name in props:
|
||||||
|
pub = self._ros_node._property_publishers.get(name)
|
||||||
|
if pub:
|
||||||
|
pub.publish_property()
|
||||||
|
frame = data.get("frameCode")
|
||||||
|
reactor_id = None
|
||||||
|
try:
|
||||||
|
reactor_id = self._frame_to_reactor_id.get(int(frame))
|
||||||
|
except Exception:
|
||||||
|
reactor_id = None
|
||||||
|
if reactor_id and hasattr(self._ros_node, "sub_devices"):
|
||||||
|
child = self._ros_node.sub_devices.get(reactor_id)
|
||||||
|
if child and hasattr(child, "driver_instance"):
|
||||||
|
child.driver_instance.update_metrics(data)
|
||||||
|
pubs = getattr(child.ros_node_instance, "_property_publishers", {})
|
||||||
|
for name in props:
|
||||||
|
p = pubs.get(name)
|
||||||
|
if p:
|
||||||
|
p.publish_property()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
event = {
|
||||||
|
"frameCode": data.get("frameCode"),
|
||||||
|
"generateTime": data.get("generateTime"),
|
||||||
|
"targetTemperature": data.get("targetTemperature"),
|
||||||
|
"settingTemperature": data.get("settingTemperature"),
|
||||||
|
"inTemperature": data.get("inTemperature"),
|
||||||
|
"outTemperature": data.get("outTemperature"),
|
||||||
|
"pt100Temperature": data.get("pt100Temperature"),
|
||||||
|
"sensorAverageTemperature": data.get("sensorAverageTemperature"),
|
||||||
|
"speed": data.get("speed"),
|
||||||
|
"force": data.get("force"),
|
||||||
|
"viscosity": data.get("viscosity"),
|
||||||
|
"averageViscosity": data.get("averageViscosity"),
|
||||||
|
"request_time": report_request.request_time,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"reactor_id": self._frame_to_reactor_id.get(int(data.get("frameCode", 0))) if str(data.get("frameCode", "")).isdigit() else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_file = base_dir / "temperature_cutoff_events.json"
|
||||||
|
try:
|
||||||
|
existing = json.loads(out_file.read_text(encoding="utf-8")) if out_file.exists() else []
|
||||||
|
if not isinstance(existing, list):
|
||||||
|
existing = []
|
||||||
|
except Exception:
|
||||||
|
existing = []
|
||||||
|
existing.append(event)
|
||||||
|
out_file.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
||||||
|
ns = self._ros_node.namespace
|
||||||
|
topics = {
|
||||||
|
"targetTemperature": f"{ns}/metrics/temperature_cutoff/target_temperature",
|
||||||
|
"settingTemperature": f"{ns}/metrics/temperature_cutoff/setting_temperature",
|
||||||
|
"inTemperature": f"{ns}/metrics/temperature_cutoff/in_temperature",
|
||||||
|
"outTemperature": f"{ns}/metrics/temperature_cutoff/out_temperature",
|
||||||
|
"pt100Temperature": f"{ns}/metrics/temperature_cutoff/pt100_temperature",
|
||||||
|
"sensorAverageTemperature": f"{ns}/metrics/temperature_cutoff/sensor_average_temperature",
|
||||||
|
"speed": f"{ns}/metrics/temperature_cutoff/speed",
|
||||||
|
"force": f"{ns}/metrics/temperature_cutoff/force",
|
||||||
|
"viscosity": f"{ns}/metrics/temperature_cutoff/viscosity",
|
||||||
|
"averageViscosity": f"{ns}/metrics/temperature_cutoff/average_viscosity",
|
||||||
|
}
|
||||||
|
for k, t in topics.items():
|
||||||
|
v = data.get(k)
|
||||||
|
if v is not None:
|
||||||
|
pub = self._ros_node.create_publisher(Float64, t, 10)
|
||||||
|
pub.publish(convert_to_ros_msg(Float64, float(v)))
|
||||||
|
|
||||||
|
evt_pub = self._ros_node.create_publisher(String, f"{ns}/events/temperature_cutoff", 10)
|
||||||
|
evt_pub.publish(convert_to_ros_msg(String, json.dumps(event, ensure_ascii=False)))
|
||||||
|
|
||||||
|
return {"processed": True, "frame": data.get("frameCode")}
|
||||||
|
except Exception as e:
|
||||||
|
return {"processed": False, "error": str(e)}
|
||||||
|
|
||||||
|
def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = None, timeout: int = 7200, check_interval: int = 10) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
timeout = int(timeout) if timeout else 7200
|
||||||
|
check_interval = int(check_interval) if check_interval else 10
|
||||||
|
if not batch_create_result or batch_create_result == "":
|
||||||
|
raise ValueError("batch_create_result为空")
|
||||||
|
try:
|
||||||
|
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
|
||||||
|
batch_create_result = batch_create_result.replace('[...]', '[]')
|
||||||
|
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
|
||||||
|
if isinstance(result_obj, dict) and "return_value" in result_obj:
|
||||||
|
inner = result_obj.get("return_value")
|
||||||
|
if isinstance(inner, str):
|
||||||
|
result_obj = json.loads(inner)
|
||||||
|
elif isinstance(inner, dict):
|
||||||
|
result_obj = inner
|
||||||
|
order_codes = result_obj.get("order_codes", [])
|
||||||
|
order_ids = result_obj.get("order_ids", [])
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"解析batch_create_result失败: {e}")
|
||||||
|
if not order_codes or not order_ids:
|
||||||
|
raise ValueError("缺少order_codes或order_ids")
|
||||||
|
if not isinstance(order_codes, list):
|
||||||
|
order_codes = [order_codes]
|
||||||
|
if not isinstance(order_ids, list):
|
||||||
|
order_ids = [order_ids]
|
||||||
|
if len(order_codes) != len(order_ids):
|
||||||
|
raise ValueError("order_codes与order_ids数量不匹配")
|
||||||
|
total = len(order_codes)
|
||||||
|
pending = {c: {"order_id": order_ids[i], "completed": False} for i, c in enumerate(order_codes)}
|
||||||
|
reports = []
|
||||||
|
start_time = time.time()
|
||||||
|
while pending:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time > timeout:
|
||||||
|
for oc in list(pending.keys()):
|
||||||
|
reports.append({
|
||||||
|
"order_code": oc,
|
||||||
|
"order_id": pending[oc]["order_id"],
|
||||||
|
"status": "timeout",
|
||||||
|
"completion_status": None,
|
||||||
|
"report": None,
|
||||||
|
"extracted": None,
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
break
|
||||||
|
completed_round = []
|
||||||
|
for oc in list(pending.keys()):
|
||||||
|
oid = pending[oc]["order_id"]
|
||||||
|
if oc in self.order_completion_status:
|
||||||
|
info = self.order_completion_status[oc]
|
||||||
|
try:
|
||||||
|
rq = json.dumps({"order_id": oid})
|
||||||
|
rep = self.hardware_interface.order_report(rq)
|
||||||
|
if not rep:
|
||||||
|
rep = {"error": "无法获取报告"}
|
||||||
|
reports.append({
|
||||||
|
"order_code": oc,
|
||||||
|
"order_id": oid,
|
||||||
|
"status": "completed",
|
||||||
|
"completion_status": info.get('status'),
|
||||||
|
"report": rep,
|
||||||
|
"extracted": self._extract_actuals_from_report(rep),
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
completed_round.append(oc)
|
||||||
|
del self.order_completion_status[oc]
|
||||||
|
except Exception as e:
|
||||||
|
reports.append({
|
||||||
|
"order_code": oc,
|
||||||
|
"order_id": oid,
|
||||||
|
"status": "error",
|
||||||
|
"completion_status": info.get('status') if 'info' in locals() else None,
|
||||||
|
"report": None,
|
||||||
|
"extracted": None,
|
||||||
|
"error": str(e),
|
||||||
|
"elapsed_time": elapsed_time
|
||||||
|
})
|
||||||
|
completed_round.append(oc)
|
||||||
|
for oc in completed_round:
|
||||||
|
del pending[oc]
|
||||||
|
if pending:
|
||||||
|
time.sleep(check_interval)
|
||||||
|
completed_count = sum(1 for r in reports if r['status'] == 'completed')
|
||||||
|
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
|
||||||
|
error_count = sum(1 for r in reports if r['status'] == 'error')
|
||||||
|
final_elapsed_time = time.time() - start_time
|
||||||
|
summary = {
|
||||||
|
"total": total,
|
||||||
|
"completed": completed_count,
|
||||||
|
"timeout": timeout_count,
|
||||||
|
"error": error_count,
|
||||||
|
"elapsed_time": round(final_elapsed_time, 2),
|
||||||
|
"reports": reports
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"return_info": json.dumps(summary, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
|
||||||
def liquid_feeding_beaker(
|
def liquid_feeding_beaker(
|
||||||
self,
|
self,
|
||||||
volume: str = "350",
|
volume: str = "350",
|
||||||
@@ -580,10 +1003,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
# print(f"\n✅ 任务创建成功: {result}")
|
# print(f"\n✅ 任务创建成功: {result}")
|
||||||
# print(f"\n✅ 任务创建成功")
|
# print(f"\n✅ 任务创建成功")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
# 返回结果,包含合并后的工作流数据和订单参数
|
# 返回结果,包含合并后的工作流数据和订单参数
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"success": True,
|
"success": True,
|
||||||
"result": result,
|
"result": result,
|
||||||
"merged_workflow": merged_workflow,
|
"merged_workflow": merged_workflow,
|
||||||
"order_params": order_params
|
"order_params": order_params
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import traceback
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional, Union
|
from typing import Dict, Any, List, Optional, Union
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer
|
from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer
|
||||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
|
||||||
@@ -19,6 +20,7 @@ from unilabos.resources.graphio import resource_bioyond_to_plr, resource_plr_to_
|
|||||||
|
|
||||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
|
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
|
||||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||||
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||||
@@ -676,13 +678,13 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
self._synced_resources = []
|
self._synced_resources = []
|
||||||
|
|
||||||
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
|
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
|
||||||
time.sleep(3)
|
future = ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
|
||||||
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
|
|
||||||
"plr_resources": resource,
|
"plr_resources": resource,
|
||||||
"target_device_id": mount_device_id,
|
"target_device_id": mount_device_id,
|
||||||
"target_resources": mount_resource,
|
"target_resources": mount_resource,
|
||||||
"sites": sites,
|
"sites": sites,
|
||||||
})
|
})
|
||||||
|
return future
|
||||||
|
|
||||||
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""创建Bioyond通信模块"""
|
"""创建Bioyond通信模块"""
|
||||||
@@ -1327,6 +1329,7 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
logger.error(f"处理物料变更报送失败: {e}")
|
logger.error(f"处理物料变更报送失败: {e}")
|
||||||
return {"processed": False, "error": str(e)}
|
return {"processed": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def handle_external_error(self, error_data: Dict[str, Any]) -> Dict[str, Any]:
|
def handle_external_error(self, error_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""处理错误处理报送
|
"""处理错误处理报送
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
@@ -76,6 +77,14 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload_for_log = {"method": "POST", **request_data}
|
||||||
|
self._save_raw_request(endpoint, payload_for_log)
|
||||||
|
if hasattr(self.workstation, '_reports_received_count'):
|
||||||
|
self.workstation._reports_received_count += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 统一的报送端点路由(基于LIMS协议规范)
|
# 统一的报送端点路由(基于LIMS协议规范)
|
||||||
if endpoint == '/report/step_finish':
|
if endpoint == '/report/step_finish':
|
||||||
response = self._handle_step_finish_report(request_data)
|
response = self._handle_step_finish_report(request_data)
|
||||||
@@ -90,6 +99,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
response = self._handle_material_change_report(request_data)
|
response = self._handle_material_change_report(request_data)
|
||||||
elif endpoint == '/report/error_handling':
|
elif endpoint == '/report/error_handling':
|
||||||
response = self._handle_error_handling_report(request_data)
|
response = self._handle_error_handling_report(request_data)
|
||||||
|
elif endpoint == '/report/temperature-cutoff':
|
||||||
|
response = self._handle_temperature_cutoff_report(request_data)
|
||||||
# 保留LIMS协议端点以兼容现有系统
|
# 保留LIMS协议端点以兼容现有系统
|
||||||
elif endpoint == '/LIMS/step_finish':
|
elif endpoint == '/LIMS/step_finish':
|
||||||
response = self._handle_step_finish_report(request_data)
|
response = self._handle_step_finish_report(request_data)
|
||||||
@@ -107,7 +118,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"/report/order_finish",
|
"/report/order_finish",
|
||||||
"/report/batch_update",
|
"/report/batch_update",
|
||||||
"/report/material_change",
|
"/report/material_change",
|
||||||
"/report/error_handling"
|
"/report/error_handling",
|
||||||
|
"/report/temperature-cutoff"
|
||||||
]}
|
]}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,6 +140,11 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
parsed_path = urlparse(self.path)
|
parsed_path = urlparse(self.path)
|
||||||
endpoint = parsed_path.path
|
endpoint = parsed_path.path
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._save_raw_request(endpoint, {"method": "GET"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if endpoint == '/status':
|
if endpoint == '/status':
|
||||||
response = self._handle_status_check()
|
response = self._handle_status_check()
|
||||||
elif endpoint == '/health':
|
elif endpoint == '/health':
|
||||||
@@ -318,6 +335,50 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
message=f"任务完成报送处理失败: {str(e)}"
|
message=f"任务完成报送处理失败: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _handle_temperature_cutoff_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
try:
|
||||||
|
required_fields = ['token', 'request_time', 'data']
|
||||||
|
if missing := [f for f in required_fields if f not in request_data]:
|
||||||
|
return HttpResponse(success=False, message=f"缺少必要字段: {', '.join(missing)}")
|
||||||
|
|
||||||
|
data = request_data['data']
|
||||||
|
metrics = [
|
||||||
|
'frameCode',
|
||||||
|
'generateTime',
|
||||||
|
'targetTemperature',
|
||||||
|
'settingTemperature',
|
||||||
|
'inTemperature',
|
||||||
|
'outTemperature',
|
||||||
|
'pt100Temperature',
|
||||||
|
'sensorAverageTemperature',
|
||||||
|
'speed',
|
||||||
|
'force',
|
||||||
|
'viscosity',
|
||||||
|
'averageViscosity'
|
||||||
|
]
|
||||||
|
if miss := [f for f in metrics if f not in data]:
|
||||||
|
return HttpResponse(success=False, message=f"data字段缺少必要内容: {', '.join(miss)}")
|
||||||
|
|
||||||
|
report_request = WorkstationReportRequest(
|
||||||
|
token=request_data['token'],
|
||||||
|
request_time=request_data['request_time'],
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
if hasattr(self.workstation, 'process_temperature_cutoff_report'):
|
||||||
|
result = self.workstation.process_temperature_cutoff_report(report_request)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"温度/粘度报送已处理: 帧{data['frameCode']}",
|
||||||
|
acknowledgment_id=f"TEMP_CUTOFF_{int(time.time()*1000)}_{data['frameCode']}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理温度/粘度报送失败: {e}\n{traceback.format_exc()}")
|
||||||
|
return HttpResponse(success=False, message=f"温度/粘度报送处理失败: {str(e)}")
|
||||||
|
|
||||||
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
"""处理批量报送"""
|
"""处理批量报送"""
|
||||||
try:
|
try:
|
||||||
@@ -510,6 +571,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"POST /report/batch_update",
|
"POST /report/batch_update",
|
||||||
"POST /report/material_change",
|
"POST /report/material_change",
|
||||||
"POST /report/error_handling",
|
"POST /report/error_handling",
|
||||||
|
"POST /report/temperature-cutoff",
|
||||||
"GET /status",
|
"GET /status",
|
||||||
"GET /health"
|
"GET /health"
|
||||||
]
|
]
|
||||||
@@ -547,6 +609,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
"""重写日志方法"""
|
"""重写日志方法"""
|
||||||
logger.debug(f"HTTP请求: {format % args}")
|
logger.debug(f"HTTP请求: {format % args}")
|
||||||
|
|
||||||
|
def _save_raw_request(self, endpoint: str, request_data: Dict[str, Any]) -> None:
|
||||||
|
try:
|
||||||
|
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
log_path = getattr(self.workstation, "_http_log_path", None)
|
||||||
|
log_file = Path(log_path) if log_path else (base_dir / f"http_{int(time.time()*1000)}.log")
|
||||||
|
payload = {
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"received_at": datetime.now().isoformat(),
|
||||||
|
"body": request_data
|
||||||
|
}
|
||||||
|
with open(log_file, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WorkstationHTTPService:
|
class WorkstationHTTPService:
|
||||||
"""工作站HTTP服务"""
|
"""工作站HTTP服务"""
|
||||||
@@ -572,6 +650,10 @@ class WorkstationHTTPService:
|
|||||||
|
|
||||||
# 创建HTTP服务器
|
# 创建HTTP服务器
|
||||||
self.server = HTTPServer((self.host, self.port), handler_factory)
|
self.server = HTTPServer((self.host, self.port), handler_factory)
|
||||||
|
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
session_log = base_dir / f"http_{int(time.time()*1000)}.log"
|
||||||
|
setattr(self.workstation, "_http_log_path", str(session_log))
|
||||||
|
|
||||||
# 安全地获取 device_id 用于线程命名
|
# 安全地获取 device_id 用于线程命名
|
||||||
device_id = "unknown"
|
device_id = "unknown"
|
||||||
@@ -599,6 +681,7 @@ class WorkstationHTTPService:
|
|||||||
logger.info("扩展报送端点:")
|
logger.info("扩展报送端点:")
|
||||||
logger.info(" - POST /report/material_change # 物料变更报送")
|
logger.info(" - POST /report/material_change # 物料变更报送")
|
||||||
logger.info(" - POST /report/error_handling # 错误处理报送")
|
logger.info(" - POST /report/error_handling # 错误处理报送")
|
||||||
|
logger.info(" - POST /report/temperature-cutoff # 温度/粘度报送")
|
||||||
logger.info("兼容端点:")
|
logger.info("兼容端点:")
|
||||||
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
||||||
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
||||||
@@ -700,6 +783,9 @@ if __name__ == "__main__":
|
|||||||
def handle_external_error(self, error_data):
|
def handle_external_error(self, error_data):
|
||||||
return {"handled": True}
|
return {"handled": True}
|
||||||
|
|
||||||
|
def process_temperature_cutoff_report(self, report_request):
|
||||||
|
return {"processed": True, "metrics": report_request.data}
|
||||||
|
|
||||||
workstation = BioyondWorkstation()
|
workstation = BioyondWorkstation()
|
||||||
http_service = WorkstationHTTPService(workstation)
|
http_service = WorkstationHTTPService(workstation)
|
||||||
|
|
||||||
@@ -723,4 +809,3 @@ if __name__ == "__main__":
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"服务器运行错误: {e}")
|
print(f"服务器运行错误: {e}")
|
||||||
http_service.stop()
|
http_service.stop()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
camera.USB:
|
camera:
|
||||||
category:
|
category:
|
||||||
- camera
|
- camera
|
||||||
class:
|
class:
|
||||||
|
|||||||
@@ -4,106 +4,6 @@ reaction_station.bioyond:
|
|||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
auto-create_order:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
json_str: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
json_str:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- json_str
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: create_order参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-merge_workflow_with_parameters:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
json_str: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
json_str:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- json_str
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: merge_workflow_with_parameters参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_web_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
web_workflow_json: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
web_workflow_json:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- web_workflow_json
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_web_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_step_query:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_step_query参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
drip_back:
|
drip_back:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -160,6 +60,56 @@ reaction_station.bioyond:
|
|||||||
title: drip_back参数
|
title: drip_back参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
extract_actuals_from_batch_reports:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
batch_reports_result: batch_reports_result
|
||||||
|
goal_default:
|
||||||
|
batch_reports_result: ''
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: batch_reports_result
|
||||||
|
data_source: handle
|
||||||
|
data_type: string
|
||||||
|
handler_key: BATCH_REPORTS_RESULT
|
||||||
|
io_type: source
|
||||||
|
label: Batch Order Completion Reports
|
||||||
|
output:
|
||||||
|
- data_key: return_info
|
||||||
|
data_source: executor
|
||||||
|
data_type: string
|
||||||
|
handler_key: ACTUALS_EXTRACTED
|
||||||
|
io_type: sink
|
||||||
|
label: Extracted Actuals
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
batch_reports_result:
|
||||||
|
description: 批量任务完成信息JSON字符串或对象,包含reports数组
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- batch_reports_result
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: JSON字符串,包含actuals数组,每项含order_code, order_id, actualTargetWeigh,
|
||||||
|
actualVolume
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: extract_actuals_from_batch_reports结果
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: extract_actuals_from_batch_reports参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
liquid_feeding_beaker:
|
liquid_feeding_beaker:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -287,22 +237,41 @@ reaction_station.bioyond:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
assign_material_name: assign_material_name
|
assign_material_name: assign_material_name
|
||||||
|
extracted_actuals: extracted_actuals
|
||||||
|
feeding_order_data: feeding_order_data
|
||||||
temperature: temperature
|
temperature: temperature
|
||||||
time: time
|
time: time
|
||||||
titration_type: titration_type
|
titration_type: titration_type
|
||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume_formula: volume_formula
|
volume_formula: volume_formula
|
||||||
|
x_value: x_value
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: ''
|
||||||
temperature: ''
|
extracted_actuals: ''
|
||||||
time: ''
|
feeding_order_data: ''
|
||||||
titration_type: ''
|
temperature: '25.00'
|
||||||
torque_variation: ''
|
time: '90'
|
||||||
|
titration_type: '2'
|
||||||
|
torque_variation: '2'
|
||||||
volume_formula: ''
|
volume_formula: ''
|
||||||
handles: {}
|
x_value: ''
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: extracted_actuals
|
||||||
|
data_source: handle
|
||||||
|
data_type: string
|
||||||
|
handler_key: ACTUALS_EXTRACTED
|
||||||
|
io_type: source
|
||||||
|
label: Extracted Actuals From Reports
|
||||||
|
- data_key: feeding_order_data
|
||||||
|
data_source: handle
|
||||||
|
data_type: object
|
||||||
|
handler_key: feeding_order
|
||||||
|
io_type: source
|
||||||
|
label: Feeding Order Data From Calculation Node
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料(滴定)
|
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -310,28 +279,37 @@ reaction_station.bioyond:
|
|||||||
assign_material_name:
|
assign_material_name:
|
||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
|
extracted_actuals:
|
||||||
|
description: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh(m二酐滴定)和actualVolume(V二酐滴定)
|
||||||
|
type: string
|
||||||
|
feeding_order_data:
|
||||||
|
description: 'feeding_order JSON对象,用于获取m二酐值(type为main_anhydride的amount)。示例:
|
||||||
|
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
|
||||||
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
description: 温度设定(°C)
|
default: '25.00'
|
||||||
|
description: 温度设定(°C),默认25.00
|
||||||
type: string
|
type: string
|
||||||
time:
|
time:
|
||||||
description: 观察时间(分钟)
|
default: '90'
|
||||||
|
description: 观察时间(分钟),默认90
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
default: '2'
|
||||||
|
description: 是否滴定(1=否, 2=是),默认2
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
default: '2'
|
||||||
|
description: 是否观察 (1=否, 2=是),默认2
|
||||||
type: string
|
type: string
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(μL)
|
description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
||||||
|
type: string
|
||||||
|
x_value:
|
||||||
|
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume_formula
|
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
- time
|
|
||||||
- torque_variation
|
|
||||||
- titration_type
|
|
||||||
- temperature
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -546,7 +524,19 @@ reaction_station.bioyond:
|
|||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
|
||||||
protocol_type: []
|
protocol_type: []
|
||||||
status_types:
|
status_types:
|
||||||
workflow_sequence: String
|
all_workflows: dict
|
||||||
|
average_viscosity: float
|
||||||
|
bioyond_status: dict
|
||||||
|
force: float
|
||||||
|
in_temperature: float
|
||||||
|
out_temperature: float
|
||||||
|
pt100_temperature: float
|
||||||
|
sensor_average_temperature: float
|
||||||
|
setting_temperature: float
|
||||||
|
speed: float
|
||||||
|
target_temperature: float
|
||||||
|
viscosity: float
|
||||||
|
workstation_status: dict
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Bioyond反应站
|
description: Bioyond反应站
|
||||||
@@ -558,18 +548,75 @@ reaction_station.bioyond:
|
|||||||
config:
|
config:
|
||||||
type: object
|
type: object
|
||||||
deck:
|
deck:
|
||||||
type: string
|
type: object
|
||||||
protocol_type:
|
|
||||||
type: string
|
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
workflow_sequence:
|
all_workflows:
|
||||||
items:
|
type: object
|
||||||
type: string
|
bioyond_status:
|
||||||
type: array
|
type: object
|
||||||
|
workstation_status:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- workflow_sequence
|
- bioyond_status
|
||||||
|
- all_workflows
|
||||||
|
- workstation_status
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
|
reaction_station.reactor:
|
||||||
|
category:
|
||||||
|
- reactor
|
||||||
|
- reaction_station_bioyond
|
||||||
|
class:
|
||||||
|
action_value_mappings: {}
|
||||||
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor
|
||||||
|
status_types:
|
||||||
|
average_viscosity: float
|
||||||
|
force: float
|
||||||
|
in_temperature: float
|
||||||
|
out_temperature: float
|
||||||
|
pt100_temperature: float
|
||||||
|
sensor_average_temperature: float
|
||||||
|
setting_temperature: float
|
||||||
|
speed: float
|
||||||
|
target_temperature: float
|
||||||
|
viscosity: float
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 反应站子设备-反应器
|
||||||
|
handles: []
|
||||||
|
icon: reaction_station.webp
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
config:
|
||||||
|
type: object
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
average_viscosity:
|
||||||
|
type: number
|
||||||
|
force:
|
||||||
|
type: number
|
||||||
|
in_temperature:
|
||||||
|
type: number
|
||||||
|
out_temperature:
|
||||||
|
type: number
|
||||||
|
pt100_temperature:
|
||||||
|
type: number
|
||||||
|
sensor_average_temperature:
|
||||||
|
type: number
|
||||||
|
setting_temperature:
|
||||||
|
type: number
|
||||||
|
speed:
|
||||||
|
type: number
|
||||||
|
target_temperature:
|
||||||
|
type: number
|
||||||
|
viscosity:
|
||||||
|
type: number
|
||||||
|
required: []
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
|||||||
dx=10.0,
|
dx=10.0,
|
||||||
dy=10.0,
|
dy=10.0,
|
||||||
dz=10.0,
|
dz=10.0,
|
||||||
item_dx=137.0,
|
item_dx=147.0,
|
||||||
item_dy=96.0,
|
item_dy=106.0,
|
||||||
item_dz=120.0,
|
item_dz=130.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
col_offset=0, # 从01开始: A01, A02, A03, A04
|
col_offset=0, # 从01开始: A01, A02, A03, A04
|
||||||
layout="row-major", # ⭐ 改为行优先排序
|
layout="row-major", # ⭐ 改为行优先排序
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ from cv_bridge import CvBridge
|
|||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
||||||
|
|
||||||
class VideoPublisher(BaseROS2DeviceNode):
|
class VideoPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id='video_publisher', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
def __init__(self, device_id='video_publisher', device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
||||||
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
device_uuid=device_uuid,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface="camera",
|
hardware_interface="camera",
|
||||||
|
|||||||
Reference in New Issue
Block a user