From 961752fb0d01e355799ba7c9f02726c40fe48d90 Mon Sep 17 00:00:00 2001
From: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Date: Sun, 7 Sep 2025 00:43:23 +0800
Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0schema=E7=9A=84title?=
=?UTF-8?q?=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
unilabos/registry/devices/liquid_handler.yaml | 398 +++++++++---------
.../devices/organic_miscellaneous.yaml | 8 +-
unilabos/registry/devices/robot_gripper.yaml | 2 +-
.../registry/devices/robot_linear_motion.yaml | 36 +-
unilabos/registry/devices/temperature.yaml | 8 +-
unilabos/registry/devices/virtual_device.yaml | 170 ++++----
unilabos/registry/devices/work_station.yaml | 396 +++++++++--------
unilabos/registry/registry.py | 9 +-
.../resources/organic/workstation.yaml | 24 +-
unilabos/ros/msgs/message_converter.py | 50 ++-
10 files changed, 572 insertions(+), 529 deletions(-)
diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml
index df38dbb..8c3543c 100644
--- a/unilabos/registry/devices/liquid_handler.yaml
+++ b/unilabos/registry/devices/liquid_handler.yaml
@@ -155,7 +155,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
reagent_sources:
@@ -194,7 +194,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -208,12 +208,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -230,7 +230,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: reagent_sources
type: object
type: array
spread:
@@ -271,7 +271,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -285,12 +285,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -307,7 +307,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
use_channels:
@@ -436,7 +436,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
resources:
@@ -475,7 +475,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -489,12 +489,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -511,7 +511,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: resources
type: object
type: array
spread:
@@ -826,7 +826,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
resources:
@@ -865,7 +865,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -879,12 +879,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -901,7 +901,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: resources
type: object
type: array
spread:
@@ -1007,7 +1007,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
tip_spots:
@@ -1046,7 +1046,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1060,12 +1060,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1082,7 +1082,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: tip_spots
type: object
type: array
use_channels:
@@ -1173,7 +1173,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offset
type: object
tip_rack:
properties:
@@ -1210,7 +1210,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1224,12 +1224,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1246,7 +1246,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: tip_rack
type: object
required:
- tip_rack
@@ -1352,7 +1352,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
targets:
@@ -1391,7 +1391,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1405,12 +1405,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1427,7 +1427,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
required:
@@ -1552,7 +1552,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: destination_offset
type: object
drop_direction:
type: string
@@ -1571,7 +1571,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: intermediate_locations
type: object
type: array
lid:
@@ -1609,7 +1609,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1623,12 +1623,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1645,7 +1645,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: lid
type: object
pickup_direction:
type: string
@@ -1665,7 +1665,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: resource_offset
type: object
to:
properties:
@@ -1702,7 +1702,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1716,12 +1716,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1738,7 +1738,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: to
type: object
required:
- lid
@@ -1869,7 +1869,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: destination_offset
type: object
drop_direction:
type: string
@@ -1888,7 +1888,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: intermediate_locations
type: object
type: array
pickup_direction:
@@ -1907,7 +1907,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: pickup_offset
type: object
plate:
properties:
@@ -1944,7 +1944,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1958,12 +1958,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1980,7 +1980,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: plate
type: object
put_direction:
type: string
@@ -1996,7 +1996,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: resource_offset
type: object
to:
properties:
@@ -2033,7 +2033,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2047,12 +2047,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2069,7 +2069,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: to
type: object
required:
- plate
@@ -2181,7 +2181,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: destination_offset
type: object
drop_direction:
type: string
@@ -2200,7 +2200,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: intermediate_locations
type: object
type: array
pickup_direction:
@@ -2244,7 +2244,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2258,12 +2258,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2280,7 +2280,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: resource
type: object
resource_offset:
properties:
@@ -2294,7 +2294,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: resource_offset
type: object
to:
properties:
@@ -2308,7 +2308,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: to
type: object
required:
- resource
@@ -2421,7 +2421,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2435,12 +2435,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2457,7 +2457,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: well
type: object
required:
- well
@@ -2540,7 +2540,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
tip_spots:
@@ -2579,7 +2579,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2593,12 +2593,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2615,7 +2615,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: tip_spots
type: object
type: array
use_channels:
@@ -2701,7 +2701,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offset
type: object
tip_rack:
properties:
@@ -2738,7 +2738,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2752,12 +2752,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2774,7 +2774,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: tip_rack
type: object
required:
- tip_rack
@@ -2925,7 +2925,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
sources:
@@ -2964,7 +2964,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2978,12 +2978,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3000,7 +3000,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: sources
type: object
type: array
spread:
@@ -3054,7 +3054,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3068,12 +3068,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3090,7 +3090,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: waste_liquid
type: object
required:
- vols
@@ -3255,7 +3255,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
sources:
@@ -3294,7 +3294,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3308,12 +3308,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3330,7 +3330,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: sources
type: object
type: array
spread:
@@ -3384,7 +3384,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3398,12 +3398,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3420,7 +3420,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: waste_liquid
type: object
required:
- vols
@@ -3650,7 +3650,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3664,12 +3664,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3686,7 +3686,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: source
type: object
target:
properties:
@@ -3723,7 +3723,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3737,12 +3737,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3759,7 +3759,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: target
type: object
volume:
type: number
@@ -4107,7 +4107,7 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
sources:
@@ -4146,7 +4146,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4160,12 +4160,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4182,7 +4182,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: sources
type: object
type: array
spread:
@@ -4223,7 +4223,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4237,12 +4237,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4259,7 +4259,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
tip_racks:
@@ -4298,7 +4298,7 @@ liquid_handler:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4312,12 +4312,12 @@ liquid_handler:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4334,7 +4334,7 @@ liquid_handler:
- pose
- config
- data
- title: Resource
+ title: tip_racks
type: object
type: array
touch_tip:
@@ -5099,7 +5099,7 @@ liquid_handler.biomek:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
sources:
@@ -5138,7 +5138,7 @@ liquid_handler.biomek:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5152,12 +5152,12 @@ liquid_handler.biomek:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5174,7 +5174,7 @@ liquid_handler.biomek:
- pose
- config
- data
- title: Resource
+ title: sources
type: object
type: array
spread:
@@ -5215,7 +5215,7 @@ liquid_handler.biomek:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5229,12 +5229,12 @@ liquid_handler.biomek:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5251,7 +5251,7 @@ liquid_handler.biomek:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
tip_racks:
@@ -5290,7 +5290,7 @@ liquid_handler.biomek:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5304,12 +5304,12 @@ liquid_handler.biomek:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5326,7 +5326,7 @@ liquid_handler.biomek:
- pose
- config
- data
- title: Resource
+ title: tip_racks
type: object
type: array
touch_tip:
@@ -5555,7 +5555,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
reagent_sources:
@@ -5594,7 +5594,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5608,12 +5608,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5630,7 +5630,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: reagent_sources
type: object
type: array
spread:
@@ -5671,7 +5671,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5685,12 +5685,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5707,7 +5707,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
use_channels:
@@ -5837,7 +5837,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
resources:
@@ -5876,7 +5876,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5890,12 +5890,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5912,7 +5912,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: resources
type: object
type: array
spread:
@@ -6255,7 +6255,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
resources:
@@ -6294,7 +6294,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -6308,12 +6308,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -6330,7 +6330,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: resources
type: object
type: array
spread:
@@ -6435,7 +6435,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
tip_spots:
@@ -6474,7 +6474,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -6488,12 +6488,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -6510,7 +6510,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: tip_spots
type: object
type: array
use_channels:
@@ -6626,7 +6626,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
targets:
@@ -6665,7 +6665,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -6679,12 +6679,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -6701,7 +6701,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
required:
@@ -6790,7 +6790,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
tip_spots:
@@ -6829,7 +6829,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -6843,12 +6843,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -6865,7 +6865,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: tip_spots
type: object
type: array
use_channels:
@@ -7027,7 +7027,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
sources:
@@ -7066,7 +7066,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -7080,12 +7080,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -7102,7 +7102,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: sources
type: object
type: array
spread:
@@ -7156,7 +7156,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -7170,12 +7170,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -7192,7 +7192,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: waste_liquid
type: object
required:
- vols
@@ -7315,7 +7315,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -7329,12 +7329,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -7351,7 +7351,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: wells
type: object
type: array
required:
@@ -7448,7 +7448,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -7462,12 +7462,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -7484,7 +7484,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: tip_racks
type: object
type: array
required:
@@ -7800,7 +7800,7 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: offsets
type: object
type: array
sources:
@@ -7839,7 +7839,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -7853,12 +7853,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -7875,7 +7875,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: sources
type: object
type: array
spread:
@@ -7916,7 +7916,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -7930,12 +7930,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -7952,7 +7952,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: targets
type: object
type: array
tip_racks:
@@ -7991,7 +7991,7 @@ liquid_handler.prcxi:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -8005,12 +8005,12 @@ liquid_handler.prcxi:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -8027,7 +8027,7 @@ liquid_handler.prcxi:
- pose
- config
- data
- title: Resource
+ title: tip_racks
type: object
type: array
touch_tip:
@@ -8220,7 +8220,7 @@ liquid_handler.revvity:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -8234,12 +8234,12 @@ liquid_handler.revvity:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -8256,7 +8256,7 @@ liquid_handler.revvity:
- pose
- config
- data
- title: Resource
+ title: resource
type: object
wf_name:
type: string
diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml
index 1ac2ac2..025fad5 100644
--- a/unilabos/registry/devices/organic_miscellaneous.yaml
+++ b/unilabos/registry/devices/organic_miscellaneous.yaml
@@ -336,7 +336,7 @@ separator.homemade:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -350,12 +350,12 @@ separator.homemade:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -372,7 +372,7 @@ separator.homemade:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml
index f3ee141..0e79395 100644
--- a/unilabos/registry/devices/robot_gripper.yaml
+++ b/unilabos/registry/devices/robot_gripper.yaml
@@ -533,7 +533,7 @@ gripper.mock:
required:
- position
- max_effort
- title: GripperCommand
+ title: command
type: object
required:
- command
diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml
index 4cc4eaf..1aeb861 100644
--- a/unilabos/registry/devices/robot_linear_motion.yaml
+++ b/unilabos/registry/devices/robot_linear_motion.yaml
@@ -139,12 +139,12 @@ linear_motion.grbl:
required:
- sec
- nanosec
- title: Time
+ title: stamp
type: object
required:
- stamp
- frame_id
- title: Header
+ title: header
type: object
pose:
properties:
@@ -163,7 +163,7 @@ linear_motion.grbl:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -177,17 +177,17 @@ linear_motion.grbl:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
required:
- header
- pose
- title: PoseStamped
+ title: current_pose
type: object
distance_remaining:
type: number
@@ -204,7 +204,7 @@ linear_motion.grbl:
required:
- sec
- nanosec
- title: Duration
+ title: estimated_time_remaining
type: object
navigation_time:
properties:
@@ -219,7 +219,7 @@ linear_motion.grbl:
required:
- sec
- nanosec
- title: Duration
+ title: navigation_time
type: object
number_of_poses_remaining:
maximum: 32767
@@ -262,12 +262,12 @@ linear_motion.grbl:
required:
- sec
- nanosec
- title: Time
+ title: stamp
type: object
required:
- stamp
- frame_id
- title: Header
+ title: header
type: object
pose:
properties:
@@ -286,7 +286,7 @@ linear_motion.grbl:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -300,17 +300,17 @@ linear_motion.grbl:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
required:
- header
- pose
- title: PoseStamped
+ title: poses
type: object
type: array
required:
@@ -323,7 +323,7 @@ linear_motion.grbl:
result:
properties: {}
required: []
- title: Empty
+ title: result
type: object
required:
- result
@@ -371,12 +371,12 @@ linear_motion.grbl:
required:
- sec
- nanosec
- title: Time
+ title: stamp
type: object
required:
- stamp
- frame_id
- title: Header
+ title: header
type: object
position:
type: number
@@ -406,7 +406,7 @@ linear_motion.grbl:
required:
- sec
- nanosec
- title: Duration
+ title: min_duration
type: object
position:
type: number
diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml
index f83d921..bb717b5 100644
--- a/unilabos/registry/devices/temperature.yaml
+++ b/unilabos/registry/devices/temperature.yaml
@@ -362,7 +362,7 @@ heaterstirrer.dalong:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -376,12 +376,12 @@ heaterstirrer.dalong:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -398,7 +398,7 @@ heaterstirrer.dalong:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml
index d97d625..f94bdad 100644
--- a/unilabos/registry/devices/virtual_device.yaml
+++ b/unilabos/registry/devices/virtual_device.yaml
@@ -145,7 +145,7 @@ virtual_centrifuge:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -159,12 +159,12 @@ virtual_centrifuge:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -181,7 +181,7 @@ virtual_centrifuge:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -446,7 +446,7 @@ virtual_column:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -460,12 +460,12 @@ virtual_column:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -482,7 +482,7 @@ virtual_column:
- pose
- config
- data
- title: Resource
+ title: from_vessel
type: object
pct1:
type: string
@@ -531,7 +531,7 @@ virtual_column:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -545,12 +545,12 @@ virtual_column:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -567,7 +567,7 @@ virtual_column:
- pose
- config
- data
- title: Resource
+ title: to_vessel
type: object
required:
- from_vessel
@@ -850,7 +850,7 @@ virtual_filter:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -864,12 +864,12 @@ virtual_filter:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -886,7 +886,7 @@ virtual_filter:
- pose
- config
- data
- title: Resource
+ title: filtrate_vessel
type: object
stir:
type: boolean
@@ -929,7 +929,7 @@ virtual_filter:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -943,12 +943,12 @@ virtual_filter:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -965,7 +965,7 @@ virtual_filter:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: number
@@ -1071,8 +1071,8 @@ virtual_filter:
- status
- progress
- current_temp
- - filtered_volume
- current_status
+ - filtered_volume
- message
- max_temp
- max_stir_speed
@@ -1455,7 +1455,7 @@ virtual_heatchill:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1469,12 +1469,12 @@ virtual_heatchill:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1491,7 +1491,7 @@ virtual_heatchill:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -1610,7 +1610,7 @@ virtual_heatchill:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1624,12 +1624,12 @@ virtual_heatchill:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1646,7 +1646,7 @@ virtual_heatchill:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -1747,7 +1747,7 @@ virtual_heatchill:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1761,12 +1761,12 @@ virtual_heatchill:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1783,7 +1783,7 @@ virtual_heatchill:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -2397,7 +2397,7 @@ virtual_rotavap:
required:
- sec
- nanosec
- title: Duration
+ title: time_remaining
type: object
time_spent:
properties:
@@ -2412,7 +2412,7 @@ virtual_rotavap:
required:
- sec
- nanosec
- title: Duration
+ title: time_spent
type: object
required:
- status
@@ -2468,7 +2468,7 @@ virtual_rotavap:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2482,12 +2482,12 @@ virtual_rotavap:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2504,7 +2504,7 @@ virtual_rotavap:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -2889,7 +2889,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2903,12 +2903,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2925,7 +2925,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: from_vessel
type: object
product_phase:
type: string
@@ -2964,7 +2964,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2978,12 +2978,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3000,7 +3000,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: product_vessel
type: object
purpose:
type: string
@@ -3043,7 +3043,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3057,12 +3057,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3079,7 +3079,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: separation_vessel
type: object
settling_time:
type: number
@@ -3128,7 +3128,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3142,12 +3142,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3164,7 +3164,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: to_vessel
type: object
vessel:
properties:
@@ -3201,7 +3201,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3215,12 +3215,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3237,7 +3237,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: string
@@ -3276,7 +3276,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3290,12 +3290,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3312,7 +3312,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: waste_phase_to_vessel
type: object
waste_vessel:
properties:
@@ -3349,7 +3349,7 @@ virtual_separator:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3363,12 +3363,12 @@ virtual_separator:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3385,7 +3385,7 @@ virtual_separator:
- pose
- config
- data
- title: Resource
+ title: waste_vessel
type: object
required:
- vessel
@@ -3951,7 +3951,7 @@ virtual_solid_dispenser:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3965,12 +3965,12 @@ virtual_solid_dispenser:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3987,7 +3987,7 @@ virtual_solid_dispenser:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
viscous:
type: boolean
@@ -4332,7 +4332,7 @@ virtual_stirrer:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4346,12 +4346,12 @@ virtual_stirrer:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4368,7 +4368,7 @@ virtual_stirrer:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -4492,7 +4492,7 @@ virtual_stirrer:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4506,12 +4506,12 @@ virtual_stirrer:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4528,7 +4528,7 @@ virtual_stirrer:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -4639,7 +4639,7 @@ virtual_stirrer:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4653,12 +4653,12 @@ virtual_stirrer:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4675,7 +4675,7 @@ virtual_stirrer:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -4704,6 +4704,7 @@ virtual_stirrer:
status_types:
current_speed: float
current_vessel: str
+ device_info: dict
is_stirring: bool
max_speed: float
min_speed: float
@@ -4738,6 +4739,8 @@ virtual_stirrer:
type: number
current_vessel:
type: string
+ device_info:
+ type: object
is_stirring:
type: boolean
max_speed:
@@ -4759,6 +4762,7 @@ virtual_stirrer:
- remaining_time
- max_speed
- min_speed
+ - device_info
type: object
version: 1.0.0
virtual_transfer_pump:
diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml
index c2f0248..5f20f45 100644
--- a/unilabos/registry/devices/work_station.yaml
+++ b/unilabos/registry/devices/work_station.yaml
@@ -103,7 +103,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -117,12 +117,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -139,7 +139,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: from_repo
type: object
from_repo_position:
type: string
@@ -178,7 +178,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -192,12 +192,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -214,7 +214,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: to_repo
type: object
to_repo_position:
type: string
@@ -390,7 +390,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -404,12 +404,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -426,7 +426,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
viscous:
type: boolean
@@ -579,7 +579,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -593,12 +593,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -615,7 +615,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -752,7 +752,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -766,12 +766,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -788,7 +788,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -892,7 +892,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_remaining
type: object
time_spent:
properties:
@@ -907,7 +907,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_spent
type: object
required:
- status
@@ -961,7 +961,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -975,12 +975,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -997,7 +997,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: number
@@ -1138,7 +1138,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1152,12 +1152,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1174,7 +1174,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: number
@@ -1345,7 +1345,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1359,12 +1359,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1381,7 +1381,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: string
@@ -1514,7 +1514,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1528,12 +1528,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1550,7 +1550,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- compound
@@ -1641,7 +1641,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_remaining
type: object
time_spent:
properties:
@@ -1656,7 +1656,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_spent
type: object
required:
- status
@@ -1704,7 +1704,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1718,12 +1718,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1740,7 +1740,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -1841,7 +1841,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_remaining
type: object
time_spent:
properties:
@@ -1856,7 +1856,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_spent
type: object
required:
- status
@@ -1912,7 +1912,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -1926,12 +1926,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -1948,7 +1948,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -2117,7 +2117,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2131,12 +2131,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2153,7 +2153,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: filtrate_vessel
type: object
stir:
type: boolean
@@ -2196,7 +2196,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2210,12 +2210,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2232,7 +2232,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: number
@@ -2431,7 +2431,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2445,12 +2445,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2467,7 +2467,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: filter_through
type: object
from_vessel:
properties:
@@ -2504,7 +2504,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2518,12 +2518,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2540,7 +2540,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: from_vessel
type: object
residence_time:
type: number
@@ -2579,7 +2579,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2593,12 +2593,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2615,7 +2615,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: to_vessel
type: object
required:
- from_vessel
@@ -2771,7 +2771,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2785,12 +2785,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2807,7 +2807,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -2938,7 +2938,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -2952,12 +2952,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -2974,7 +2974,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -3087,7 +3087,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3101,12 +3101,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3123,7 +3123,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -3245,7 +3245,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3259,12 +3259,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3281,7 +3281,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- temp
@@ -3434,7 +3434,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_remaining
type: object
time_spent:
properties:
@@ -3449,7 +3449,7 @@ workstation:
required:
- sec
- nanosec
- title: Duration
+ title: time_spent
type: object
required:
- status
@@ -3501,7 +3501,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3515,12 +3515,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3537,7 +3537,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: from_vessel
type: object
rate_spec:
type: string
@@ -3590,7 +3590,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3604,12 +3604,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3626,7 +3626,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: to_vessel
type: object
transfer_flowrate:
type: number
@@ -3784,7 +3784,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3798,12 +3798,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3820,7 +3820,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: string
@@ -3939,7 +3939,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -3953,12 +3953,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -3975,7 +3975,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- solvent
@@ -4136,7 +4136,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4150,12 +4150,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4172,7 +4172,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: from_vessel
type: object
pct1:
type: string
@@ -4221,7 +4221,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4235,12 +4235,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4257,7 +4257,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: to_vessel
type: object
required:
- from_vessel
@@ -4542,7 +4542,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4556,12 +4556,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4578,7 +4578,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: from_vessel
type: object
product_phase:
type: string
@@ -4617,7 +4617,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4631,12 +4631,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4653,7 +4653,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: product_vessel
type: object
purpose:
type: string
@@ -4696,7 +4696,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4710,12 +4710,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4732,7 +4732,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: separation_vessel
type: object
settling_time:
type: number
@@ -4781,7 +4781,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4795,12 +4795,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4817,7 +4817,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: to_vessel
type: object
vessel:
properties:
@@ -4854,7 +4854,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4868,12 +4868,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4890,7 +4890,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: string
@@ -4929,7 +4929,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -4943,12 +4943,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -4965,7 +4965,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: waste_phase_to_vessel
type: object
waste_vessel:
properties:
@@ -5002,7 +5002,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5016,12 +5016,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5038,7 +5038,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: waste_vessel
type: object
required:
- vessel
@@ -5182,7 +5182,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5196,12 +5196,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5218,7 +5218,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -5358,7 +5358,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5372,12 +5372,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5394,7 +5394,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -5517,7 +5517,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5531,12 +5531,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5553,7 +5553,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
required:
- vessel
@@ -5856,7 +5856,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5870,12 +5870,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5892,7 +5892,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: filtrate_vessel
type: object
mass:
type: string
@@ -5947,7 +5947,7 @@ workstation:
- y
- z
- w
- title: Quaternion
+ title: orientation
type: object
position:
properties:
@@ -5961,12 +5961,12 @@ workstation:
- x
- y
- z
- title: Point
+ title: position
type: object
required:
- position
- orientation
- title: Pose
+ title: pose
type: object
sample_id:
type: string
@@ -5983,7 +5983,7 @@ workstation:
- pose
- config
- data
- title: Resource
+ title: vessel
type: object
volume:
type: string
@@ -6146,26 +6146,6 @@ workstation.example:
- work_station
class:
action_value_mappings:
- auto-append_resource:
- feedback: {}
- goal: {}
- goal_default: {}
- handles: {}
- result: {}
- schema:
- description: ''
- properties:
- feedback: {}
- goal:
- properties: {}
- required: []
- type: object
- result: {}
- required:
- - goal
- title: append_resource参数
- type: object
- type: UniLabJsonCommand
auto-create_resource:
feedback: {}
goal: {}
@@ -6218,6 +6198,62 @@ workstation.example:
title: create_resource参数
type: object
type: UniLabJsonCommand
+ auto-transfer_bottle:
+ feedback: {}
+ goal: {}
+ goal_default:
+ base_plate: null
+ tip_rack: null
+ handles: {}
+ result: {}
+ schema:
+ description: ''
+ properties:
+ feedback: {}
+ goal:
+ properties:
+ base_plate:
+ type: string
+ tip_rack:
+ type: string
+ required:
+ - tip_rack
+ - base_plate
+ type: object
+ result: {}
+ required:
+ - goal
+ title: transfer_bottle参数
+ type: object
+ type: UniLabJsonCommand
+ auto-trigger_resource_update:
+ feedback: {}
+ goal: {}
+ goal_default:
+ from_plate: null
+ to_base_plate: null
+ handles: {}
+ result: {}
+ schema:
+ description: ''
+ properties:
+ feedback: {}
+ goal:
+ properties:
+ from_plate:
+ type: string
+ to_base_plate:
+ type: string
+ required:
+ - from_plate
+ - to_base_plate
+ type: object
+ result: {}
+ required:
+ - goal
+ title: trigger_resource_update参数
+ type: object
+ type: UniLabJsonCommand
module: unilabos.ros.nodes.presets.workstation:WorkStationExample
status_types: {}
type: ros2
diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py
index 3cb2937..7ca71ea 100644
--- a/unilabos/registry/registry.py
+++ b/unilabos/registry/registry.py
@@ -24,12 +24,11 @@ DEFAULT_PATHS = [Path(__file__).absolute().parent]
class Registry:
def __init__(self, registry_paths=None):
import ctypes
+
try:
import unilabos_msgs
except ImportError:
- logger.error(
- "[UniLab Registry] unilabos_msgs模块未找到,请确保已根据官方文档安装unilabos_msgs包。"
- )
+ logger.error("[UniLab Registry] unilabos_msgs模块未找到,请确保已根据官方文档安装unilabos_msgs包。")
sys.exit(1)
try:
ctypes.CDLL(str(Path(unilabos_msgs.__file__).parent / "unilabos_msgs_s__rosidl_typesupport_c.pyd"))
@@ -219,7 +218,7 @@ class Registry:
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
self.resource_type_registry.update(data)
- logger.trace(
+ logger.trace( # type: ignore
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
+ f"Add {list(data.keys())}"
)
@@ -406,7 +405,7 @@ class Registry:
devices_path = abs_path / "devices"
device_comms_path = abs_path / "device_comms"
files = list(devices_path.glob("*.yaml")) + list(device_comms_path.glob("*.yaml"))
- logger.trace(
+ logger.trace( # type: ignore
f"[UniLab Registry] devices: {devices_path.exists()}, device_comms: {device_comms_path.exists()}, "
+ f"total: {len(files)}"
)
diff --git a/unilabos/registry/resources/organic/workstation.yaml b/unilabos/registry/resources/organic/workstation.yaml
index 440f06c..5250dfc 100644
--- a/unilabos/registry/resources/organic/workstation.yaml
+++ b/unilabos/registry/resources/organic/workstation.yaml
@@ -1,12 +1,12 @@
-get_workstation_plate_resource:
- category:
- - workstation
- class:
- module: unilabos.ros.nodes.presets.workstation:get_workstation_plate_resource
- type: pylabrobot
- description: workstation example resource
- handles: []
- icon: ''
- init_param_schema: {}
- registry_type: resource
- version: 1.0.0
+#get_workstation_plate_resource:
+# category:
+# - workstation
+# class:
+# module: unilabos.devices.workstation.workstation_base:get_workstation_plate_resource
+# type: pylabrobot
+# description: workstation example resource
+# handles: []
+# icon: ''
+# init_param_schema: {}
+# registry_type: resource
+# version: 1.0.0
diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py
index b0716aa..ae8d80e 100644
--- a/unilabos/ros/msgs/message_converter.py
+++ b/unilabos/ros/msgs/message_converter.py
@@ -508,7 +508,7 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
Python字典
"""
data: Dict[str, Any] = {}
-
+
# # 🔧 添加调试信息
# print(f"🔍 convert_from_ros_msg_with_mapping 开始")
# print(f"🔍 ros_msg 类型: {type(ros_msg)}")
@@ -517,14 +517,14 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
# print("-" * 60)
for msg_name, attr_name in value_mapping.items():
- # print(f"🔍 处理映射: {msg_name} -> {attr_name}")
-
+ # print(f"🔍 处理映射: {msg_name} -> {attr_name}")
+
msg_path = msg_name.split(".")
current = ros_msg
-
+
# print(f"🔍 msg_path: {msg_path}")
# print(f"🔍 current 初始值: {current} (类型: {type(current)})")
-
+
try:
if not attr_name.endswith("[]"):
# 处理单值映射
@@ -537,7 +537,7 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
else:
# print(f"❌ 属性 '{name}' 不存在于 {type(current)}")
break
-
+
converted_value = convert_from_ros_msg(current)
# print(f"🔍 转换后的值: {converted_value} (类型: {type(converted_value)})")
data[attr_name] = converted_value
@@ -585,13 +585,13 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
# print(f"❌ 映射转换错误 {msg_name} -> {attr_name}: {e}")
logger.debug(f"Mapping conversion error for {msg_name} -> {attr_name}")
continue
-
+
# print(f"🔍 当前 data 状态: {data}")
# print("-" * 40)
- #print(f"🔍 convert_from_ros_msg_with_mapping 结束")
- #print(f"🔍 最终 data: {data}")
- #print("=" * 60)
+ # print(f"🔍 convert_from_ros_msg_with_mapping 结束")
+ # print(f"🔍 最终 data: {data}")
+ # print("=" * 60)
return data
@@ -646,25 +646,28 @@ basic_type_map = {
}
-def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None) -> Dict[str, Any]:
+def ros_field_type_to_json_schema(
+ type_info: Type | str, field_name: str
+) -> Dict[str, Any]:
"""
将 ROS 字段类型转换为 JSON Schema 类型定义
Args:
type_info: ROS 类型
slot_type: ROS 类型
+ field_name: 字段名,用于设置复杂类型的title
Returns:
对应的 JSON Schema 类型定义
"""
if isinstance(type_info, UnboundedSequence):
- return {"type": "array", "items": ros_field_type_to_json_schema(type_info.value_type)}
+ return {"type": "array", "items": ros_field_type_to_json_schema(type_info.value_type, field_name)} # type: ignore
if isinstance(type_info, NamespacedType):
cls_name = ".".join(type_info.namespaces) + ":" + type_info.name
type_class = msg_converter_manager.get_class(cls_name)
- return ros_field_type_to_json_schema(type_class)
+ return ros_field_type_to_json_schema(type_class, field_name)
elif isinstance(type_info, BasicType):
- return ros_field_type_to_json_schema(type_info.typename)
+ return ros_field_type_to_json_schema(type_info.typename, field_name)
elif isinstance(type_info, UnboundedString):
return basic_type_map["string"]
elif isinstance(type_info, str):
@@ -681,8 +684,9 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None)
},
"required": ["sec", "nanosec"],
}
+ return {}
else:
- return ros_message_to_json_schema(type_info)
+ return ros_message_to_json_schema(type_info, field_name)
# # 处理数组类型
# if field_type.endswith('[]'):
# item_type = field_type[:-2]
@@ -706,28 +710,28 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None)
# return {'type': 'object', 'description': f'未知类型: {field_type}'}
-def ros_message_to_json_schema(msg_class: Any) -> Dict[str, Any]:
+def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any]:
"""
将 ROS 消息类转换为 JSON Schema
Args:
msg_class: ROS 消息类
+ field_name: 字段名,用于设置schema的title,如果为None则使用类名
Returns:
对应的 JSON Schema 定义
"""
schema = {"type": "object", "properties": {}, "required": []}
- # 获取类名作为标题
- if hasattr(msg_class, "__name__"):
- schema["title"] = msg_class.__name__
+ # 优先使用字段名作为标题,否则使用类名
+ schema["title"] = field_name
# 获取消息的字段和字段类型
try:
for ind, slot_info in enumerate(msg_class._fields_and_field_types.items()):
slot_name, slot_type = slot_info
type_info = msg_class.SLOT_TYPES[ind]
- field_schema = ros_field_type_to_json_schema(type_info, slot_type)
+ field_schema = ros_field_type_to_json_schema(type_info, slot_name)
schema["properties"][slot_name] = field_schema
schema["required"].append(slot_name)
# if hasattr(msg_class, 'get_fields_and_field_types'):
@@ -786,15 +790,15 @@ def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, An
"properties": {
"goal": {
# 'description': 'Action 目标 - 从客户端发送到服务器',
- **ros_message_to_json_schema(action_class.Goal)
+ **ros_message_to_json_schema(action_class.Goal, action_class.Goal.__name__)
},
"feedback": {
# 'description': 'Action 反馈 - 执行过程中从服务器发送到客户端',
- **ros_message_to_json_schema(action_class.Feedback)
+ **ros_message_to_json_schema(action_class.Feedback, action_class.Feedback.__name__)
},
"result": {
# 'description': 'Action 结果 - 完成后从服务器发送到客户端',
- **ros_message_to_json_schema(action_class.Result)
+ **ros_message_to_json_schema(action_class.Result, action_class.Result.__name__)
},
},
"required": ["goal"],
From c25283ae04a200e0a03999c8d56e2406fbdcbaca Mon Sep 17 00:00:00 2001
From: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Date: Sun, 7 Sep 2025 12:53:00 +0800
Subject: [PATCH 2/3] =?UTF-8?q?=E4=B8=BB=E6=9C=BA=E8=8A=82=E7=82=B9?=
=?UTF-8?q?=E4=BF=A1=E6=81=AF=E7=AD=89=E6=94=AF=E6=8C=81=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E5=88=B7=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
example_devices.py | 588 +++
unilabos/app/web/api.py | 1082 +++++-
unilabos/app/web/pages.py | 33 +-
unilabos/app/web/templates/base.html | 1 -
unilabos/app/web/templates/home.html | 45 +-
.../app/web/templates/registry_editor.html | 1085 ++++++
unilabos/app/web/templates/status.html | 3450 +++++++++++------
7 files changed, 5015 insertions(+), 1269 deletions(-)
create mode 100644 example_devices.py
create mode 100644 unilabos/app/web/templates/registry_editor.html
diff --git a/example_devices.py b/example_devices.py
new file mode 100644
index 0000000..d5b26b2
--- /dev/null
+++ b/example_devices.py
@@ -0,0 +1,588 @@
+"""
+示例设备类文件,用于测试注册表编辑器
+"""
+
+import asyncio
+from typing import Dict, Any, Optional, List
+
+
+class SmartPumpController:
+ """
+ 智能泵控制器
+
+ 支持多种泵送模式,具有高精度流量控制和自动校准功能。
+ 适用于实验室自动化系统中的液体处理任务。
+ """
+
+ def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
+ """
+ 初始化智能泵控制器
+
+ Args:
+ device_id: 设备唯一标识符
+ port: 通信端口
+ """
+ self.device_id = device_id
+ self.port = port
+ self.is_connected = False
+ self.current_flow_rate = 0.0
+ self.total_volume_pumped = 0.0
+ self.calibration_factor = 1.0
+ self.pump_mode = "continuous" # continuous, volume, rate
+
+ def connect_device(self, timeout: int = 10) -> bool:
+ """
+ 连接到泵设备
+
+ Args:
+ timeout: 连接超时时间(秒)
+
+ Returns:
+ bool: 连接是否成功
+ """
+ # 模拟连接过程
+ self.is_connected = True
+ return True
+
+ def disconnect_device(self) -> bool:
+ """
+ 断开设备连接
+
+ Returns:
+ bool: 断开连接是否成功
+ """
+ self.is_connected = False
+ self.current_flow_rate = 0.0
+ return True
+
+ def set_flow_rate(self, flow_rate: float, units: str = "ml/min") -> bool:
+ """
+ 设置泵流速
+
+ Args:
+ flow_rate: 流速值
+ units: 流速单位
+
+ Returns:
+ bool: 设置是否成功
+ """
+ if not self.is_connected:
+ return False
+
+ self.current_flow_rate = flow_rate
+ return True
+
+ async def pump_volume_async(self, volume: float, flow_rate: float) -> Dict[str, Any]:
+ """
+ 异步泵送指定体积的液体
+
+ Args:
+ volume: 目标体积 (mL)
+ flow_rate: 泵送流速 (mL/min)
+
+ Returns:
+ Dict: 包含操作结果的字典
+ """
+ if not self.is_connected:
+ return {"success": False, "error": "设备未连接"}
+
+ # 计算泵送时间
+ pump_time = (volume / flow_rate) * 60 # 转换为秒
+
+ self.current_flow_rate = flow_rate
+ await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
+
+ self.total_volume_pumped += volume
+ self.current_flow_rate = 0.0
+
+ return {
+ "success": True,
+ "pumped_volume": volume,
+ "actual_time": min(pump_time, 3.0),
+ "total_volume": self.total_volume_pumped,
+ }
+
+ def emergency_stop(self) -> bool:
+ """
+ 紧急停止泵
+
+ Returns:
+ bool: 停止是否成功
+ """
+ self.current_flow_rate = 0.0
+ return True
+
+ def perform_calibration(self, reference_volume: float, measured_volume: float) -> bool:
+ """
+ 执行泵校准
+
+ Args:
+ reference_volume: 参考体积
+ measured_volume: 实际测量体积
+
+ Returns:
+ bool: 校准是否成功
+ """
+ if measured_volume > 0:
+ self.calibration_factor = reference_volume / measured_volume
+ return True
+ return False
+
+ # 状态查询方法
+ def get_connection_status(self) -> str:
+ """获取连接状态"""
+ return "connected" if self.is_connected else "disconnected"
+
+ def get_current_flow_rate(self) -> float:
+ """获取当前流速 (mL/min)"""
+ return self.current_flow_rate
+
+ def get_total_volume(self) -> float:
+ """获取累计泵送体积 (mL)"""
+ return self.total_volume_pumped
+
+ def get_calibration_factor(self) -> float:
+ """获取校准因子"""
+ return self.calibration_factor
+
+ def get_pump_mode(self) -> str:
+ """获取泵送模式"""
+ return self.pump_mode
+
+ def get_device_status(self) -> Dict[str, Any]:
+ """获取设备完整状态信息"""
+ return {
+ "device_id": self.device_id,
+ "connected": self.is_connected,
+ "flow_rate": self.current_flow_rate,
+ "total_volume": self.total_volume_pumped,
+ "calibration_factor": self.calibration_factor,
+ "mode": self.pump_mode,
+ "running": self.current_flow_rate > 0,
+ }
+
+
+class AdvancedTemperatureController:
+ """
+ 高级温度控制器
+
+ 支持PID控制、多点温度监控和程序化温度曲线。
+ 适用于需要精确温度控制的化学反应和材料处理过程。
+ """
+
+ def __init__(self, controller_id: str = "temp_controller_01"):
+ """
+ 初始化温度控制器
+
+ Args:
+ controller_id: 控制器ID
+ """
+ self.controller_id = controller_id
+ self.current_temperature = 25.0
+ self.target_temperature = 25.0
+ self.is_heating = False
+ self.is_cooling = False
+ self.pid_enabled = True
+ self.temperature_history: List[Dict] = []
+
+ def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
+ """
+ 设置目标温度
+
+ Args:
+ temperature: 目标温度 (°C)
+ rate: 升温/降温速率 (°C/min)
+
+ Returns:
+ bool: 设置是否成功
+ """
+ self.target_temperature = temperature
+ return True
+
+ async def heat_to_temperature_async(
+ self, temperature: float, tolerance: float = 0.5, timeout: int = 600
+ ) -> Dict[str, Any]:
+ """
+ 异步加热到指定温度
+
+ Args:
+ temperature: 目标温度 (°C)
+ tolerance: 温度容差 (°C)
+ timeout: 最大等待时间 (秒)
+
+ Returns:
+ Dict: 操作结果
+ """
+ self.target_temperature = temperature
+ start_temp = self.current_temperature
+
+ if temperature > start_temp:
+ self.is_heating = True
+ elif temperature < start_temp:
+ self.is_cooling = True
+
+ # 模拟温度变化过程
+ steps = min(abs(temperature - start_temp) * 2, 20) # 计算步数
+ step_time = min(timeout / steps if steps > 0 else 1, 2.0) # 每步最多2秒
+
+ for step in range(int(steps)):
+ progress = (step + 1) / steps
+ self.current_temperature = start_temp + (temperature - start_temp) * progress
+
+ # 记录温度历史
+ self.temperature_history.append(
+ {
+ "timestamp": asyncio.get_event_loop().time(),
+ "temperature": self.current_temperature,
+ "target": self.target_temperature,
+ }
+ )
+
+ await asyncio.sleep(step_time)
+
+ # 保持历史记录不超过100条
+ if len(self.temperature_history) > 100:
+ self.temperature_history.pop(0)
+
+ # 最终设置为目标温度
+ self.current_temperature = temperature
+ self.is_heating = False
+ self.is_cooling = False
+
+ return {
+ "success": True,
+ "final_temperature": self.current_temperature,
+ "start_temperature": start_temp,
+ "time_taken": steps * step_time,
+ }
+
+ def enable_pid_control(self, kp: float = 1.0, ki: float = 0.1, kd: float = 0.05) -> bool:
+ """
+ 启用PID控制
+
+ Args:
+ kp: 比例增益
+ ki: 积分增益
+ kd: 微分增益
+
+ Returns:
+ bool: 启用是否成功
+ """
+ self.pid_enabled = True
+ return True
+
+ def run_temperature_program(self, program: List[Dict]) -> bool:
+ """
+ 运行温度程序
+
+ Args:
+ program: 温度程序列表,每个元素包含温度和持续时间
+
+ Returns:
+ bool: 程序启动是否成功
+ """
+ # 模拟程序启动
+ return True
+
+ # 状态查询方法
+ def get_current_temperature(self) -> float:
+ """获取当前温度 (°C)"""
+ return round(self.current_temperature, 2)
+
+ def get_target_temperature(self) -> float:
+ """获取目标温度 (°C)"""
+ return self.target_temperature
+
+ def get_heating_status(self) -> bool:
+ """获取加热状态"""
+ return self.is_heating
+
+ def get_cooling_status(self) -> bool:
+ """获取制冷状态"""
+ return self.is_cooling
+
+ def get_pid_status(self) -> bool:
+ """获取PID控制状态"""
+ return self.pid_enabled
+
+ def get_temperature_history(self) -> List[Dict]:
+ """获取温度历史记录"""
+ return self.temperature_history[-10:] # 返回最近10条记录
+
+ def get_controller_status(self) -> Dict[str, Any]:
+ """获取控制器完整状态"""
+ return {
+ "controller_id": self.controller_id,
+ "current_temp": self.current_temperature,
+ "target_temp": self.target_temperature,
+ "is_heating": self.is_heating,
+ "is_cooling": self.is_cooling,
+ "pid_enabled": self.pid_enabled,
+ "history_count": len(self.temperature_history),
+ }
+
+
+class MultiChannelAnalyzer:
+ """
+ 多通道分析仪
+
+ 支持同时监测多个通道的信号,提供实时数据采集和分析功能。
+ 常用于光谱分析、电化学测量等应用场景。
+ """
+
+ def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
+ """
+ 初始化多通道分析仪
+
+ Args:
+ analyzer_id: 分析仪ID
+ channels: 通道数量
+ """
+ self.analyzer_id = analyzer_id
+ self.channel_count = channels
+ self.channel_data = {i: {"value": 0.0, "unit": "V", "enabled": True} for i in range(channels)}
+ self.is_measuring = False
+ self.sample_rate = 1000 # Hz
+
+ def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
+ """
+ 配置通道
+
+ Args:
+ channel: 通道编号
+ enabled: 是否启用
+ unit: 测量单位
+
+ Returns:
+ bool: 配置是否成功
+ """
+ if 0 <= channel < self.channel_count:
+ self.channel_data[channel]["enabled"] = enabled
+ self.channel_data[channel]["unit"] = unit
+ return True
+ return False
+
+ async def start_measurement_async(self, duration: int = 10) -> Dict[str, Any]:
+ """
+ 开始异步测量
+
+ Args:
+ duration: 测量持续时间(秒)
+
+ Returns:
+ Dict: 测量结果
+ """
+ self.is_measuring = True
+
+ # 模拟数据采集
+ measurements = []
+ for second in range(duration):
+ timestamp = asyncio.get_event_loop().time()
+ frame_data = {}
+
+ for channel in range(self.channel_count):
+ if self.channel_data[channel]["enabled"]:
+ # 模拟传感器数据
+ import random
+
+ value = random.uniform(-5.0, 5.0)
+ frame_data[f"channel_{channel}"] = value
+ self.channel_data[channel]["value"] = value
+
+ measurements.append({"timestamp": timestamp, "data": frame_data})
+
+ await asyncio.sleep(1.0) # 每秒采集一次
+
+ self.is_measuring = False
+
+ return {
+ "success": True,
+ "duration": duration,
+ "samples_count": len(measurements),
+ "measurements": measurements[-5:], # 只返回最后5个样本
+ "channels_active": len([ch for ch in self.channel_data.values() if ch["enabled"]]),
+ }
+
+ def stop_measurement(self) -> bool:
+ """
+ 停止测量
+
+ Returns:
+ bool: 停止是否成功
+ """
+ self.is_measuring = False
+ return True
+
+ def reset_channels(self) -> bool:
+ """
+ 重置所有通道
+
+ Returns:
+ bool: 重置是否成功
+ """
+ for channel in self.channel_data:
+ self.channel_data[channel]["value"] = 0.0
+ return True
+
+ # 状态查询方法
+ def get_measurement_status(self) -> bool:
+ """获取测量状态"""
+ return self.is_measuring
+
+ def get_channel_count(self) -> int:
+ """获取通道数量"""
+ return self.channel_count
+
+ def get_sample_rate(self) -> float:
+ """获取采样率 (Hz)"""
+ return self.sample_rate
+
+ def get_channel_values(self) -> Dict[int, float]:
+ """获取所有通道的当前值"""
+ return {ch: data["value"] for ch, data in self.channel_data.items() if data["enabled"]}
+
+ def get_enabled_channels(self) -> List[int]:
+ """获取已启用的通道列表"""
+ return [ch for ch, data in self.channel_data.items() if data["enabled"]]
+
+ def get_analyzer_status(self) -> Dict[str, Any]:
+ """获取分析仪完整状态"""
+ return {
+ "analyzer_id": self.analyzer_id,
+ "channel_count": self.channel_count,
+ "is_measuring": self.is_measuring,
+ "sample_rate": self.sample_rate,
+ "active_channels": len(self.get_enabled_channels()),
+ "channel_data": self.channel_data,
+ }
+
+
+class AutomatedDispenser:
+ """
+ 自动分配器
+
+ 精确控制固体和液体材料的分配,支持多种分配模式和容器管理。
+ 集成称重功能,确保分配精度和重现性。
+ """
+
+ def __init__(self, dispenser_id: str = "dispenser_01"):
+ """
+ 初始化自动分配器
+
+ Args:
+ dispenser_id: 分配器ID
+ """
+ self.dispenser_id = dispenser_id
+ self.is_ready = True
+ self.current_position = {"x": 0.0, "y": 0.0, "z": 0.0}
+ self.dispensed_total = 0.0
+ self.container_capacity = 1000.0 # mL
+ self.precision_mode = True
+
+ def move_to_position(self, x: float, y: float, z: float) -> bool:
+ """
+ 移动到指定位置
+
+ Args:
+ x: X坐标 (mm)
+ y: Y坐标 (mm)
+ z: Z坐标 (mm)
+
+ Returns:
+ bool: 移动是否成功
+ """
+ self.current_position = {"x": x, "y": y, "z": z}
+ return True
+
+ async def dispense_liquid_async(self, volume: float, container_id: str, viscosity: str = "low") -> Dict[str, Any]:
+ """
+ 异步分配液体
+
+ Args:
+ volume: 分配体积 (mL)
+ container_id: 容器ID
+ viscosity: 液体粘度等级
+
+ Returns:
+ Dict: 分配结果
+ """
+ if not self.is_ready:
+ return {"success": False, "error": "设备未就绪"}
+
+ if volume <= 0:
+ return {"success": False, "error": "体积必须大于0"}
+
+ # 模拟分配过程
+ dispense_time = volume * 0.1 # 每mL需要0.1秒
+ if viscosity == "high":
+ dispense_time *= 2 # 高粘度液体需要更长时间
+
+ await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
+
+ self.dispensed_total += volume
+
+ return {
+ "success": True,
+ "dispensed_volume": volume,
+ "container_id": container_id,
+ "actual_time": min(dispense_time, 5.0),
+ "total_dispensed": self.dispensed_total,
+ }
+
+ def clean_dispenser(self, wash_volume: float = 5.0) -> bool:
+ """
+ 清洗分配器
+
+ Args:
+ wash_volume: 清洗液体积 (mL)
+
+ Returns:
+ bool: 清洗是否成功
+ """
+ # 模拟清洗过程
+ return True
+
+ def calibrate_volume(self, target_volume: float) -> bool:
+ """
+ 校准分配体积
+
+ Args:
+ target_volume: 校准目标体积 (mL)
+
+ Returns:
+ bool: 校准是否成功
+ """
+ # 模拟校准过程
+ return True
+
+ # 状态查询方法
+ def get_ready_status(self) -> bool:
+ """获取就绪状态"""
+ return self.is_ready
+
+ def get_current_position(self) -> Dict[str, float]:
+ """获取当前位置坐标"""
+ return self.current_position.copy()
+
+ def get_dispensed_total(self) -> float:
+ """获取累计分配体积 (mL)"""
+ return self.dispensed_total
+
+ def get_container_capacity(self) -> float:
+ """获取容器容量 (mL)"""
+ return self.container_capacity
+
+ def get_precision_mode(self) -> bool:
+ """获取精密模式状态"""
+ return self.precision_mode
+
+ def get_dispenser_status(self) -> Dict[str, Any]:
+ """获取分配器完整状态"""
+ return {
+ "dispenser_id": self.dispenser_id,
+ "ready": self.is_ready,
+ "position": self.current_position,
+ "dispensed_total": self.dispensed_total,
+ "capacity": self.container_capacity,
+ "precision_mode": self.precision_mode,
+ }
diff --git a/unilabos/app/web/api.py b/unilabos/app/web/api.py
index eb18b4d..bf51f4a 100644
--- a/unilabos/app/web/api.py
+++ b/unilabos/app/web/api.py
@@ -26,6 +26,49 @@ admin = APIRouter()
# 存储所有活动的WebSocket连接
active_connections: set[WebSocket] = set()
+# 存储注册表编辑器的WebSocket连接
+registry_editor_connections: set[WebSocket] = set()
+# 存储状态页面的WebSocket连接
+status_page_connections: set[WebSocket] = set()
+
+# 状态跟踪变量,用于差异检测
+_static_data_sent_connections: set[WebSocket] = set()
+_previous_host_node_info: dict = {}
+_previous_local_devices: dict = {}
+
+
+def compute_host_node_diff(current: dict, previous: dict) -> dict:
+ """计算主机节点信息的差异,只返回有变化的部分"""
+ diff = {}
+
+ # 检查可用性变化
+ if current.get("available") != previous.get("available"):
+ diff["available"] = current.get("available")
+
+ # 检查设备列表变化
+ current_devices = current.get("devices", {})
+ previous_devices = previous.get("devices", {})
+ if current_devices != previous_devices:
+ diff["devices"] = current_devices
+
+ # 检查动作客户端变化
+ current_action_clients = current.get("action_clients", {})
+ previous_action_clients = previous.get("action_clients", {})
+ if current_action_clients != previous_action_clients:
+ diff["action_clients"] = current_action_clients
+
+ # 检查订阅主题变化
+ current_topics = current.get("subscribed_topics", [])
+ previous_topics = previous.get("subscribed_topics", [])
+ if current_topics != previous_topics:
+ diff["subscribed_topics"] = current_topics
+
+ # 设备状态始终包含(因为需要实时更新)
+ if "device_status" in current:
+ diff["device_status"] = current["device_status"]
+ diff["device_status_timestamps"] = current.get("device_status_timestamps", {})
+
+ return diff
async def broadcast_device_status():
@@ -56,6 +99,137 @@ async def broadcast_device_status():
await asyncio.sleep(1)
+async def broadcast_status_page_data():
+ """广播状态页面数据到所有连接的客户端(优化版:增量更新)"""
+ global _previous_local_devices, _static_data_sent_connections, _previous_host_node_info
+
+ while True:
+ try:
+ if status_page_connections:
+ from unilabos.app.web.utils.host_utils import get_host_node_info
+ from unilabos.app.web.utils.ros_utils import get_ros_node_info
+ from unilabos.app.web.utils.device_utils import get_registry_info
+ from unilabos.config.config import BasicConfig
+ from unilabos.registry.registry import lab_registry
+ from unilabos.ros.msgs.message_converter import msg_converter_manager
+ from unilabos.utils.type_check import TypeEncoder
+ import json
+
+ # 获取当前数据
+ host_node_info = get_host_node_info()
+ ros_node_info = get_ros_node_info()
+
+ # 检查需要发送静态数据的新连接
+ new_connections = status_page_connections - _static_data_sent_connections
+
+ # 向新连接发送静态数据(Device Types、Resource Types、Converter Modules)
+ if new_connections:
+ devices = []
+ resources = []
+ modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
+
+ if lab_registry:
+ devices = json.loads(
+ json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder)
+ )
+ # 资源类型
+ for resource_id, resource_info in lab_registry.resource_type_registry.items():
+ resources.append(
+ {
+ "id": resource_id,
+ "name": resource_info.get("name", "未命名"),
+ "file_path": resource_info.get("file_path", ""),
+ }
+ )
+
+ # 获取导入的模块
+ if msg_converter_manager:
+ modules["names"] = msg_converter_manager.list_modules()
+ all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
+ modules["total_count"] = len(all_classes)
+ modules["classes"] = all_classes
+
+ # 静态数据
+ registry_info = get_registry_info()
+ static_data = {
+ "type": "static_data_init",
+ "data": {
+ "devices": devices,
+ "resources": resources,
+ "modules": modules,
+ "registry_info": registry_info,
+ "is_host_mode": BasicConfig.is_host_mode,
+ "host_node_info": host_node_info, # 添加主机节点初始信息
+ "ros_node_info": ros_node_info, # 添加本地设备初始信息
+ },
+ }
+
+ # 发送到新连接
+ disconnected_new_connections = set()
+ for connection in new_connections:
+ try:
+ await connection.send_json(static_data)
+ _static_data_sent_connections.add(connection)
+ except Exception as e:
+ print(f"Error sending static data to new client: {e}")
+ disconnected_new_connections.add(connection)
+
+ # 清理断开的新连接
+ for conn in disconnected_new_connections:
+ status_page_connections.discard(conn)
+ _static_data_sent_connections.discard(conn)
+
+ # 检查主机节点信息是否有变更
+ host_node_diff = compute_host_node_diff(host_node_info, _previous_host_node_info)
+ host_changed = bool(host_node_diff)
+
+ # 检查Local Devices是否有变更
+ current_devices = ros_node_info.get("registered_devices", {})
+ devices_changed = current_devices != _previous_local_devices
+
+ # 只有当有真正的变化时才发送更新
+ if host_changed or devices_changed:
+ # 发送增量更新数据
+ update_data = {
+ "type": "incremental_update",
+ "data": {
+ "timestamp": asyncio.get_event_loop().time(),
+ },
+ }
+
+ # 只包含有变化的主机节点信息
+ if host_changed:
+ update_data["data"]["host_node_info"] = host_node_diff
+
+ # 如果Local Devices发生变更,添加到更新数据中
+ if devices_changed:
+ update_data["data"]["ros_node_info"] = ros_node_info
+ _previous_local_devices = current_devices.copy()
+
+ # 更新主机节点状态
+ if host_changed:
+ _previous_host_node_info = host_node_info.copy()
+
+ # 发送增量更新到所有连接
+ disconnected_connections = set()
+ for connection in status_page_connections:
+ try:
+ await connection.send_json(update_data)
+ except Exception as e:
+ print(f"Error sending incremental update to client: {e}")
+ disconnected_connections.add(connection)
+
+ # 清理断开的连接
+ for conn in disconnected_connections:
+ status_page_connections.discard(conn)
+ _static_data_sent_connections.discard(conn)
+
+ await asyncio.sleep(1) # 每秒检查一次更新
+ except Exception as e:
+ print(f"Error in status page broadcast: {e}")
+ await asyncio.sleep(1)
+
+
@api.websocket("/ws/device_status")
async def websocket_device_status(websocket: WebSocket):
"""WebSocket端点,用于实时获取设备状态"""
@@ -72,6 +246,838 @@ async def websocket_device_status(websocket: WebSocket):
active_connections.remove(websocket)
+@api.websocket("/ws/registry_editor")
+async def websocket_registry_editor(websocket: WebSocket):
+ """WebSocket端点,用于注册表编辑器"""
+ await websocket.accept()
+ registry_editor_connections.add(websocket)
+
+ try:
+ while True:
+ # 接收来自客户端的消息
+ message = await websocket.receive_text()
+ import json
+
+ data = json.loads(message)
+
+ if data.get("type") == "import_file":
+ await handle_file_import(websocket, data["data"])
+ elif data.get("type") == "analyze_file":
+ await handle_file_analysis(websocket, data["data"])
+ elif data.get("type") == "analyze_file_content":
+ await handle_file_content_analysis(websocket, data["data"])
+ elif data.get("type") == "import_file_content":
+ await handle_file_content_import(websocket, data["data"])
+
+ except WebSocketDisconnect:
+ registry_editor_connections.remove(websocket)
+ except Exception as e:
+ print(f"Registry Editor WebSocket error: {e}")
+ if websocket in registry_editor_connections:
+ registry_editor_connections.remove(websocket)
+
+
+@api.websocket("/ws/status_page")
+async def websocket_status_page(websocket: WebSocket):
+ """WebSocket端点,用于状态页面实时数据更新"""
+ await websocket.accept()
+ status_page_connections.add(websocket)
+
+ try:
+ while True:
+ # 接收来自客户端的消息(用于保持连接活跃)
+ message = await websocket.receive_text()
+ # 状态页面通常只需要接收数据,不需要发送复杂指令
+
+ except WebSocketDisconnect:
+ status_page_connections.remove(websocket)
+ except Exception as e:
+ print(f"Status Page WebSocket error: {e}")
+ if websocket in status_page_connections:
+ status_page_connections.remove(websocket)
+
+
+async def handle_file_analysis(websocket: WebSocket, request_data: dict):
+ """处理文件分析请求,获取文件中的类列表"""
+ import json
+ import os
+ import sys
+ import inspect
+ import traceback
+ from pathlib import Path
+ from unilabos.config.config import BasicConfig
+
+ file_path = request_data.get("file_path")
+
+ async def send_log(message: str, level: str = "info"):
+ """发送日志消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "log", "message": message, "level": level}))
+ except Exception as e:
+ print(f"Failed to send log: {e}")
+
+ async def send_analysis_result(result_data: dict):
+ """发送分析结果到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "file_analysis_result", "data": result_data}))
+ except Exception as e:
+ print(f"Failed to send analysis result: {e}")
+
+ try:
+ # 验证文件路径参数
+ if not file_path:
+ await send_analysis_result({"success": False, "error": "文件路径为空", "file_path": ""})
+ return
+
+ # 获取工作目录并构建完整路径
+ working_dir_str = getattr(BasicConfig, "working_dir", None) or os.getcwd()
+ working_dir = Path(working_dir_str)
+ full_file_path = working_dir / file_path
+
+ # 验证文件路径
+ if not full_file_path.exists():
+ await send_analysis_result(
+ {"success": False, "error": f"文件路径不存在: {file_path}", "file_path": file_path}
+ )
+ return
+
+ await send_log(f"开始分析文件: {file_path}")
+
+ # 验证文件是Python文件
+ if not file_path.endswith(".py"):
+ await send_analysis_result({"success": False, "error": "请选择Python文件 (.py)", "file_path": file_path})
+ return
+
+ full_file_path = full_file_path.absolute()
+ await send_log(f"文件绝对路径: {full_file_path}")
+
+ # 添加文件目录到sys.path
+ file_dir = str(full_file_path.parent)
+ if file_dir not in sys.path:
+ sys.path.insert(0, file_dir)
+ await send_log(f"已添加路径到sys.path: {file_dir}")
+
+ # 确定模块名
+ module_name = full_file_path.stem
+ await send_log(f"使用模块名: {module_name}")
+
+ # 导入模块进行分析
+ try:
+ # 如果模块已经导入,先删除以便重新导入
+ if module_name in sys.modules:
+ del sys.modules[module_name]
+ await send_log(f"已删除旧模块: {module_name}")
+
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location(module_name, full_file_path)
+ if spec is None or spec.loader is None:
+ await send_analysis_result(
+ {"success": False, "error": "无法创建模块规范", "file_path": str(full_file_path)}
+ )
+ return
+
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ await send_log(f"成功导入模块用于分析: {module_name}")
+
+ except Exception as e:
+ await send_analysis_result(
+ {"success": False, "error": f"导入模块失败: {str(e)}", "file_path": str(full_file_path)}
+ )
+ return
+
+ # 分析模块中的类
+ classes = []
+ for name in dir(module):
+ try:
+ obj = getattr(module, name)
+ if isinstance(obj, type) and obj.__module__ == module_name:
+ # 获取类的文档字符串
+ docstring = inspect.getdoc(obj) or ""
+ # 只取第一行作为简短描述
+ short_desc = docstring.split("\n")[0] if docstring else ""
+
+ classes.append({"name": name, "docstring": short_desc, "full_docstring": docstring})
+ except Exception as e:
+ await send_log(f"分析类 {name} 时出错: {str(e)}", "warning")
+ continue
+
+ if not classes:
+ await send_analysis_result(
+ {
+ "success": False,
+ "error": "模块中未找到任何类定义",
+ "file_path": str(full_file_path),
+ "module_name": module_name,
+ }
+ )
+ return
+
+ await send_log(f"找到 {len(classes)} 个类: {[cls['name'] for cls in classes]}")
+
+ # 发送分析结果
+ await send_analysis_result(
+ {"success": True, "file_path": str(full_file_path), "module_name": module_name, "classes": classes}
+ )
+
+ except Exception as e:
+ await send_analysis_result(
+ {
+ "success": False,
+ "error": f"分析过程中发生错误: {str(e)}",
+ "file_path": file_path,
+ "traceback": traceback.format_exc(),
+ }
+ )
+
+
+async def handle_file_content_analysis(websocket: WebSocket, request_data: dict):
+ """处理文件内容分析请求,直接分析上传的文件内容"""
+ import json
+ import os
+ import sys
+ import inspect
+ import traceback
+ import tempfile
+ from pathlib import Path
+
+ file_name = request_data.get("file_name")
+ file_content = request_data.get("file_content")
+ file_size = request_data.get("file_size", 0)
+
+ async def send_log(message: str, level: str = "info"):
+ """发送日志消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "log", "message": message, "level": level}))
+ except Exception as e:
+ print(f"Failed to send log: {e}")
+
+ async def send_analysis_result(result_data: dict):
+ """发送分析结果到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "file_analysis_result", "data": result_data}))
+ except Exception as e:
+ print(f"Failed to send analysis result: {e}")
+
+ try:
+ # 验证文件内容
+ if not file_name or not file_content:
+ await send_analysis_result({"success": False, "error": "文件名或文件内容为空", "file_name": file_name})
+ return
+
+ await send_log(f"开始分析文件: {file_name} ({file_size} 字节)")
+
+ # 验证文件是Python文件
+ if not file_name.endswith(".py"):
+ await send_analysis_result({"success": False, "error": "请选择Python文件 (.py)", "file_name": file_name})
+ return
+
+ # 创建临时文件
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as temp_file:
+ temp_file.write(file_content)
+ temp_file_path = temp_file.name
+
+ await send_log(f"创建临时文件: {temp_file_path}")
+
+ try:
+ # 添加临时文件目录到sys.path
+ temp_dir = str(Path(temp_file_path).parent)
+ if temp_dir not in sys.path:
+ sys.path.insert(0, temp_dir)
+ await send_log(f"已添加临时目录到sys.path: {temp_dir}")
+
+ # 确定模块名(去掉.py扩展名)
+ module_name = file_name.replace(".py", "").replace("-", "_").replace(" ", "_")
+ await send_log(f"使用模块名: {module_name}")
+
+ # 导入模块进行分析
+ try:
+ # 如果模块已经导入,先删除以便重新导入
+ if module_name in sys.modules:
+ del sys.modules[module_name]
+ await send_log(f"已删除旧模块: {module_name}")
+
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location(module_name, temp_file_path)
+ if spec is None or spec.loader is None:
+ raise Exception("无法创建模块规范")
+
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ await send_log(f"成功导入模块用于分析: {module_name}")
+
+ except Exception as e:
+ await send_analysis_result(
+ {"success": False, "error": f"导入模块失败: {str(e)}", "file_name": file_name}
+ )
+ return
+
+ # 分析模块中的类
+ classes = []
+ for name in dir(module):
+ try:
+ obj = getattr(module, name)
+ if isinstance(obj, type) and obj.__module__ == module_name:
+ # 获取类的文档字符串
+ docstring = inspect.getdoc(obj) or ""
+ # 只取第一行作为简短描述
+ short_desc = docstring.split("\n")[0] if docstring else "无描述"
+
+ classes.append({"name": name, "docstring": short_desc, "full_docstring": docstring})
+ except Exception as e:
+ await send_log(f"分析类 {name} 时出错: {str(e)}", "warning")
+ continue
+
+ if not classes:
+ await send_analysis_result(
+ {
+ "success": False,
+ "error": "模块中未找到任何类定义",
+ "file_name": file_name,
+ "module_name": module_name,
+ }
+ )
+ return
+
+ await send_log(f"找到 {len(classes)} 个类: {[cls['name'] for cls in classes]}")
+
+ # 发送分析结果
+ await send_analysis_result(
+ {
+ "success": True,
+ "file_name": file_name,
+ "module_name": module_name,
+ "classes": classes,
+ "temp_file_path": temp_file_path, # 保存临时文件路径供后续使用
+ }
+ )
+
+ finally:
+ # 清理临时文件(在导入完成后再删除)
+ try:
+ if os.path.exists(temp_file_path):
+ # 延迟删除,给导入操作留出时间
+ import threading
+
+ def delayed_cleanup():
+ import time
+
+ time.sleep(60) # 等待60秒后删除
+ try:
+ os.unlink(temp_file_path)
+ except OSError:
+ pass
+
+ threading.Thread(target=delayed_cleanup, daemon=True).start()
+ except Exception as e:
+ await send_log(f"清理临时文件时出错: {str(e)}", "warning")
+
+ except Exception as e:
+ await send_analysis_result(
+ {
+ "success": False,
+ "error": f"分析过程中发生错误: {str(e)}",
+ "file_name": file_name,
+ "traceback": traceback.format_exc(),
+ }
+ )
+
+
+async def handle_file_content_import(websocket: WebSocket, request_data: dict):
+ """处理基于文件内容的导入请求"""
+ import json
+ import os
+ import sys
+ import traceback
+ import tempfile
+ from pathlib import Path
+
+ file_name = request_data.get("file_name")
+ file_content = request_data.get("file_content")
+ file_size = request_data.get("file_size", 0)
+ registry_type = request_data.get("registry_type", "device")
+ class_name = request_data.get("class_name")
+
+ async def send_log(message: str, level: str = "info"):
+ """发送日志消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "log", "message": message, "level": level}))
+ except Exception as e:
+ print(f"Failed to send log: {e}")
+
+ async def send_progress(message: str):
+ """发送进度消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "progress", "message": message}))
+ except Exception as e:
+ print(f"Failed to send progress: {e}")
+
+ async def send_error(message: str):
+ """发送错误消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "error", "message": message}))
+ except Exception as e:
+ print(f"Failed to send error: {e}")
+
+ async def send_result(result_data: dict):
+ """发送结果数据到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "result", "data": result_data}))
+ except Exception as e:
+ print(f"Failed to send result: {e}")
+
+ try:
+ # 验证输入参数
+ if not file_name or not file_content or not class_name:
+ await send_error("文件名、文件内容或类名为空")
+ return
+
+ await send_log(f"开始处理文件: {file_name} ({file_size} 字节)")
+ await send_progress("正在创建临时文件...")
+
+ # 创建临时文件
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as temp_file:
+ temp_file.write(file_content)
+ temp_file_path = temp_file.name
+
+ await send_log(f"创建临时文件: {temp_file_path}")
+
+ # 添加临时文件目录到sys.path
+ temp_dir = str(Path(temp_file_path).parent)
+ if temp_dir not in sys.path:
+ sys.path.insert(0, temp_dir)
+ await send_log(f"已添加临时目录到sys.path: {temp_dir}")
+
+ # 确定模块名
+ module_name = file_name.replace(".py", "").replace("-", "_").replace(" ", "_")
+ await send_log(f"使用模块名: {module_name}")
+
+ # 导入模块
+ try:
+ # 如果模块已经导入,先删除以便重新导入
+ if module_name in sys.modules:
+ del sys.modules[module_name]
+ await send_log(f"已删除旧模块: {module_name}")
+
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location(module_name, temp_file_path)
+ if spec is None or spec.loader is None:
+ await send_error("无法创建模块规范")
+ return
+
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ await send_log(f"成功导入模块: {module_name}")
+
+ except Exception as e:
+ await send_error(f"导入模块失败: {str(e)}")
+ return
+
+ # 验证类存在
+ if not hasattr(module, class_name):
+ await send_error(f"模块中未找到类: {class_name}")
+ return
+
+ target_class = getattr(module, class_name)
+ await send_log(f"找到目标类: {class_name}")
+
+ # 使用registry.py的增强类信息功能进行分析
+ await send_progress("正在生成注册表信息...")
+
+ try:
+ from unilabos.utils.import_manager import get_enhanced_class_info
+
+ # 分析类信息
+ enhanced_info = get_enhanced_class_info(f"{module_name}:{class_name}", use_dynamic=True)
+
+ if not enhanced_info.get("dynamic_import_success", False):
+ await send_error("动态导入类信息失败")
+ return
+
+ await send_log("成功分析类信息")
+
+ # 生成注册表schema
+ registry_schema = {
+ "class_name": class_name,
+ "module": f"{module_name}:{class_name}",
+ "type": "python",
+ "description": enhanced_info.get("class_docstring", ""),
+ "version": "1.0.0",
+ "category": [registry_type],
+ "status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
+ "action_value_mappings": {},
+ "init_param_schema": {},
+ "registry_type": registry_type,
+ "file_path": f"uploaded_file://{file_name}",
+ }
+
+ # 处理动作方法
+ for method_name, method_info in enhanced_info["action_methods"].items():
+ registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
+ "type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
+ "goal": {},
+ "feedback": {},
+ "result": {},
+ "args": method_info["args"],
+ "description": method_info.get("docstring", ""),
+ }
+
+ await send_log("成功生成注册表schema")
+
+ # 准备结果数据
+ result = {
+ "class_info": {
+ "class_name": class_name,
+ "module_name": module_name,
+ "file_name": file_name,
+ "file_size": file_size,
+ "docstring": enhanced_info.get("class_docstring", ""),
+ "dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
+ },
+ "registry_schema": registry_schema,
+ "action_methods": enhanced_info["action_methods"],
+ "status_methods": enhanced_info["status_methods"],
+ }
+
+ # 发送结果
+ await send_result(result)
+ await send_log("分析完成")
+
+ except Exception as e:
+ await send_error(f"分析类信息时发生错误: {str(e)}")
+ await send_log(f"详细错误信息: {traceback.format_exc()}")
+ return
+
+ finally:
+ # 清理临时文件
+ try:
+ if os.path.exists(temp_file_path):
+ import threading
+
+ def delayed_cleanup():
+ import time
+
+ time.sleep(30) # 等待30秒后删除
+ try:
+ os.unlink(temp_file_path)
+ except OSError:
+ pass
+
+ threading.Thread(target=delayed_cleanup, daemon=True).start()
+ except Exception as e:
+ await send_log(f"清理临时文件时出错: {str(e)}", "warning")
+
+ except Exception as e:
+ await send_error(f"处理过程中发生错误: {str(e)}")
+ await send_log(f"详细错误信息: {traceback.format_exc()}")
+
+
+async def handle_file_import(websocket: WebSocket, request_data: dict):
+ """处理文件导入请求"""
+ import json
+ import os
+ import sys
+ import traceback
+ from pathlib import Path
+ from unilabos.config.config import BasicConfig
+
+ file_path = request_data.get("file_path")
+ registry_type = request_data.get("registry_type", "device")
+ class_name = request_data.get("class_name")
+ module_name = request_data.get("module_name")
+
+ async def send_log(message: str, level: str = "info"):
+ """发送日志消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "log", "message": message, "level": level}))
+ except Exception as e:
+ print(f"Failed to send log: {e}")
+
+ async def send_progress(message: str):
+ """发送进度消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "progress", "message": message}))
+ except Exception as e:
+ print(f"Failed to send progress: {e}")
+
+ async def send_error(message: str):
+ """发送错误消息到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "error", "message": message}))
+ except Exception as e:
+ print(f"Failed to send error: {e}")
+
+ async def send_result(result_data: dict):
+ """发送结果数据到客户端"""
+ try:
+ await websocket.send_text(json.dumps({"type": "result", "data": result_data}))
+ except Exception as e:
+ print(f"Failed to send result: {e}")
+
+ try:
+ # 验证文件路径参数
+ if not file_path:
+ await send_error("文件路径为空")
+ return
+
+ # 获取工作目录并构建完整路径
+ working_dir_str = getattr(BasicConfig, "working_dir", None) or os.getcwd()
+ working_dir = Path(working_dir_str)
+ full_file_path = working_dir / file_path
+
+ # 验证文件路径
+ if not full_file_path.exists():
+ await send_error(f"文件路径不存在: {file_path}")
+ return
+
+ await send_log(f"开始处理文件: {file_path}")
+ await send_progress("正在验证文件...")
+
+ # 验证文件是Python文件
+ if not file_path.endswith(".py"):
+ await send_error("请选择Python文件 (.py)")
+ return
+
+ full_file_path = full_file_path.absolute()
+ await send_log(f"文件绝对路径: {full_file_path}")
+
+ # 动态导入模块
+ await send_progress("正在导入模块...")
+
+ # 添加文件目录到sys.path
+ file_dir = str(full_file_path.parent)
+ if file_dir not in sys.path:
+ sys.path.insert(0, file_dir)
+ await send_log(f"已添加路径到sys.path: {file_dir}")
+
+ # 确定模块名
+ if not module_name:
+ module_name = full_file_path.stem
+ await send_log(f"使用模块名: {module_name}")
+
+ # 导入模块
+ try:
+ # 如果模块已经导入,先删除以便重新导入
+ if module_name in sys.modules:
+ del sys.modules[module_name]
+ await send_log(f"已删除旧模块: {module_name}")
+
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location(module_name, full_file_path)
+ if spec is None or spec.loader is None:
+ await send_error("无法创建模块规范")
+ return
+
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ await send_log(f"成功导入模块: {module_name}")
+
+ except Exception as e:
+ await send_error(f"导入模块失败: {str(e)}")
+ return
+
+ # 分析模块
+ await send_progress("正在分析模块...")
+
+ # 获取模块中的所有类
+ classes = []
+ for name in dir(module):
+ obj = getattr(module, name)
+ if isinstance(obj, type) and obj.__module__ == module_name:
+ classes.append((name, obj))
+
+ if not classes:
+ await send_error("模块中未找到任何类定义")
+ return
+
+ await send_log(f"找到 {len(classes)} 个类: {[name for name, _ in classes]}")
+
+ # 确定要分析的类
+ target_class = None
+ target_class_name = None
+
+ if class_name:
+ # 用户指定了类名
+ for name, cls in classes:
+ if name == class_name:
+ target_class = cls
+ target_class_name = name
+ break
+ if not target_class:
+ await send_error(f"未找到指定的类: {class_name}")
+ return
+ else:
+ # 自动选择第一个类
+ target_class_name, target_class = classes[0]
+ await send_log(f"自动选择类: {target_class_name}")
+
+ # 使用registry.py的增强类信息功能进行分析
+ await send_progress("正在生成注册表信息...")
+
+ try:
+ from unilabos.utils.import_manager import get_enhanced_class_info
+
+ # 分析类信息
+ enhanced_info = get_enhanced_class_info(f"{module_name}:{target_class_name}", use_dynamic=True)
+
+ if not enhanced_info.get("dynamic_import_success", False):
+ await send_error("动态导入类信息失败")
+ return
+
+ await send_log("成功分析类信息")
+
+ # 生成注册表schema
+ registry_schema = {
+ "class_name": target_class_name,
+ "module": f"{module_name}:{target_class_name}",
+ "type": "python",
+ "description": enhanced_info.get("class_docstring", ""),
+ "version": "1.0.0",
+ "category": [registry_type],
+ "status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
+ "action_value_mappings": {},
+ "init_param_schema": {},
+ "registry_type": registry_type,
+ "file_path": str(full_file_path),
+ }
+
+ # 处理动作方法
+ for method_name, method_info in enhanced_info["action_methods"].items():
+ registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
+ "type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
+ "goal": {},
+ "feedback": {},
+ "result": {},
+ "args": method_info["args"],
+ "description": method_info.get("docstring", ""),
+ }
+
+ await send_log("成功生成注册表schema")
+
+ # 转换为YAML格式
+ import yaml
+ from unilabos.utils.type_check import NoAliasDumper
+
+ # 创建最终的YAML配置(使用设备ID作为根键)
+ class_name_safe = class_name or "unknown"
+ suffix = "_device" if registry_type == "device" else "_resource"
+ device_id = f"{class_name_safe.lower()}{suffix}"
+ final_config = {device_id: registry_schema}
+
+ yaml_content = yaml.dump(
+ final_config, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper, sort_keys=True
+ )
+
+ # 准备结果数据(只保留YAML结果)
+ result = {
+ "registry_schema": yaml_content,
+ "device_id": device_id,
+ "class_name": class_name,
+ "module_name": module_name,
+ "file_path": file_path,
+ }
+
+ # 发送结果
+ await send_result(result)
+ await send_log("注册表生成完成")
+
+ except Exception as e:
+ await send_error(f"分析类信息时发生错误: {str(e)}")
+ await send_log(f"详细错误信息: {traceback.format_exc()}")
+ return
+
+ except Exception as e:
+ await send_error(f"处理过程中发生错误: {str(e)}")
+ await send_log(f"详细错误信息: {traceback.format_exc()}")
+
+
+@api.get("/file-browser", summary="Browse files and directories", response_model=Resp)
+def get_file_browser_data(path: str = ""):
+ """获取文件浏览器数据"""
+ import os
+ from pathlib import Path
+ from unilabos.config.config import BasicConfig
+
+ try:
+ # 获取工作目录
+ working_dir_str = getattr(BasicConfig, "working_dir", None) or os.getcwd()
+ working_dir = Path(working_dir_str)
+
+ # 如果提供了相对路径,则在工作目录下查找
+ if path:
+ target_path = working_dir / path
+ else:
+ target_path = working_dir
+
+ # 确保路径在工作目录内(安全检查)
+ target_path = target_path.resolve()
+
+ if not target_path.exists():
+ return Resp(code=RespCode.ErrorInvalidReq, message=f"路径不存在: {path}")
+
+ if not target_path.is_dir():
+ return Resp(code=RespCode.ErrorInvalidReq, message=f"不是目录: {path}")
+
+ # 获取目录内容
+ items = []
+
+ parent_path = target_path.parent
+ relative_parent = parent_path.relative_to(working_dir)
+ items.append(
+ {
+ "name": "..",
+ "type": "directory",
+ "path": str(relative_parent) if relative_parent != Path(".") else "",
+ "size": 0,
+ "is_parent": True,
+ }
+ )
+
+ # 获取子目录和文件
+ try:
+ for item in sorted(target_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
+ if item.name.startswith("."): # 跳过隐藏文件
+ continue
+
+ item_type = "directory" if item.is_dir() else "file"
+ relative_path = item.relative_to(working_dir)
+
+ item_info = {
+ "name": item.name,
+ "type": item_type,
+ "path": str(relative_path),
+ "size": item.stat().st_size if item.is_file() else 0,
+ "is_python": item.suffix == ".py" if item.is_file() else False,
+ "is_parent": False,
+ }
+ items.append(item_info)
+ except PermissionError:
+ return Resp(code=RespCode.ErrorInvalidReq, message="无权限访问此目录")
+
+ return Resp(
+ data={
+ "current_path": str(target_path.relative_to(working_dir)) if target_path != working_dir else "",
+ "working_dir": str(working_dir),
+ "items": items,
+ }
+ )
+
+ except Exception as e:
+ return Resp(code=RespCode.ErrorInvalidReq, message=f"获取目录信息失败: {str(e)}")
+
+
@api.get("/resources", summary="Resource list", response_model=Resp)
def get_resources():
"""获取资源列表"""
@@ -82,18 +1088,6 @@ def get_resources():
return Resp(data=dict(data))
-@api.get("/repository", summary="Raw Material list", response_model=Resp)
-def get_raw_material():
- """获取原材料列表"""
- return Resp(data={})
-
-
-@api.post("/repository", summary="Raw Material set", response_model=Resp)
-def post_raw_material():
- """设置原材料"""
- return Resp(data={})
-
-
@api.get("/devices", summary="Device list", response_model=Resp)
def get_devices():
"""获取设备列表"""
@@ -104,12 +1098,6 @@ def get_devices():
return Resp(data=dict(data))
-@api.get("/devices/{id}/info", summary="Device info", response_model=Resp)
-def device_info(id: str):
- """获取设备信息"""
- return Resp(data={})
-
-
@api.get("/job/{id}/status", summary="Job status", response_model=JobStatusResp)
def job_status(id: str):
"""获取任务状态"""
@@ -129,63 +1117,6 @@ def post_job_add(req: JobAddReq):
return JobAddResp(data=data)
-@api.post("/job/step_finish", summary="步骤完成推送", response_model=Resp)
-def callback_step_finish(req: JobStepFinishReq):
- """任务步骤完成回调"""
- print(req)
- return Resp(data={})
-
-
-@api.post("/job/preintake_finish", summary="通量完成推送", response_model=Resp)
-def callback_preintake_finish(req: JobPreintakeFinishReq):
- """通量完成回调"""
- print(req)
- return Resp(data={})
-
-
-@api.post("/job/finish", summary="完成推送", response_model=Resp)
-def callback_order_finish(req: JobFinishReq):
- """任务完成回调"""
- print(req)
- return Resp(data={})
-
-
-@admin.get("/device_models", summary="Device model list", response_model=Resp)
-def admin_device_models():
- """获取设备模型列表"""
- return Resp(data={})
-
-
-@admin.post("/device_model/add", summary="Add Device model", response_model=Resp)
-def admin_device_model_add():
- """添加设备模型"""
- return Resp(data={})
-
-
-@admin.delete("/device_model/{id}", summary="Delete device model", response_model=Resp)
-def admin_device_model_del(id: str):
- """删除设备模型"""
- return Resp(data={})
-
-
-@admin.get("/devices", summary="Device list", response_model=Resp)
-def admin_devices():
- """获取设备列表(管理员)"""
- return Resp(data={})
-
-
-@admin.post("/devices/add", summary="Add Device", response_model=Resp)
-def admin_device_add():
- """添加设备"""
- return Resp(data={})
-
-
-@admin.delete("/devices/{id}", summary="Delete device", response_model=Resp)
-def admin_device_del(id: str):
- """删除设备"""
- return Resp(data={})
-
-
def setup_api_routes(app):
"""设置API路由"""
app.include_router(admin, prefix="/admin/v1", tags=["admin"])
@@ -195,3 +1126,4 @@ def setup_api_routes(app):
@app.on_event("startup")
async def startup_event():
asyncio.create_task(broadcast_device_status())
+ asyncio.create_task(broadcast_status_page_data())
diff --git a/unilabos/app/web/pages.py b/unilabos/app/web/pages.py
index 9f9a6c0..2887c2a 100644
--- a/unilabos/app/web/pages.py
+++ b/unilabos/app/web/pages.py
@@ -78,21 +78,23 @@ def setup_web_pages(router: APIRouter) -> None:
HTMLResponse: 渲染后的HTML页面
"""
try:
- # 准备设备数据
+ # 准备初始数据结构(这些数据将通过WebSocket实时更新)
devices = []
resources = []
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
- # 获取在线设备信息
+ # 获取在线设备信息(用于初始渲染)
ros_node_info = get_ros_node_info()
- # 获取主机节点信息
+ # 获取主机节点信息(用于初始渲染)
host_node_info = get_host_node_info()
- # 获取Registry路径信息
+ # 获取Registry路径信息(静态信息,不需要实时更新)
registry_info = get_registry_info()
- # 获取已加载的设备
+ # 获取初始数据用于页面渲染(后续将被WebSocket数据覆盖)
if lab_registry:
- devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
+ devices = json.loads(
+ json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder)
+ )
# 资源类型
for resource_id, resource_info in lab_registry.resource_type_registry.items():
resources.append(
@@ -103,7 +105,7 @@ def setup_web_pages(router: APIRouter) -> None:
}
)
- # 获取导入的模块
+ # 获取导入的模块(初始数据)
if msg_converter_manager:
modules["names"] = msg_converter_manager.list_modules()
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
@@ -171,3 +173,20 @@ def setup_web_pages(router: APIRouter) -> None:
except Exception as e:
error(f"打开文件夹时出错: {str(e)}")
return {"status": "error", "message": f"Failed to open folder: {str(e)}"}
+
+ @router.get("/registry-editor", response_class=HTMLResponse, summary="Registry Editor")
+ async def registry_editor_page() -> str:
+ """
+ 注册表编辑页面,用于导入Python文件并生成注册表
+
+ Returns:
+ HTMLResponse: 渲染后的HTML页面
+ """
+ try:
+ # 使用模板渲染页面
+ template = env.get_template("registry_editor.html")
+ html = template.render()
+ return html
+ except Exception as e:
+ error(f"生成注册表编辑页面时出错: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Error generating registry editor page: {str(e)}")
diff --git a/unilabos/app/web/templates/base.html b/unilabos/app/web/templates/base.html
index 8e3a31d..cb3e1a4 100644
--- a/unilabos/app/web/templates/base.html
+++ b/unilabos/app/web/templates/base.html
@@ -162,7 +162,6 @@
{% block header %}UniLab{% endblock %}
{% block nav %}
- Home
{% endblock %}
{% block top_info %}{% endblock %}
diff --git a/unilabos/app/web/templates/home.html b/unilabos/app/web/templates/home.html
index a95f9d6..2517f1c 100644
--- a/unilabos/app/web/templates/home.html
+++ b/unilabos/app/web/templates/home.html
@@ -1,22 +1,25 @@
-{% extends "base.html" %}
-
-{% block title %}UniLab API{% endblock %}
-
-{% block header %}UniLab API{% endblock %}
-
-{% block nav %}
-System Status
-{% endblock %}
-
-{% block content %}
-
-
Available Endpoints
- {% for route in routes %}
-
- {% endfor %}
+{% extends "base.html" %} {% block title %}UniLab API{% endblock %} {% block
+header %}UniLab API{% endblock %} {% block nav %}
+
-{% endblock %}
\ No newline at end of file
+{% endblock %} {% block content %}
+
+
Available Endpoints
+ {% for route in routes %}
+
+ {% endfor %}
+
+{% endblock %}
diff --git a/unilabos/app/web/templates/registry_editor.html b/unilabos/app/web/templates/registry_editor.html
new file mode 100644
index 0000000..8e23b39
--- /dev/null
+++ b/unilabos/app/web/templates/registry_editor.html
@@ -0,0 +1,1085 @@
+{% extends "base.html" %} {% block title %}注册表编辑器 - UniLab{% endblock %}
+{% block header %}注册表编辑器{% endblock %} {% block nav %}
+{% endblock %} {% block scripts %}
+
+{% endblock %} {% block content %}
+
+
+
+{% endblock %}
diff --git a/unilabos/app/web/templates/status.html b/unilabos/app/web/templates/status.html
index e1105b7..44412a2 100644
--- a/unilabos/app/web/templates/status.html
+++ b/unilabos/app/web/templates/status.html
@@ -1,1255 +1,2375 @@
-{% extends "base.html" %}
+{% extends "base.html" %} {% block title %}UniLab System Status{% endblock %} {%
+block header %}UniLab System Status{% endblock %} {% block top_info %}
+
+
-{% block title %}UniLab System Status{% endblock %}
-
-{% block header %}UniLab System Status{% endblock %}
-
-{% block top_info %}
-
- 系统模式: {{ "主机模式 (HOST)" if is_host_mode else "从机模式 (SLAVE)" }}
-
+
+ 系统模式:
+ {{ "主机模式 (HOST)" if is_host_mode else "从机模式 (SLAVE)" }}
+
{% if registry_info %}
- {% if registry_info.paths %}
-
-
注册表路径:
-
- {% for path in registry_info.paths %}
- -
- {{ path }}
- 📁
-
- {% endfor %}
-
-
- {% endif %}
-
- {% if registry_info.devices_paths %}
-
-
设备目录:
-
- {% for path in registry_info.devices_paths %}
- -
- {{ path }}
- 📁
-
- {% endfor %}
-
-
- {% endif %}
-
- {% if registry_info.device_comms_paths %}
-
-
设备通信目录:
-
- {% for path in registry_info.device_comms_paths %}
- -
- {{ path }}
- 📁
-
- {% endfor %}
-
-
- {% endif %}
-
- {% if registry_info.resources_paths %}
-
-
资源目录:
-
- {% for path in registry_info.resources_paths %}
- -
- {{ path }}
- 📁
-
- {% endfor %}
-
-
- {% endif %}
+ {% if registry_info.paths %}
+
+
注册表路径:
+
+ {% for path in registry_info.paths %}
+ -
+ {{ path }}
+ 📁
+
+ {% endfor %}
+
+
+ {% endif %} {% if registry_info.devices_paths %}
+
+
设备目录:
+
+ {% for path in registry_info.devices_paths %}
+ -
+ {{ path }}
+ 📁
+
+ {% endfor %}
+
+
+ {% endif %} {% if registry_info.device_comms_paths %}
+
+
设备通信目录:
+
+ {% for path in registry_info.device_comms_paths %}
+ -
+ {{ path }}
+ 📁
+
+ {% endfor %}
+
+
+ {% endif %} {% if registry_info.resources_paths %}
+
+
资源目录:
+
+ {% for path in registry_info.resources_paths %}
+ -
+ {{ path }}
+ 📁
+
+ {% endfor %}
+
+
+ {% endif %}
{% endif %}
-{% endblock %}
-
-{% block content %}
+{% endblock %} {% block content %}
{% if is_host_mode and host_node_info.available %}
-
主机节点信息
-
-
-
-
已管理设备 {{ host_node_info.devices|length }}
-
-
- | 设备ID |
- 命名空间 |
- 机器名称 |
- 状态 |
-
- {% for device_id, device_info in host_node_info.devices.items() %}
-
- | {{ device_id }} |
- {{ device_info.namespace }} |
- {{ device_info.machine_name }} |
- {{ "在线" if device_info.is_online else "离线" }} |
-
- {% else %}
-
- | 没有发现已管理的设备 |
-
- {% endfor %}
-
-
-
-
-
-
动作客户端 {{ host_node_info.action_clients|length }}
-
- 已接纳动作:
-
-
- | 话题 |
- 类型 |
- |
-
- {% for action_name, action_info in host_node_info.action_clients.items() %}
-
- | {{ action_name }} |
- {{ action_info.type_name }} |
- ▼ |
-
-
-
-
- 发送命令:
-
- ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_convert }} "{{ action_info.goal_info }}"
-
-
-
- 提示: 根据目标结构修改命令参数
-
- |
-
- {% endfor %}
-
-
-
-
-
-
-
已订阅主题 {{ host_node_info.subscribed_topics|length }}
-
- {% if host_node_info.subscribed_topics %}
-
- {% for topic in host_node_info.subscribed_topics %}
-
- {{ topic }}
-
-
- {% endfor %}
+
主机节点信息
+
+
+
+
+ 已管理设备
+ {{ host_node_info.devices|length }}
+
+
+
+ | 设备ID |
+ 命名空间 |
+ 机器名称 |
+ 状态 |
+
+ {% for device_id, device_info in host_node_info.devices.items() %}
+
+ | {{ device_id }} |
+ {{ device_info.namespace }} |
+ {{ device_info.machine_name }} |
+
+ {{ "在线" if device_info.is_online else "离线" }}
+ |
+
+ {% else %}
+
+ | 没有发现已管理的设备 |
+
+ {% endfor %}
+
+
+
+
+
+
+ 动作客户端
+ {{ host_node_info.action_clients|length }}
+
+
+ 已接纳动作:
+
+
+ | 话题 |
+ 类型 |
+ |
+
+ {% for action_name, action_info in host_node_info.action_clients.items()
+ %}
+
+ | {{ action_name }} |
+ {{ action_info.type_name }} |
+ ▼ |
+
+
+
+
+ 发送命令:
+
+
+ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_convert }} "{{ action_info.goal_info }}"
+
+
+
+ 提示: 根据目标结构修改命令参数
- {% else %}
- 没有发现已订阅的主题
- {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+ 已订阅主题
+ {{ host_node_info.subscribed_topics|length }}
+
+
+ {% if host_node_info.subscribed_topics %}
+
+ {% for topic in host_node_info.subscribed_topics %}
+
+ {{ topic }}
+
+ {% endfor %}
+
+ {% else %}
+
没有发现已订阅的主题
+ {% endif %}
-
-
- {% if host_node_info.device_status %}
-
-
设备状态
-
-
- | 设备ID |
- 属性 |
- 值 |
- 最后更新 |
-
- {% for device_id, properties in host_node_info.device_status.items() %}
- {% for prop_name, prop_value in properties.items() %}
-
- {% if loop.first %}
- | {{ device_id }} |
- {% endif %}
- {{ prop_name }} |
- {{ prop_value }} |
-
- {% if device_id in host_node_info.device_status_timestamps and prop_name in host_node_info.device_status_timestamps[device_id] %}
- {% set ts_info = host_node_info.device_status_timestamps[device_id][prop_name] %}
- {% if ts_info.elapsed >= 0 %}
- {{ ts_info.elapsed }} 秒前
- {% else %}
- 未更新
- {% endif %}
- {% else %}
- 无数据
- {% endif %}
- |
-
- {% endfor %}
- {% else %}
-
- | 没有设备状态数据 |
-
- {% endfor %}
-
-
- {% endif %}
+
+
+
+ {% if host_node_info.device_status %}
+
+
设备状态
+
+
+ | 设备ID |
+ 属性 |
+ 值 |
+ 最后更新 |
+
+ {% for device_id, properties in host_node_info.device_status.items() %} {%
+ for prop_name, prop_value in properties.items() %}
+
+ {% if loop.first %}
+ | {{ device_id }} |
+ {% endif %}
+ {{ prop_name }} |
+ {{ prop_value }} |
+
+ {% if device_id in host_node_info.device_status_timestamps and
+ prop_name in host_node_info.device_status_timestamps[device_id] %} {%
+ set ts_info =
+ host_node_info.device_status_timestamps[device_id][prop_name] %} {% if
+ ts_info.elapsed >= 0 %}
+ {{ ts_info.elapsed }} 秒前
+ {% else %}
+ 未更新
+ {% endif %} {% else %}
+ 无数据
+ {% endif %}
+ |
+
+ {% endfor %} {% else %}
+
+ | 没有设备状态数据 |
+
+ {% endfor %}
+
+
+ {% endif %}
{% endif %}
-
Local Devices
-
-
- | Device ID |
- 节点名称 |
- 命名空间 |
- 机器名称 |
- 状态项 |
- 动作数 |
-
- {% for device_id, device_info in ros_node_info.registered_devices.items() %}
- {% set device_loop_index = loop.index %}
-
- | {{ device_id }} |
- {{ device_info.node_name }} |
- {{ device_info.namespace }} |
- {{ device_info.machine_name|default("本地") }} |
- {{ ros_node_info.device_topics.get(device_id, {})|length }} |
- {{ ros_node_info.device_actions.get(device_id, {})|length }} ▼ |
-
-
-
-
- UUID: {{ device_info.uuid }}
- {% if device_id in ros_node_info.device_topics %}
- 已发布状态:
-
-
- | 名称 |
- 类型 |
- 话题 |
- 间隔 |
- |
-
- {% for status_name, status_info in ros_node_info.device_topics[device_id].items() %}
-
- | {{ status_name }} |
- {{ status_info.type_name }} |
- {{ status_info.topic_path }} |
- {{ status_info.timer_period }} |
- ▼ |
-
-
-
-
- 订阅命令:
-
- ros2 topic echo {{ status_info.topic_path }}
-
-
-
- |
-
- {% endfor %}
-
- {% endif %}
-
- {% if device_id in ros_node_info.device_actions %}
- 已发布动作:
-
-
- | 名称 |
- 类型 |
- 话题 |
- |
-
- {% for action_name, action_info in ros_node_info.device_actions[device_id].items() %}
-
- | {{ action_name }} |
- {{ action_info.type_name }} |
- {{ action_info.action_path }} |
- ▼ |
-
-
-
-
- 发送命令:
-
- ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_convert }} "{{ action_info.goal_info }}"
-
-
-
- 提示: 根据目标结构修改命令参数
-
- |
-
- {% endfor %}
-
- {% endif %}
+ Local Devices
+
+
+ | Device ID |
+ 节点名称 |
+ 命名空间 |
+ 机器名称 |
+ 状态项 |
+ 动作数 |
+
+ {% for device_id, device_info in ros_node_info.registered_devices.items() %}
+ {% set device_loop_index = loop.index %}
+
+ | {{ device_id }} |
+ {{ device_info.node_name }} |
+ {{ device_info.namespace }} |
+ {{ device_info.machine_name|default("本地") }} |
+ {{ ros_node_info.device_topics.get(device_id, {})|length }} |
+
+ {{ ros_node_info.device_actions.get(device_id, {})|length }}
+ ▼
+ |
+
+
+
+
+ UUID: {{ device_info.uuid }} {% if device_id in
+ ros_node_info.device_topics %}
+ 已发布状态:
+
+
+ | 名称 |
+ 类型 |
+ 话题 |
+ 间隔 |
+ |
+
+ {% for status_name, status_info in
+ ros_node_info.device_topics[device_id].items() %}
+
+ | {{ status_name }} |
+ {{ status_info.type_name }} |
+ {{ status_info.topic_path }} |
+ {{ status_info.timer_period }} |
+ ▼ |
+
+
+
+
+ 订阅命令:
+
+ ros2 topic echo {{ status_info.topic_path }}
+
+
- |
-
- {% endfor %}
-
+ |
+
+ {% endfor %}
+
+ {% endif %} {% if device_id in ros_node_info.device_actions %}
+ 已发布动作:
+
+
+ | 名称 |
+ 类型 |
+ 话题 |
+ |
+
+ {% for action_name, action_info in
+ ros_node_info.device_actions[device_id].items() %}
+
+ | {{ action_name }} |
+ {{ action_info.type_name }} |
+ {{ action_info.action_path }} |
+ ▼ |
+
+
+
+
+ 发送命令:
+
+
+ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_convert }} "{{ action_info.goal_info }}"
+
+
+
+ 提示: 根据目标结构修改命令参数
+
+ |
+
+ {% endfor %}
+
+ {% endif %}
+
+ |
+
+ {% endfor %}
+
-
Device Types
+
+
-
- | ID |
- Name |
- File Path |
- |
-
- {% for device in devices %}
-
- | {{ device.id }} |
- {{ device.name }} |
-
- {{ device.file_path }}
- 📁
- |
- ▼ |
-
-
-
-
- {% if device.class %}
- {{ device.class | tojson(indent=4) }}
- {% else %}
-
- // No data
- {% endif %}
+
+ | ID |
+ Name |
+ File Path |
+ |
+
+ {% for device in devices %}
+
+ | {{ device.id }} |
+ {{ device.name }} |
+
+ {{ device.file_path }}
+ 📁
+ |
+ ▼ |
+
+
+
+
+ {% if device.class %}
+ {{ device.class | tojson(indent=4) }}
+ {% else %}
+
+ // No data
+ {% endif %} {% if device.is_online %}
+
+ 在线
+
+ {% endif %} {% if device.is_online and device.status_publishers %}
+ 状态发布者:
+
+ {% for status_name, status_info in
+ device.status_publishers.items() %}
+ -
+ {{ status_name }} - 类型: {{ status_info.type
+ }}
话题: {{ status_info.topic }}
+
+ {% endfor %}
+
+ {% endif %} {% if device.is_online and device.actions %}
+ 可用动作:
+
+ {% for action_name, action_info in device.actions.items() %}
+ -
+ {{ action_name }} - 类型: {{ action_info.type
+ }}
话题: {{ action_info.topic }}
+
+
+ 发送命令:
+
+ {{ action_info.command }}
+
+
+
+ {% if action_info %}
+ {{ action_info | tojson(indent=4) }}
+ {% else %}
+
+ // No data
+ {% endif %}
+
+
- {% if device.is_online %}
- 在线
- {% endif %}
-
- {% if device.is_online and device.status_publishers %}
- 状态发布者:
-
- {% for status_name, status_info in device.status_publishers.items() %}
- -
- {{ status_name }} - 类型: {{ status_info.type }}
-
话题: {{ status_info.topic }}
-
- {% endfor %}
-
- {% endif %}
-
- {% if device.is_online and device.actions %}
- 可用动作:
-
- {% for action_name, action_info in device.actions.items() %}
- -
- {{ action_name }} - 类型: {{ action_info.type }}
-
话题: {{ action_info.topic }}
-
-
- 发送命令:
-
- {{ action_info.command }}
-
-
-
- {% if action_info %}
- {{ action_info | tojson(indent=4) }}
- {% else %}
-
- // No data
- {% endif %}
-
-
-
- 提示: 根据目标结构修改命令参数
-
-
- {% endfor %}
-
- {% endif %}
+ 提示: 根据目标结构修改命令参数
- |
-
- {% endfor %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ |
+
+ {% endfor %}
+
-
Resource Types
+
+
-
- | ID |
- Name |
- File Path |
-
- {% for resource in resources %}
-
- | {{ resource.id }} |
- {{ resource.name }} |
-
- {{ resource.file_path }}
- 📁
- |
-
- {% endfor %}
+
+ | ID |
+ Name |
+ File Path |
+
+ {% for resource in resources %}
+
+ | {{ resource.id }} |
+ {{ resource.name }} |
+
+ {{ resource.file_path }}
+ 📁
+ |
+
+ {% endfor %}
+
-
Converter Modules
+
+
Loaded Modules
-
- | Module Path |
-
- {% for module in modules.names %}
-
- | {{ module }} |
-
- {% endfor %}
+
+ | Module Path |
+
+ {% for module in modules.names %}
+
+ | {{ module }} |
+
+ {% endfor %}
-
Available Classes
- ({{ modules.total_count }})
+
+ Available Classes
+ ({{ modules.total_count }})
-
- | Class Name |
-
- {% for class_name in modules.classes %}
-
- | {{ class_name }} |
-
- {% endfor %}
+
+ | Class Name |
+
+ {% for class_name in modules.classes %}
+
+ | {{ class_name }} |
+
+ {% endfor %}
+
-{% endblock %}
-
-{% block scripts %}
-{{ super() }}
+{% endblock %} {% block scripts %} {{ super() }}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
From 361eae2f6dc4e6c59a9593283efa35530dc16ee3 Mon Sep 17 00:00:00 2001
From: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Date: Sun, 7 Sep 2025 20:57:48 +0800
Subject: [PATCH 3/3] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E7=BC=96?=
=?UTF-8?q?=E8=BE=91=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
unilabos/app/web/api.py | 283 +++++++++----
unilabos/app/web/templates/home.html | 2 +-
.../app/web/templates/registry_editor.html | 382 ++++++++++++++++--
unilabos/app/web/templates/status.html | 5 +-
unilabos/registry/registry.py | 2 +-
5 files changed, 571 insertions(+), 103 deletions(-)
diff --git a/unilabos/app/web/api.py b/unilabos/app/web/api.py
index bf51f4a..95276ac 100644
--- a/unilabos/app/web/api.py
+++ b/unilabos/app/web/api.py
@@ -7,6 +7,8 @@ API模块
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import asyncio
+import yaml
+
from unilabos.app.controler import devices, job_add, job_info
from unilabos.app.model import (
Resp,
@@ -19,6 +21,8 @@ from unilabos.app.model import (
JobFinishReq,
)
from unilabos.app.web.utils.host_utils import get_host_node_info
+from unilabos.registry.registry import lab_registry
+from unilabos.utils.type_check import NoAliasDumper
# 创建API路由器
api = APIRouter()
@@ -603,6 +607,7 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
file_size = request_data.get("file_size", 0)
registry_type = request_data.get("registry_type", "device")
class_name = request_data.get("class_name")
+ module_prefix = request_data.get("module_prefix", "")
async def send_log(message: str, level: str = "info"):
"""发送日志消息到客户端"""
@@ -656,7 +661,12 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
# 确定模块名
module_name = file_name.replace(".py", "").replace("-", "_").replace(" ", "_")
+
+ # 如果有 module_prefix,则使用完整的模块路径
+ full_module_name = f"{module_prefix}.{module_name}" if module_prefix else module_name
await send_log(f"使用模块名: {module_name}")
+ if module_prefix:
+ await send_log(f"完整模块路径: {full_module_name}")
# 导入模块
try:
@@ -697,7 +707,7 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
from unilabos.utils.import_manager import get_enhanced_class_info
# 分析类信息
- enhanced_info = get_enhanced_class_info(f"{module_name}:{class_name}", use_dynamic=True)
+ enhanced_info = get_enhanced_class_info(f"{full_module_name}:{class_name}", use_dynamic=True)
if not enhanced_info.get("dynamic_import_success", False):
await send_error("动态导入类信息失败")
@@ -705,47 +715,102 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
await send_log("成功分析类信息")
- # 生成注册表schema
- registry_schema = {
- "class_name": class_name,
- "module": f"{module_name}:{class_name}",
- "type": "python",
- "description": enhanced_info.get("class_docstring", ""),
- "version": "1.0.0",
- "category": [registry_type],
- "status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
- "action_value_mappings": {},
- "init_param_schema": {},
- "registry_type": registry_type,
- "file_path": f"uploaded_file://{file_name}",
- }
-
- # 处理动作方法
- for method_name, method_info in enhanced_info["action_methods"].items():
- registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
- "type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
- "goal": {},
- "feedback": {},
- "result": {},
- "args": method_info["args"],
- "description": method_info.get("docstring", ""),
+ # 根据注册表类型生成不同的schema
+ if registry_type == "resource":
+ # 资源类型的简单结构
+ category_name = file_name.replace(".py", "") if file_name else "unknown"
+ registry_schema = {
+ "description": enhanced_info.get("class_docstring", ""),
+ "category": [category_name],
+ "class": {
+ "module": f"{full_module_name}:{class_name}",
+ "type": "python",
+ },
+ "handles": [],
+ "icon": "",
+ "init_param_schema": {},
+ "registry_type": "resource",
+ "version": "1.0.0",
+ "file_path": f"uploaded_file://{file_name}",
+ }
+ else:
+ # 设备类型的复杂结构
+ registry_schema = {
+ "description": enhanced_info.get("class_docstring", ""),
+ "class": {
+ "module": f"{full_module_name}:{class_name}",
+ "type": "python",
+ "status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
+ "action_value_mappings": {},
+ },
+ "version": "1.0.0",
+ "handles": [],
+ "init_param_schema": {},
+ "registry_type": "device",
+ "file_path": f"uploaded_file://{file_name}",
}
+ # 处理动作方法(仅对设备类型)
+ for method_name, method_info in enhanced_info["action_methods"].items():
+ registry_schema["class"]["action_value_mappings"][f"auto-{method_name}"] = {
+ "type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
+ "goal": {},
+ "feedback": {},
+ "result": {},
+ "args": method_info["args"],
+ "description": method_info.get("docstring", ""),
+ }
+
await send_log("成功生成注册表schema")
+ # 格式化状态方法信息
+ status_info = {}
+ for status_name, status_data in enhanced_info.get("status_methods", {}).items():
+ status_info[status_name] = {
+ "return_type": status_data.get("return_type", "未知类型"),
+ "docstring": status_data.get("docstring", "无描述"),
+ "is_property": status_data.get("is_property", False),
+ }
+
+ # 格式化动作方法信息
+ action_info = {}
+ for action_name, action_data in enhanced_info.get("action_methods", {}).items():
+ args = action_data.get("args", [])
+ action_info[action_name] = {
+ "param_count": len(args),
+ "params": [
+ {"name": arg.get("name", ""), "type": arg.get("type", ""), "default": arg.get("default")}
+ for arg in args
+ ],
+ "is_async": action_data.get("is_async", False),
+ "docstring": action_data.get("docstring", "无描述"),
+ "return_suggestion": "建议返回字典类型 (dict) 以便更好地结构化结果数据",
+ }
+
# 准备结果数据
result = {
"class_info": {
"class_name": class_name,
"module_name": module_name,
+ "module_prefix": module_prefix,
+ "full_module_name": full_module_name,
"file_name": file_name,
"file_size": file_size,
"docstring": enhanced_info.get("class_docstring", ""),
"dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
+ "registry_type": registry_type,
},
"registry_schema": registry_schema,
- "action_methods": enhanced_info["action_methods"],
- "status_methods": enhanced_info["status_methods"],
+ "class_analysis": {
+ "status_methods": status_info,
+ "action_methods": action_info,
+ "init_params": enhanced_info.get("init_params", []),
+ "status_methods_count": len(status_info),
+ "action_methods_count": len(action_info),
+ },
+ # 保持向后兼容
+ "action_methods": enhanced_info.get("action_methods", {}),
+ "status_methods": enhanced_info.get("status_methods", {}),
}
# 发送结果
@@ -794,6 +859,11 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
registry_type = request_data.get("registry_type", "device")
class_name = request_data.get("class_name")
module_name = request_data.get("module_name")
+ description = request_data.get("description", "")
+ safe_class_name = request_data.get("safe_class_name", "")
+ icon = request_data.get("icon", "")
+ module_prefix = request_data.get("module_prefix", "")
+ handles = request_data.get("handles", [])
async def send_log(message: str, level: str = "info"):
"""发送日志消息到客户端"""
@@ -862,7 +932,12 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
# 确定模块名
if not module_name:
module_name = full_file_path.stem
+
+ # 如果有 module_prefix,则使用完整的模块路径
+ full_module_name = f"{module_prefix}.{module_name}" if module_prefix else module_name
await send_log(f"使用模块名: {module_name}")
+ if module_prefix:
+ await send_log(f"完整模块路径: {full_module_name}")
# 导入模块
try:
@@ -930,7 +1005,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
from unilabos.utils.import_manager import get_enhanced_class_info
# 分析类信息
- enhanced_info = get_enhanced_class_info(f"{module_name}:{target_class_name}", use_dynamic=True)
+ enhanced_info = get_enhanced_class_info(f"{full_module_name}:{target_class_name}", use_dynamic=True)
if not enhanced_info.get("dynamic_import_success", False):
await send_error("动态导入类信息失败")
@@ -938,55 +1013,124 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
await send_log("成功分析类信息")
- # 生成注册表schema
- registry_schema = {
- "class_name": target_class_name,
- "module": f"{module_name}:{target_class_name}",
- "type": "python",
- "description": enhanced_info.get("class_docstring", ""),
- "version": "1.0.0",
- "category": [registry_type],
- "status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
- "action_value_mappings": {},
- "init_param_schema": {},
- "registry_type": registry_type,
- "file_path": str(full_file_path),
- }
-
- # 处理动作方法
- for method_name, method_info in enhanced_info["action_methods"].items():
- registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
- "type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
- "goal": {},
- "feedback": {},
- "result": {},
- "args": method_info["args"],
- "description": method_info.get("docstring", ""),
+ # 根据注册表类型生成不同的schema
+ if registry_type == "resource":
+ # 资源类型的简单结构
+ category_name = Path(file_path).stem if file_path else "unknown"
+ registry_schema = {
+ "description": description or enhanced_info.get("class_docstring", ""),
+ "category": [category_name],
+ "class": {
+ "module": f"{full_module_name}:{target_class_name}",
+ "type": "python",
+ },
+ "handles": handles,
+ "icon": icon,
+ "init_param_schema": {},
+ "registry_type": "resource",
+ "version": "1.0.0",
+ }
+ else:
+ # 设备类型的复杂结构
+ registry_schema = {
+ "description": description or enhanced_info.get("class_docstring", ""),
+ "class": {
+ "module": f"{full_module_name}:{target_class_name}",
+ "type": "python",
+ "status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
+ "action_value_mappings": {
+ f"auto-{k}": {
+ "type": "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand",
+ "goal": {},
+ "feedback": {},
+ "result": {},
+ "schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
+ "goal_default": {i["name"]: i["default"] for i in v["args"]},
+ "handles": [],
+ }
+ # 不生成已配置action的动作
+ for k, v in enhanced_info["action_methods"].items()
+ },
+ },
+ "version": "1.0.0",
+ "handles": handles,
+ "icon": icon,
+ "init_param_schema": {
+ "config": lab_registry._generate_unilab_json_command_schema(
+ enhanced_info["init_params"], "__init__"
+ )["properties"]["goal"],
+ "data": lab_registry._generate_status_types_schema(enhanced_info["status_methods"]),
+ },
+ "registry_type": "device",
}
await send_log("成功生成注册表schema")
- # 转换为YAML格式
- import yaml
- from unilabos.utils.type_check import NoAliasDumper
-
- # 创建最终的YAML配置(使用设备ID作为根键)
- class_name_safe = class_name or "unknown"
- suffix = "_device" if registry_type == "device" else "_resource"
- device_id = f"{class_name_safe.lower()}{suffix}"
- final_config = {device_id: registry_schema}
+ # 创建最终的YAML配置(使用ID作为根键)
+ if safe_class_name:
+ item_id = safe_class_name
+ else:
+ class_name_safe = (target_class_name or "unknown").lower()
+ if registry_type == "resource":
+ # 资源ID通常直接使用类名,不加后缀
+ item_id = class_name_safe
+ else:
+ # 设备ID使用类名加_device后缀
+ item_id = f"{class_name_safe}_device"
+ final_config = {item_id: registry_schema}
yaml_content = yaml.dump(
final_config, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper, sort_keys=True
)
- # 准备结果数据(只保留YAML结果)
+ # 格式化状态方法信息
+ status_info = {}
+ for status_name, status_data in enhanced_info.get("status_methods", {}).items():
+ status_info[status_name] = {
+ "return_type": status_data.get("return_type", "未知类型"),
+ "docstring": status_data.get("docstring", "无描述"),
+ "is_property": status_data.get("is_property", False),
+ }
+
+ # 格式化动作方法信息
+ action_info = {}
+ for action_name, action_data in enhanced_info.get("action_methods", {}).items():
+ args = action_data.get("args", [])
+ action_info[action_name] = {
+ "param_count": len(args),
+ "params": [
+ {"name": arg.get("name", ""), "type": arg.get("type", ""), "default": arg.get("default")}
+ for arg in args
+ ],
+ "is_async": action_data.get("is_async", False),
+ "docstring": action_data.get("docstring", "无描述"),
+ "return_suggestion": "建议返回字典类型 (dict) 以便更好地结构化结果数据",
+ }
+
+ # 准备结果数据(包含详细的类分析信息)
result = {
"registry_schema": yaml_content,
- "device_id": device_id,
- "class_name": class_name,
+ "item_id": item_id,
+ "registry_type": registry_type,
+ "class_name": target_class_name,
"module_name": module_name,
"file_path": file_path,
+ "config_params": {
+ "safe_class_name": safe_class_name or item_id,
+ "description": description,
+ "icon": icon,
+ "module_prefix": module_prefix,
+ "full_module_name": full_module_name,
+ "handles_count": len(handles),
+ "handles": handles,
+ },
+ "class_analysis": {
+ "class_docstring": enhanced_info.get("class_docstring", ""),
+ "status_methods": status_info,
+ "action_methods": action_info,
+ "init_params": enhanced_info.get("init_params", []),
+ "dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
+ },
}
# 发送结果
@@ -1034,12 +1178,11 @@ def get_file_browser_data(path: str = ""):
items = []
parent_path = target_path.parent
- relative_parent = parent_path.relative_to(working_dir)
items.append(
{
"name": "..",
"type": "directory",
- "path": str(relative_parent) if relative_parent != Path(".") else "",
+ "path": str(parent_path),
"size": 0,
"is_parent": True,
}
@@ -1048,16 +1191,12 @@ def get_file_browser_data(path: str = ""):
# 获取子目录和文件
try:
for item in sorted(target_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
- if item.name.startswith("."): # 跳过隐藏文件
- continue
-
item_type = "directory" if item.is_dir() else "file"
- relative_path = item.relative_to(working_dir)
item_info = {
"name": item.name,
"type": item_type,
- "path": str(relative_path),
+ "path": str(item),
"size": item.stat().st_size if item.is_file() else 0,
"is_python": item.suffix == ".py" if item.is_file() else False,
"is_parent": False,
@@ -1068,7 +1207,7 @@ def get_file_browser_data(path: str = ""):
return Resp(
data={
- "current_path": str(target_path.relative_to(working_dir)) if target_path != working_dir else "",
+ "current_path": str(target_path),
"working_dir": str(working_dir),
"items": items,
}
diff --git a/unilabos/app/web/templates/home.html b/unilabos/app/web/templates/home.html
index 2517f1c..ac716e5 100644
--- a/unilabos/app/web/templates/home.html
+++ b/unilabos/app/web/templates/home.html
@@ -8,7 +8,7 @@ header %}UniLab API{% endblock %} {% block nav %}
target="_blank"
>主页
- class="nav-tab">状态
+
状态
注册表编辑
{% endblock %} {% block content %}
diff --git a/unilabos/app/web/templates/registry_editor.html b/unilabos/app/web/templates/registry_editor.html
index 8e23b39..4cb59c7 100644
--- a/unilabos/app/web/templates/registry_editor.html
+++ b/unilabos/app/web/templates/registry_editor.html
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% block title %}注册表编辑器 - UniLab{% endblock %}
-{% block header %}注册表编辑器{% endblock %} {% block nav %}
-{% endblock %} {% block scripts %}
+{% block header %}注册表编辑器{% endblock %} {% block nav %} {% endblock %} {%
+block scripts %}