Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions test/mock/laiyu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"nodes": [
{
"id": "xyz_and_pipette",
"name": "laiyu_liquid xyz and pipette",
"children": [],
"parent": "",
"type": "device",
"class": "xyz_pipette_device",
"position": {
"x": 450,
"y": 450,
"z": 0
},
"config": {
"port": "/dev/ttyUSB0"
}
}
],
"links": []
}
24 changes: 24 additions & 0 deletions test/mock/mock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"nodes": [
{
"id": "virtual_printer_device",
"name": "虚拟打印设备",
"children": [],
"parent": "",
"type": "device",
"class": "virtual_printer",
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"host_id": "demo-host",
"port": "VIRTUAL",
"prefix": "[VIRTUAL-PRINTER]",
"pretty": true
}
}
],
"links": []
}
100 changes: 100 additions & 0 deletions unilabos/devices/virtual/virtual_printer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import json
import logging
from datetime import datetime
from typing import Any, Dict, Optional

from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode


class VirtualPrinter:
_ros_node: BaseROS2DeviceNode

def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
if device_id is None and "id" in kwargs:
device_id = kwargs.pop("id")
if config is None and "config" in kwargs:
config = kwargs.pop("config")

self.device_id = device_id or "virtual_printer"
self.config = config or {}

self.logger = logging.getLogger(f"VirtualPrinter.{self.device_id}")
self.data: Dict[str, Any] = {}

self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL")
self.prefix = self.config.get("prefix") or kwargs.get("prefix", "[VIRTUAL-PRINTER]")
self.pretty = bool(self.config.get("pretty", True))

print(f"{self.prefix} created: id={self.device_id}, port={self.port}")

def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node

async def initialize(self) -> bool:
self.data.update(
{
"status": "Idle",
"message": "Ready",
"last_received": None,
"received_count": 0,
}
)
self.logger.info("Initialized")
return True

async def cleanup(self) -> bool:
self.data.update({"status": "Offline", "message": "System offline"})
self.logger.info("Cleaned up")
return False

async def print_message(self, content: Any = None, **kwargs) -> Dict[str, Any]:
"""打印虚拟设备接收到的内容(推荐 action)"""
await self._record_and_print(action="print_message", content=content, kwargs=kwargs)
return {"success": True, "message": "printed", "return_info": "printed"}

async def receive(self, *args, **kwargs) -> Dict[str, Any]:
payload = {"args": list(args), "kwargs": kwargs}
await self._record_and_print(action="receive", content=payload, kwargs={})
return {"success": True, "message": "received", "return_info": "received"}

async def _record_and_print(self, action: str, content: Any, kwargs: Dict[str, Any]) -> None:
ts = datetime.now().isoformat(timespec="seconds")
record = {
"timestamp": ts,
"device_id": self.device_id,
"action": action,
"content": content,
"kwargs": kwargs,
}

self.data["last_received"] = record
self.data["received_count"] = int(self.data.get("received_count", 0)) + 1
self.data["status"] = "Idle"
self.data["message"] = f"Last action: {action} @ {ts}"

if self.pretty:
try:
txt = json.dumps(record, ensure_ascii=False, indent=2, default=str)
except Exception:
txt = str(record)
else:
txt = str(record)

print(f"{self.prefix} received:\n{txt}")
self.logger.info("Received: %s", record)

@property
def status(self) -> str:
return self.data.get("status", "Unknown")

@property
def message(self) -> str:
return self.data.get("message", "")

@property
def last_received(self) -> Any:
return self.data.get("last_received")

@property
def received_count(self) -> int:
return int(self.data.get("received_count", 0))
11 changes: 7 additions & 4 deletions unilabos/registry/devices/laiyu_liquid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,8 @@ laiyu_liquid:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
Expand Down Expand Up @@ -1491,9 +1492,11 @@ laiyu_liquid:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
Expand Down
33 changes: 21 additions & 12 deletions unilabos/registry/devices/liquid_handler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4019,7 +4019,8 @@ liquid_handler:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
Comment on lines +4022 to 4026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk):mix_times 从整数改为整型数组,可能会破坏那些期望标量值的现有使用方。

由于 schema/默认值现在变成了整型数组([0]),请确认所有调用点、序列化逻辑以及任何外部集成(工作流、UI、设备驱动)都能正确处理数组形式以及任何遗留的标量值,或者为使用方文档化一个清晰的迁移路径。

Original comment in English

issue (bug_risk): Changing mix_times from an integer to an integer array may break existing consumers expecting a scalar.

Since the schema/default is now an integer array ([0]), please verify that all call sites, serializers, and any external integrations (workflows, UIs, device drivers) correctly handle the array form and any legacy scalar values, or document a clear migration path for consumers.

Expand Down Expand Up @@ -4175,9 +4176,11 @@ liquid_handler:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
Expand Down Expand Up @@ -5037,7 +5040,8 @@ liquid_handler.biomek:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
Expand Down Expand Up @@ -5180,9 +5184,11 @@ liquid_handler.biomek:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
Expand Down Expand Up @@ -9261,7 +9267,8 @@ liquid_handler.prcxi:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
Expand Down Expand Up @@ -9417,9 +9424,11 @@ liquid_handler.prcxi:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
Expand Down
Loading
Loading