diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0fdd56a --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NODE_DEFINITION=definitions/ur_module.node.yaml +UR_IP= diff --git a/.gitignore b/.gitignore index 602a9be..24bc473 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ python-urx/ #VScode .vscode/ + +.DS_Store diff --git a/compose.yaml b/compose.yaml index 3282163..1583041 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,6 +4,7 @@ services: container_name: ur image: ghcr.io/ad-sdl/ur_module network_mode: host + env_file: .env environment: - USER_ID=${USER_ID:-1000} - GROUP_ID=${GROUP_ID:-1000} diff --git a/definitions/ur_module.info.yaml b/definitions/ur_module.info.yaml new file mode 100644 index 0000000..677e2b1 --- /dev/null +++ b/definitions/ur_module.info.yaml @@ -0,0 +1,1005 @@ +node_name: ur_module +node_id: 01JSQ1E0GM1RGT10CS1ETK9BNH +node_description: REST API node for ur module +node_type: device +module_name: ur_module +module_version: 0.0.1 +capabilities: + get_info: true + get_state: true + get_status: true + send_action: true + get_action_status: true + get_action_result: true + get_action_history: true + action_files: true + send_admin_commands: true + set_config: true + get_resources: false + get_log: true + admin_commands: + - get_location + - lock + - reset + - safety_stop + - shutdown + - unlock +node_url: null +actions: + getj: + name: getj + description: Get joint angles + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + getl: + name: getl + description: Get linear positions + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + set_freedrive: + name: set_freedrive + description: Free robot joints + args: + timeout: + name: timeout + description: how long to do freedrive + argument_type: int + required: false + default: 60 + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + set_movement_params: + name: set_movement_params + description: Set speed and acceleration parameters + args: + tcp_pose: + name: tcp_pose + description: '' + argument_type: list + required: false + default: null + velocity: + name: velocity + description: '' + argument_type: float + required: false + default: null + acceleration: + name: acceleration + description: '' + argument_type: float + required: false + default: null + gripper_speed: + name: gripper_speed + description: '' + argument_type: int + required: false + default: null + gripper_force: + name: gripper_force + description: '' + argument_type: int + required: false + default: null + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + movej: + name: movej + description: Move the robot using joint angles + args: + acceleration: + name: acceleration + description: Acceleration + argument_type: float + required: false + default: 0.6 + velocity: + name: velocity + description: Velocity + argument_type: float + required: false + default: 0.6 + locations: + joints: + name: joints + description: Joint angles to move to + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + movel: + name: movel + description: Move the robot using linar motion + args: + acceleration: + name: acceleration + description: Acceleration + argument_type: float + required: false + default: 0.6 + velocity: + name: velocity + description: Velocity + argument_type: float + required: false + default: 0.6 + locations: + target: + name: target + description: Linear location to move to + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + toggle_gripper: + name: toggle_gripper + description: Move the robot gripper + args: + open: + name: open + description: Open? + argument_type: bool + required: false + default: false + close: + name: close + description: Close? + argument_type: bool + required: false + default: false + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + gripper_transfer: + name: gripper_transfer + description: Execute a transfer in between source and target locations using Robotiq + grippers + args: + source_approach_axis: + name: source_approach_axis + description: Source location approach axis, (X/Y/Z) + argument_type: str + required: false + default: z + target_approach_axis: + name: target_approach_axis + description: Source location approach axis, (X/Y/Z) + argument_type: str + required: false + default: z + source_approach_distance: + name: source_approach_distance + description: Approach distance in meters + argument_type: float + required: false + default: 0.05 + target_approach_distance: + name: target_approach_distance + description: Approach distance in meters + argument_type: float + required: false + default: 0.05 + gripper_open: + name: gripper_open + description: Set a max value for the gripper open state + argument_type: int + required: false + default: 0 + gripper_close: + name: gripper_close + description: Set a min value for the gripper close state + argument_type: int + required: false + default: 255 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + source: + name: source + description: Location to transfer sample from + argument_type: location + required: true + default: null + target: + name: target + description: Location to transfer sample to + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + gripper_pick: + name: gripper_pick + description: Use the gripper to pick a piece of labware from the specified source + args: + source_approach_axis: + name: source_approach_axis + description: Source location approach axis, (X/Y/Z) + argument_type: str + required: false + default: z + source_approach_distance: + name: source_approach_distance + description: Approach distance in meters + argument_type: float + required: false + default: 0.05 + gripper_close: + name: gripper_close + description: Set a min value for the gripper close state + argument_type: int + required: false + default: 255 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + source: + name: source + description: Location to transfer sample from + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + gripper_place: + name: gripper_place + description: Use the gripper to place a piece of labware at the target. + args: + target_approach_axis: + name: target_approach_axis + description: Source location approach axis, (X/Y/Z) + argument_type: str + required: false + default: z + target_approach_distance: + name: target_approach_distance + description: Approach distance in meters + argument_type: float + required: false + default: 0.05 + gripper_open: + name: gripper_open + description: Set a max value for the gripper open state + argument_type: int + required: false + default: 0 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + target: + name: target + description: Location to transfer sample to + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + pick_tool: + name: pick_tool + description: Picks up a tool using the provided tool location + args: + docking_axis: + name: docking_axis + description: Docking axis, (X/Y/Z) + argument_type: str + required: false + default: y + payload: + name: payload + description: Tool payload + argument_type: float + required: false + default: null + tool_name: + name: tool_name + description: Tool name) + argument_type: str + required: false + default: null + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + tool_loc: + name: tool_loc + description: Tool location + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + Place_tool: + name: Place_tool + description: Places the attached tool back to the provided tool docking location + args: + docking_axis: + name: docking_axis + description: Docking axis, (X/Y/Z) + argument_type: str + required: false + default: y + tool_name: + name: tool_name + description: Tool name) + argument_type: str + required: false + default: null + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + tool_docking: + name: tool_docking + description: Tool docking location + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + gripper_screw_transfer: + name: gripper_screw_transfer + description: Performs a screw transfer using the Robotiq gripper and custom screwdriving + bits + args: + screw_time: + name: screw_time + description: Srew time in seconds + argument_type: int + required: false + default: 9 + gripper_open: + name: gripper_open + description: Set a max value for the gripper open state + argument_type: int + required: false + default: 0 + gripper_close: + name: gripper_close + description: Set a min value for the gripper close state + argument_type: int + required: false + default: 255 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + screwdriver_loc: + name: screwdriver_loc + description: Screwdriver location + argument_type: location + required: true + default: null + screw_loc: + name: screw_loc + description: Screw location + argument_type: location + required: true + default: null + target: + name: target + description: Location where the srewdriving will be performed + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + pipette_transfer: + name: pipette_transfer + description: Make a pipette transfer to transfer sample liquids in between two + locations + args: + volume: + name: volume + description: Set a volume in micro liters + argument_type: float + required: true + default: null + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + source: + name: source + description: Initial location of the sample + argument_type: location + required: true + default: null + target: + name: target + description: Target location of the sample + argument_type: location + required: true + default: null + tip_loc: + name: tip_loc + description: New tip location + argument_type: location + required: true + default: null + tip_trash: + name: tip_trash + description: Tip trash location + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + pipette_pick_and_move_sample: + name: pipette_pick_and_move_sample + description: Picks and moves a sample using the pipette + args: + volume: + name: volume + description: Set a volume in micro liters + argument_type: int + required: false + default: 10 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + pipette_speed: + name: pipette_speed + description: Pipette speed in m/s + argument_type: int + required: false + default: 150 + locations: + home: + name: home + description: Home location in joint angles + argument_type: location + required: true + default: null + sample_loc: + name: sample_loc + description: Sample location + argument_type: location + required: true + default: null + target: + name: target + description: Location of the object + argument_type: location + required: true + default: null + safe_waypoint: + name: safe_waypoint + description: Safe waypoint in joint angles + argument_type: location + required: false + default: null + tip_loc: + name: tip_loc + description: Tip location + argument_type: location + required: false + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + pipette_dispense_and_retrieve: + name: pipette_dispense_and_retrieve + description: Dispenses a sample and retrieves the pipette tip + args: + volume: + name: volume + description: Set a volume in micro liters + argument_type: int + required: false + default: 10 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + pipette_speed: + name: pipette_speed + description: Pipette speed in m/s + argument_type: int + required: false + default: 150 + locations: + home: + name: home + description: Home location in joint angles + argument_type: location + required: true + default: null + target: + name: target + description: Location of the object + argument_type: location + required: true + default: null + safe_waypoint: + name: safe_waypoint + description: Safe waypoint in joint angles + argument_type: location + required: false + default: null + tip_trash: + name: tip_trash + description: Tip trash location + argument_type: location + required: false + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + pick_and_flip_object: + name: pick_and_flip_object + description: Picks and flips an object 180 degrees + args: + approach_axis: + name: approach_axis + description: Approach axis, (X/Y/Z) + argument_type: str + required: false + default: z + target_approach_distance: + name: target_approach_distance + description: Approach distance in meters + argument_type: float + required: false + default: 0.05 + gripper_open: + name: gripper_open + description: Set a max value for the gripper open state + argument_type: int + required: false + default: 0 + gripper_close: + name: gripper_close + description: Set a min value for the gripper close state + argument_type: int + required: false + default: 255 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + target: + name: target + description: Location of the object + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + remove_cap: + name: remove_cap + description: Removes caps from sample vials + args: + gripper_open: + name: gripper_open + description: Set a max value for the gripper open state + argument_type: int + required: false + default: 0 + gripper_close: + name: gripper_close + description: Set a min value for the gripper close state + argument_type: int + required: false + default: 255 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + source: + name: source + description: Location of the vial cap + argument_type: location + required: true + default: null + target: + name: target + description: Location of where the cap will be placed after it is removed + from the vail + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + place_cap: + name: place_cap + description: Places caps back to sample vials + args: + gripper_open: + name: gripper_open + description: Set a max value for the gripper open state + argument_type: int + required: false + default: 0 + gripper_close: + name: gripper_close + description: Set a min value for the gripper close state + argument_type: int + required: false + default: 255 + joint_angle_locations: + name: joint_angle_locations + description: Use joint angles for all the locations + argument_type: bool + required: false + default: true + locations: + home: + name: home + description: Home location + argument_type: location + required: true + default: null + source: + name: source + description: Vail cap initial location + argument_type: location + required: true + default: null + target: + name: target + description: The vail location where the cap will installed + argument_type: location + required: true + default: null + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + run_urp_program: + name: run_urp_program + description: Runs a URP program on the UR + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + set_digital_io: + name: set_digital_io + description: Sets a channel IO output on the UR + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + e_stop: + name: e_stop + description: Emergency stop the UR robot + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null +config: + node_definition: definitions/ur_module.node.yaml + node_info_path: null + update_node_files: true + status_update_interval: 2.0 + state_update_interval: 2.0 + node_url: http://127.0.0.1:2000/ + uvicorn_kwargs: {} + ur_ip: 146.137.240.38 + tcp_pose: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + base_reference_frame: null + ur_model: UR5e + use_resources: false +config_schema: + description: Configuration for the UR node module. + properties: + node_definition: + anyOf: + - type: string + - format: path + type: string + - type: 'null' + default: default.node.yaml + description: Path to the node definition file to use. If set, the node will + load the definition from this file on startup. Otherwise, a default configuration + will be created. + title: Node Definition File + node_info_path: + anyOf: + - type: string + - format: path + type: string + - type: 'null' + default: null + description: Path to export the generated node info file. If not set, will use + the node name and the node_definition's path. + title: Node Info Path + update_node_files: + default: true + description: Whether to update the node definition and info files on startup. + If set to False, the node will not update the files even if they are out of + date. + title: Update Node Files + type: boolean + status_update_interval: + anyOf: + - type: number + - type: 'null' + default: 2.0 + description: The interval in seconds at which the node should update its status. + title: Status Update Interval + state_update_interval: + anyOf: + - type: number + - type: 'null' + default: 2.0 + description: The interval in seconds at which the node should update its state. + title: State Update Interval + node_url: + default: http://127.0.0.1:2000/ + description: The URL used to communicate with the node. This is the base URL + for the REST API. + format: uri + minLength: 1 + title: Node URL + type: string + uvicorn_kwargs: + additionalProperties: true + description: Configuration for the Uvicorn server that runs the REST API. + title: Uvicorn Configuration + type: object + ur_ip: + anyOf: + - type: string + - type: 'null' + default: null + title: Ur Ip + tcp_pose: + default: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + items: {} + title: Tcp Pose + type: array + base_reference_frame: + anyOf: + - items: {} + type: array + - type: 'null' + default: null + title: Base Reference Frame + ur_model: + default: UR5e + title: Ur Model + type: string + use_resources: + default: true + title: Use Resources + type: boolean + title: URNodeConfig + type: object diff --git a/definitions/ur_module.node.yaml b/definitions/ur_module.node.yaml index cbce149..e0b6743 100644 --- a/definitions/ur_module.node.yaml +++ b/definitions/ur_module.node.yaml @@ -1,6 +1,5 @@ node_name: ur_module node_id: 01JSQ1E0GM1RGT10CS1ETK9BNH -node_url: null node_description: REST API node for ur module node_type: device module_name: ur_module @@ -23,17 +22,3 @@ capabilities: - reset - shutdown - unlock -commands: {} -is_template: false -config_defaults: - host: localhost - port: 3030 - ur_ip: 192.168.100.109 - ur_model: UR5e - tcp_pose: - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 diff --git a/pdm.lock b/pdm.lock index 9088d03..50302ea 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:a5e19723f63a6e0207ce85c0effa1e44b342ebda30b6dafb82c63c87be7ace1c" +content_hash = "sha256:85c1010aad56dfe57a61bde45cfc44523721a17eebda7a17ee5c9fdaf9cb396d" [[metadata.targets]] requires_python = ">=3.9.21" @@ -316,6 +316,21 @@ files = [ {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] +[[package]] +name = "classy-fastapi" +version = "0.7.0" +requires_python = ">=3.9" +summary = "Class based routing for FastAPI" +groups = ["default"] +dependencies = [ + "fastapi<1.0.0,>=0.73.0", + "pydantic<3.0.0,>=2.8", +] +files = [ + {file = "classy_fastapi-0.7.0-py3-none-any.whl", hash = "sha256:eea211181d065fecb543d68b75a6a6ab93393bb7ca48551887fa898ba7d40ec3"}, + {file = "classy_fastapi-0.7.0.tar.gz", hash = "sha256:4ab934fc309b134cd8dde5f7d6f98e1303a111cf1341ebd5bf900a630d20b084"}, +] + [[package]] name = "click" version = "8.1.8" @@ -630,6 +645,17 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "invoke" +version = "2.2.1" +requires_python = ">=3.6" +summary = "Pythonic task execution" +groups = ["default"] +files = [ + {file = "invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8"}, + {file = "invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707"}, +] + [[package]] name = "linkify-it-py" version = "2.0.3" @@ -646,9 +672,9 @@ files = [ [[package]] name = "madsci-client" -version = "0.3.1" +version = "0.5.3" requires_python = ">=3.9.1" -summary = "The Modular Autonomous Discovery for Science (MADSci) Python Client and CLI." +summary = "The Modular Autonomous Discovery for Science (MADSci) Python Clients." groups = ["default"] dependencies = [ "click>=8.1.7", @@ -657,50 +683,55 @@ dependencies = [ "trogon>=0.6.0", ] files = [ - {file = "madsci_client-0.3.1-py3-none-any.whl", hash = "sha256:728dbda68b76eae6347b36e33d9423f833abaf98e43cdfdfc6f12e08cef16a7c"}, - {file = "madsci_client-0.3.1.tar.gz", hash = "sha256:437a1c44de3c41aa65c7c42460ad45bfb9183a322c6b71a2a77f4ef5dbd8ef4e"}, + {file = "madsci_client-0.5.3-py3-none-any.whl", hash = "sha256:1b611f53c0adb1a0e4b14b820e1159978f91ecd7437fab10001f52ab0e663894"}, + {file = "madsci_client-0.5.3.tar.gz", hash = "sha256:d7335229f2d7ad7a32baffcd26bb8753098a00af59b1b0ebb38d5c3da2faf5dc"}, ] [[package]] name = "madsci-common" -version = "0.3.1" +version = "0.5.3" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Common Definitions and Utilities." groups = ["default"] dependencies = [ "PyYAML>=6.0.2", "aenum>=3.1.15", + "classy-fastapi>=0.7.0", "fastapi>=0.115.4", "multiprocess>=0.70.17", "pydantic-extra-types>=2.10.2", + "pydantic-settings-export>=1.0.2", + "pydantic-settings>=2.9.1", "pydantic>=2.10", "pymongo>=4.10.1", "python-dotenv>=1.0.1", "python-multipart>=0.0.17", "python-ulid[pydantic]>=3.0.0", + "regex>=2025.7.34", "requests>=2.32.3", "semver>=3.0.4", "sqlmodel>=0.0.22", "uvicorn[standard]>=0.32.0", ] files = [ - {file = "madsci_common-0.3.1-py3-none-any.whl", hash = "sha256:6954571ae178c999e78c370b97ec23a0b65fbf1c6de01f0cc8810d5f434268d4"}, - {file = "madsci_common-0.3.1.tar.gz", hash = "sha256:c0a0d790a3c0d2670a35c7caaf306857b9a5e0c01954b02cc095756def99d0d0"}, + {file = "madsci_common-0.5.3-py3-none-any.whl", hash = "sha256:69cbcdad127e2c63ef8f086885844a1b1a0231833282fafa71bddb452e5681f7"}, + {file = "madsci_common-0.5.3.tar.gz", hash = "sha256:ad0c04d0f7363404278c6ec26b78743b22e34e0c2fe01e5bfd0a12be60d76ce5"}, ] [[package]] name = "madsci-node-module" -version = "0.3.1" +version = "0.5.3" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes." groups = ["default"] dependencies = [ "madsci-client", "madsci-common", + "regex", ] files = [ - {file = "madsci_node_module-0.3.1-py3-none-any.whl", hash = "sha256:9995179bb568199ad815fc2c30f06bcef15f030e8277c5957eee4b8e6e43da38"}, - {file = "madsci_node_module-0.3.1.tar.gz", hash = "sha256:efa119b9b8c98fc0c75a5b9aca17eee72927ffbfa586ffaefdf9738fc443117f"}, + {file = "madsci_node_module-0.5.3-py3-none-any.whl", hash = "sha256:32616ae0cfaaaecbba2641b40f1e8a7a59de4acd9fd96b5ae4c015cf00191ed2"}, + {file = "madsci_node_module-0.5.3.tar.gz", hash = "sha256:287f7e50cc2fe8ba87784b946f34e881d2a6e7f3b4fb10623b313a5aacffaa10"}, ] [[package]] @@ -891,18 +922,19 @@ files = [ [[package]] name = "paramiko" -version = "3.5.1" -requires_python = ">=3.6" +version = "4.0.0" +requires_python = ">=3.9" summary = "SSH2 protocol library" groups = ["default"] dependencies = [ "bcrypt>=3.2", "cryptography>=3.3", + "invoke>=2.0", "pynacl>=1.5", ] files = [ - {file = "paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61"}, - {file = "paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822"}, + {file = "paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9"}, + {file = "paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f"}, ] [[package]] @@ -929,7 +961,7 @@ files = [ [[package]] name = "pre-commit" -version = "4.2.0" +version = "4.3.0" requires_python = ">=3.9" summary = "A framework for managing and maintaining multi-language pre-commit hooks." groups = ["dev"] @@ -941,8 +973,8 @@ dependencies = [ "virtualenv>=20.10.0", ] files = [ - {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, - {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, + {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, + {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, ] [[package]] @@ -1141,21 +1173,52 @@ files = [ {file = "pydantic_extra_types-2.10.5.tar.gz", hash = "sha256:1dcfa2c0cf741a422f088e0dbb4690e7bfadaaf050da3d6f80d6c3cf58a2bad8"}, ] +[[package]] +name = "pydantic-settings" +version = "2.11.0" +requires_python = ">=3.9" +summary = "Settings management using Pydantic" +groups = ["default"] +dependencies = [ + "pydantic>=2.7.0", + "python-dotenv>=0.21.0", + "typing-inspection>=0.4.0", +] +files = [ + {file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"}, + {file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"}, +] + +[[package]] +name = "pydantic-settings-export" +version = "1.0.3" +requires_python = ">=3.9" +summary = "Export your Pydantic settings to documentation with ease!" +groups = ["default"] +dependencies = [ + "pydantic-settings>=2.3", + "pydantic>=2.7", + "tomli>=2.2.1; python_full_version < \"3.11\"", + "typing-extensions>=4.12.2; python_full_version < \"3.11\"", +] +files = [ + {file = "pydantic_settings_export-1.0.3-py3-none-any.whl", hash = "sha256:86c804d837c26ca2d786080b27036818c816b0963e01ea8f473727aa98b84c07"}, + {file = "pydantic_settings_export-1.0.3.tar.gz", hash = "sha256:8b4f0b5daab0113fdfe9018b5684b7fb6be6157275604f2f22cd8cdd7d7b8f7b"}, +] + [[package]] name = "pyepics" -version = "3.5.7" -requires_python = ">=3.7" +version = "3.5.8" +requires_python = ">=3.9" summary = "Epics Channel Access for Python" groups = ["default"] dependencies = [ - "importlib-resources; python_version <= \"3.8\"", - "numpy", + "numpy>=1.23", "pyparsing", - "setuptools", ] files = [ - {file = "pyepics-3.5.7-py3-none-any.whl", hash = "sha256:c2ffbeae56f6dff5e5ee0a11fa18b7deb00c8ce59daabab0055a05ddfb8abf55"}, - {file = "pyepics-3.5.7.tar.gz", hash = "sha256:be3fd878171a66fba42bf4051404b12b36ded8a448ac1894f0f81c9f2b330d7f"}, + {file = "pyepics-3.5.8-py3-none-any.whl", hash = "sha256:02f322284f558feea16f8d4efee3d102e27c4f7c25cbfdafcc28eec944110a44"}, + {file = "pyepics-3.5.8.tar.gz", hash = "sha256:d44e6ac9404b5a827a5224cde374387b47f6f3f891c8437ddbd2f9fb913bba51"}, ] [[package]] @@ -1394,7 +1457,7 @@ files = [ [[package]] name = "pytest" -version = "8.4.0" +version = "8.4.2" requires_python = ">=3.9" summary = "pytest: simple powerful testing with Python" groups = ["dev"] @@ -1408,8 +1471,8 @@ dependencies = [ "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, - {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [[package]] @@ -1530,6 +1593,130 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "regex" +version = "2025.10.23" +requires_python = ">=3.9" +summary = "Alternative regular expression module, to replace re." +groups = ["default"] +files = [ + {file = "regex-2025.10.23-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:17bbcde374bef1c5fad9b131f0e28a6a24856dd90368d8c0201e2b5a69533daa"}, + {file = "regex-2025.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e10434279cc8567f99ca6e018e9025d14f2fded2a603380b6be2090f476426"}, + {file = "regex-2025.10.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c9bb421cbe7012c744a5a56cf4d6c80829c72edb1a2991677299c988d6339c8"}, + {file = "regex-2025.10.23-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275cd1c2ed8c4a78ebfa489618d7aee762e8b4732da73573c3e38236ec5f65de"}, + {file = "regex-2025.10.23-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b426ae7952f3dc1e73a86056d520bd4e5f021397484a6835902fc5648bcacce"}, + {file = "regex-2025.10.23-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5cdaf5b6d37c7da1967dbe729d819461aab6a98a072feef65bbcff0a6e60649"}, + {file = "regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bfeff0b08f296ab28b4332a7e03ca31c437ee78b541ebc874bbf540e5932f8d"}, + {file = "regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f97236a67307b775f30a74ef722b64b38b7ab7ba3bb4a2508518a5de545459c"}, + {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be19e7de499940cd72475fb8e46ab2ecb1cf5906bebdd18a89f9329afb1df82f"}, + {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:883df76ee42d9ecb82b37ff8d01caea5895b3f49630a64d21111078bbf8ef64c"}, + {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2e9117d1d35fc2addae6281019ecc70dc21c30014b0004f657558b91c6a8f1a7"}, + {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ff1307f531a5d8cf5c20ea517254551ff0a8dc722193aab66c656c5a900ea68"}, + {file = "regex-2025.10.23-cp310-cp310-win32.whl", hash = "sha256:7888475787cbfee4a7cd32998eeffe9a28129fa44ae0f691b96cb3939183ef41"}, + {file = "regex-2025.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ec41a905908496ce4906dab20fb103c814558db1d69afc12c2f384549c17936a"}, + {file = "regex-2025.10.23-cp310-cp310-win_arm64.whl", hash = "sha256:b2b7f19a764d5e966d5a62bf2c28a8b4093cc864c6734510bdb4aeb840aec5e6"}, + {file = "regex-2025.10.23-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c531155bf9179345e85032052a1e5fe1a696a6abf9cea54b97e8baefff970fd"}, + {file = "regex-2025.10.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:912e9df4e89d383681268d38ad8f5780d7cccd94ba0e9aa09ca7ab7ab4f8e7eb"}, + {file = "regex-2025.10.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f375c61bfc3138b13e762fe0ae76e3bdca92497816936534a0177201666f44f"}, + {file = "regex-2025.10.23-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e248cc9446081119128ed002a3801f8031e0c219b5d3c64d3cc627da29ac0a33"}, + {file = "regex-2025.10.23-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b52bf9282fdf401e4f4e721f0f61fc4b159b1307244517789702407dd74e38ca"}, + {file = "regex-2025.10.23-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c084889ab2c59765a0d5ac602fd1c3c244f9b3fcc9a65fdc7ba6b74c5287490"}, + {file = "regex-2025.10.23-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80e8eb79009bdb0936658c44ca06e2fbbca67792013e3818eea3f5f228971c2"}, + {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6f259118ba87b814a8ec475380aee5f5ae97a75852a3507cf31d055b01b5b40"}, + {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9b8c72a242683dcc72d37595c4f1278dfd7642b769e46700a8df11eab19dfd82"}, + {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d7b7a0a3df9952f9965342159e0c1f05384c0f056a47ce8b61034f8cecbe83"}, + {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:413bfea20a484c524858125e92b9ce6ffdd0a4b97d4ff96b5859aa119b0f1bdd"}, + {file = "regex-2025.10.23-cp311-cp311-win32.whl", hash = "sha256:f76deef1f1019a17dad98f408b8f7afc4bd007cbe835ae77b737e8c7f19ae575"}, + {file = "regex-2025.10.23-cp311-cp311-win_amd64.whl", hash = "sha256:59bba9f7125536f23fdab5deeea08da0c287a64c1d3acc1c7e99515809824de8"}, + {file = "regex-2025.10.23-cp311-cp311-win_arm64.whl", hash = "sha256:b103a752b6f1632ca420225718d6ed83f6a6ced3016dd0a4ab9a6825312de566"}, + {file = "regex-2025.10.23-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7a44d9c00f7a0a02d3b777429281376370f3d13d2c75ae74eb94e11ebcf4a7fc"}, + {file = "regex-2025.10.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b83601f84fde939ae3478bb32a3aef36f61b58c3208d825c7e8ce1a735f143f2"}, + {file = "regex-2025.10.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec13647907bb9d15fd192bbfe89ff06612e098a5709e7d6ecabbdd8f7908fc45"}, + {file = "regex-2025.10.23-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78d76dd2957d62501084e7012ddafc5fcd406dd982b7a9ca1ea76e8eaaf73e7e"}, + {file = "regex-2025.10.23-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8668e5f067e31a47699ebb354f43aeb9c0ef136f915bd864243098524482ac43"}, + {file = "regex-2025.10.23-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a32433fe3deb4b2d8eda88790d2808fed0dc097e84f5e683b4cd4f42edef6cca"}, + {file = "regex-2025.10.23-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d97d73818c642c938db14c0668167f8d39520ca9d983604575ade3fda193afcc"}, + {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bca7feecc72ee33579e9f6ddf8babbe473045717a0e7dbc347099530f96e8b9a"}, + {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7e24af51e907d7457cc4a72691ec458320b9ae67dc492f63209f01eecb09de32"}, + {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d10bcde58bbdf18146f3a69ec46dd03233b94a4a5632af97aa5378da3a47d288"}, + {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:44383bc0c933388516c2692c9a7503e1f4a67e982f20b9a29d2fb70c6494f147"}, + {file = "regex-2025.10.23-cp312-cp312-win32.whl", hash = "sha256:6040a86f95438a0114bba16e51dfe27f1bc004fd29fe725f54a586f6d522b079"}, + {file = "regex-2025.10.23-cp312-cp312-win_amd64.whl", hash = "sha256:436b4c4352fe0762e3bfa34a5567079baa2ef22aa9c37cf4d128979ccfcad842"}, + {file = "regex-2025.10.23-cp312-cp312-win_arm64.whl", hash = "sha256:f4b1b1991617055b46aff6f6db24888c1f05f4db9801349d23f09ed0714a9335"}, + {file = "regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193"}, + {file = "regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b"}, + {file = "regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f"}, + {file = "regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a"}, + {file = "regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b"}, + {file = "regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb"}, + {file = "regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368"}, + {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351"}, + {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673"}, + {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313"}, + {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87"}, + {file = "regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1"}, + {file = "regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0"}, + {file = "regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f"}, + {file = "regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044"}, + {file = "regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc"}, + {file = "regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656"}, + {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58"}, + {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e"}, + {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a"}, + {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e"}, + {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8"}, + {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6"}, + {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8"}, + {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494"}, + {file = "regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9"}, + {file = "regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6"}, + {file = "regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473"}, + {file = "regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6"}, + {file = "regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5"}, + {file = "regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10"}, + {file = "regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34"}, + {file = "regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a"}, + {file = "regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c"}, + {file = "regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224"}, + {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4"}, + {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462"}, + {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627"}, + {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec"}, + {file = "regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8"}, + {file = "regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796"}, + {file = "regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9"}, + {file = "regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422"}, + {file = "regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788"}, + {file = "regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10"}, + {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7"}, + {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f"}, + {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61"}, + {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c"}, + {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432"}, + {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e"}, + {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43"}, + {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3"}, + {file = "regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521"}, + {file = "regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741"}, + {file = "regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed"}, + {file = "regex-2025.10.23-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8d286760ee5b77fd21cf6b33cc45e0bffd1deeda59ca65b9be996f590a9828a"}, + {file = "regex-2025.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e72e3b84b170fec02193d32620a0a7060a22e52c46e45957dcd14742e0d28fb"}, + {file = "regex-2025.10.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec506e8114fa12d21616deb44800f536d6bf2e1a69253dbf611f69af92395c99"}, + {file = "regex-2025.10.23-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7e481f9710e8e24228ce2c77b41db7662a3f68853395da86a292b49dadca2aa"}, + {file = "regex-2025.10.23-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4663ff2fc367735ae7b90b4f0e05b25554446df4addafc76fdaacaaa0ba852b5"}, + {file = "regex-2025.10.23-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0879dd3251a42d2e9b938e1e03b1e9f60de90b4d153015193f5077a376a18439"}, + {file = "regex-2025.10.23-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651c58aecbab7e97bdf8ec76298a28d2bf2b6238c099ec6bf32e6d41e2f9a9cb"}, + {file = "regex-2025.10.23-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ceabc62a0e879169cd1bf066063bd6991c3e41e437628936a2ce66e0e2071c32"}, + {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bfdf4e9aa3e7b7d02fda97509b4ceeed34542361694ecc0a81db1688373ecfbd"}, + {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:92f565ff9beb9f51bc7cc8c578a7e92eb5c4576b69043a4c58cd05d73fda83c5"}, + {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:abbea548b1076eaf8635caf1071c9d86efdf0fa74abe71fca26c05a2d64cda80"}, + {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33535dcf34f47821381e341f7b715cbd027deda4223af4d3932adcd371d3192a"}, + {file = "regex-2025.10.23-cp39-cp39-win32.whl", hash = "sha256:345c9df49a15bf6460534b104b336581bc5f35c286cac526416e7a63d389b09b"}, + {file = "regex-2025.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:f668fe1fd3358c5423355a289a4a003e58005ce829d217b828f80bd605a90145"}, + {file = "regex-2025.10.23-cp39-cp39-win_arm64.whl", hash = "sha256:07a3fd25d9074923e4d7258b551ae35ab6bdfe01904b8f0d5341c7d8b20eb18d"}, + {file = "regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26"}, +] + [[package]] name = "requests" version = "2.32.3" @@ -1565,29 +1752,30 @@ files = [ [[package]] name = "ruff" -version = "0.11.13" +version = "0.14.4" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev"] files = [ - {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, - {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, - {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, - {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, - {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, - {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, - {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, + {file = "ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518"}, + {file = "ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4"}, + {file = "ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349"}, + {file = "ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff"}, + {file = "ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c"}, + {file = "ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb"}, + {file = "ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3"}, ] [[package]] @@ -1614,17 +1802,6 @@ files = [ {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, ] -[[package]] -name = "setuptools" -version = "80.9.0" -requires_python = ">=3.9" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -groups = ["default"] -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - [[package]] name = "six" version = "1.17.0" @@ -1765,7 +1942,7 @@ name = "tomli" version = "2.2.1" requires_python = ">=3.8" summary = "A lil' TOML parser" -groups = ["dev"] +groups = ["default", "dev"] marker = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, diff --git a/pyproject.toml b/pyproject.toml index b78c902..b0c2075 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,17 +8,16 @@ authors = [ {name = "Tobias Ginsburg", email = "tginsburg@anl.gov"}, ] dependencies = [ - "fastapi>=0.103.2", - "uvicorn>=0.21.1", "paramiko", "scp", "sockets", - "pynput", + "pynput>=1.8.1", "pyepics", "numpy>=2.0.0", "urx@git+https://github.com/Dozgulbas/python-urx.git", - "madsci.node_module>=0.2.1", -] + "madsci-node-module~=0.5.0", + "madsci-client~=0.5.0", + "madsci-common~=0.5.0",] requires-python = ">=3.9.21" readme = "README.md" diff --git a/src/ur_interface/ur.py b/src/ur_interface/ur.py index feccebc..ba27b54 100644 --- a/src/ur_interface/ur.py +++ b/src/ur_interface/ur.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 """Interface for UR Driver""" +import logging import socket +import traceback from math import radians from time import sleep -from typing import Union +from typing import Optional, Union import math3d as m3 import numpy as np @@ -14,6 +16,7 @@ from urx import Robot from ur_interface.ur_dashboard import UR_DASHBOARD +from ur_interface.ur_error_types import GripperError, URConnectionError, URMovementError from ur_interface.ur_tools.gripper_controller import FingerGripperController from ur_interface.ur_tools.ot_pipette_controller import OTPipetteController from ur_interface.ur_tools.screwdriver_controller import ScrewdriverController @@ -24,36 +27,44 @@ class Connection: """Connection to the UR robot to be shared within UR driver""" - def __init__(self, hostname: str = "146.137.240.38") -> None: + def __init__(self, hostname: str = "146.137.240.38", logger: logging.Logger = None) -> None: """Connection class that creates a connection with the robot using URx Library""" self.hostname = hostname - + self.logger = logger self.connection = None self.connect_ur() def connect_ur(self): - """ - Description: Create conenction to the UR robot - """ - - for i in range(10): + """Create connection to the UR robot""" + for attempt in range(10): try: + self.logger.info(f"Attempting robot connection (attempt {attempt + 1}/10)...") self.connection = Robot(self.hostname) - except socket.error: - print("Trying robot connection {}...".format(i)) + except socket.error as e: + self.logger.warning(f"Robot connection attempt {attempt + 1} failed: {e}") + sleep(1) + + except Exception as e: + self.logger.error(f"Unexpected error during robot connection: {e}\n{traceback.format_exc()}") sleep(1) else: - print("Successful ur connection") - break + self.logger.info("Successful UR connection") + return + + raise URConnectionError(f"Failed to connect to UR robot at {self.hostname} after 10 attempts") def disconnect_ur(self): """ Description: Disconnects the socket connection with the UR robot """ - self.connection.close() - print("Robot connection is closed.") + try: + if self.connection: + self.connection.close() + self.logger.info("Robot connection closed successfully") + except Exception as e: + self.logger.error(f"Error closing robot connection: {e}\n{traceback.format_exc()}") class UR: @@ -71,9 +82,11 @@ def __init__( tool_resource_id: str = None, tcp_pose: list = [0, 0, 0, 0, 0, 0], base_reference_frame: list = None, + logger: Optional[logging.Logger] = None, ): """Constructor for the UR class. :param hostname: Hostname or ip. + :param logger: Logger object for logging messages """ if not hostname: @@ -83,28 +96,51 @@ def __init__( self.resource_client = resource_client self.tool_resource_id = tool_resource_id self.resource_owner = resource_owner + self.logger = logger or self._setup_logger() self.acceleration = 0.5 self.velocity = 0.5 self.robot_current_joint_angles = None + self.gripper_speed: int = None + self.gripper_force: int = None - self.ur_dashboard = UR_DASHBOARD(hostname=self.hostname) - self.ur = Connection(hostname=self.hostname) - self.ur_connection = self.ur.connection - - self.gripper_speed = 255 - self.gripper_force = 255 - - self.ur_connection.set_tcp(tcp_pose) - if base_reference_frame: - self._set_base_reference_frame(base_reference_frame) - self.get_movement_state() + try: + self.ur_dashboard = UR_DASHBOARD(hostname=self.hostname) + self.ur = Connection(hostname=self.hostname, logger=self.logger) + self.ur_connection = self.ur.connection + + self.gripper_speed = 255 + self.gripper_force = 255 + + self.ur_connection.set_tcp(tcp_pose) + if base_reference_frame: + self._set_base_reference_frame(base_reference_frame) + self.get_movement_state() + + except Exception as e: + self.logger.error(f"Failed to initialize UR: {e}\n{traceback.format_exc()}") + raise + + def _setup_logger(self) -> logging.Logger: + """Setup default logger if none provided""" + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger def disconnect(self): - "Disconnects the robot from URX and UR Dahsboard connections" - self.ur.disconnect_ur() - self.ur_dashboard.clear_operational_mode() - self.ur_dashboard.disconnect() + """Disconnects the robot from URX and UR Dashboard connections""" + try: + self.ur.disconnect_ur() + self.ur_dashboard.clear_operational_mode() + self.ur_dashboard.disconnect() + self.logger.info("UR disconnected successfully") + except Exception as e: + self.logger.error(f"Error during UR disconnect: {e}\n{traceback.format_exc()}") def _set_base_reference_frame(self, base_reference_frame: list) -> None: """Sets the base reference frame for the robot. @@ -114,72 +150,84 @@ def _set_base_reference_frame(self, base_reference_frame: list) -> None: if not isinstance(base_reference_frame, list) or len(base_reference_frame) != 6: raise ValueError("Base reference frame must be a list of 6 values") - # Extract position and rotation components - x, y, z, rx_deg, ry_deg, rz_deg = base_reference_frame + try: + # Extract position and rotation components + x, y, z, rx_deg, ry_deg, rz_deg = base_reference_frame - # Create translation vector (only if any translation values are non-zero) - if any([x, y, z]): - translation = m3.Vector(x, y, z) - else: - translation = m3.Vector(0, 0, 0) + # Create translation vector (only if any translation values are non-zero) + if any([x, y, z]): + translation = m3.Vector(x, y, z) + else: + translation = m3.Vector(0, 0, 0) - # Start with identity rotation - rotation = m3.Orientation() # Identity rotation + # Start with identity rotation + rotation = m3.Orientation() # Identity rotation - # Apply only non-zero rotations in order - if rx_deg != 0: - rx_rad = radians(rx_deg) - rotation = rotation * m3.Orientation.new_rot_x(rx_rad) + # Apply only non-zero rotations in order + if rx_deg != 0: + rx_rad = radians(rx_deg) + rotation = rotation * m3.Orientation.new_rot_x(rx_rad) - if ry_deg != 0: - ry_rad = radians(ry_deg) - rotation = rotation * m3.Orientation.new_rot_y(ry_rad) + if ry_deg != 0: + ry_rad = radians(ry_deg) + rotation = rotation * m3.Orientation.new_rot_y(ry_rad) - if rz_deg != 0: - rz_rad = radians(rz_deg) - rotation = rotation * m3.Orientation.new_rot_z(rz_rad) - # Create the transform - transform = m3.Transform(rotation, translation) + if rz_deg != 0: + rz_rad = radians(rz_deg) + rotation = rotation * m3.Orientation.new_rot_z(rz_rad) + # Create the transform + transform = m3.Transform(rotation, translation) - # Set the coordinate system - self.ur_connection.set_csys(transform) + # Set the coordinate system + self.ur_connection.set_csys(transform) - print(f"Base reference frame set to: {base_reference_frame}") + self.logger.info(f"Base reference frame set to: {base_reference_frame}") + except Exception as e: + self.logger.error(f"Error setting base reference frame: {e}\n{traceback.format_exc()}") + raise def get_movement_state(self) -> str: """Gets robot movement status by checking robot joint values. Return (str) READY if robot is not moving BUSY if robot is moving """ - current_location = self.ur_connection.getj() - if self.robot_current_joint_angles is None: - movement_state = "READY" - else: - if np.linalg.norm(np.array(current_location) - np.array(self.robot_current_joint_angles)) < 1e-3: + try: + current_location = self.ur_connection.getj() + if self.robot_current_joint_angles is None: movement_state = "READY" else: - movement_state = "BUSY" + if np.linalg.norm(np.array(current_location) - np.array(self.robot_current_joint_angles)) < 1e-3: + movement_state = "READY" + else: + movement_state = "BUSY" - self.robot_current_joint_angles = current_location + self.robot_current_joint_angles = current_location - return movement_state, current_location + return movement_state, current_location + + except Exception as e: + self.logger.error(f"Error getting movement state: {e}\n{traceback.format_exc()}") + raise URMovementError("Failed to get robot movement state") # noqa def home(self, home_location: Union[LocationArgument, list], linear_motion: bool = False) -> None: """Moves the robot to the home location. Args: home_location: 6 joint value location """ - - print("Homing the robot...") - if isinstance(home_location, LocationArgument): - home_loc = home_location.location - else: - home_loc = home_location - if linear_motion: - self.ur_connection.movel(home_loc, self.velocity, self.acceleration) - else: - self.ur_connection.movej(home_loc, self.velocity, self.acceleration) - print("Robot homed") + try: + self.logger.info("Homing the robot...") + if isinstance(home_location, LocationArgument): + home_loc = home_location.location + else: + home_loc = home_location + if linear_motion: + self.ur_connection.movel(home_loc, self.velocity, self.acceleration) + else: + self.ur_connection.movej(home_loc, self.velocity, self.acceleration) + self.logger.info("Robot homed") + except Exception as e: + self.logger.error(f"Error in homing the robot: {e}\n{traceback.format_exc()}") + raise URMovementError("Failed to home the robot") # noqa def pick_tool( self, @@ -221,7 +269,7 @@ def pick_tool( self.home(home) except Exception as err: - print("Error in picking tool: ", err) + self.logger.error(f"Error in picking tool: {err}\n{traceback.format_exc()}") def place_tool( self, @@ -259,7 +307,7 @@ def place_tool( self.home(home) except Exception as err: - print("Error in placing tool: ", err) + self.logger.error(f"Error in placing tool: {err}\n{traceback.format_exc()}") def set_digital_io(self, channel: int = None, value: bool = None) -> None: """Sets digital I/O outputs to open an close the channel. This helps controlling the external tools @@ -269,7 +317,7 @@ def set_digital_io(self, channel: int = None, value: bool = None) -> None: value (bool): False for close, True for open """ if channel is None or value is None: - print("Channel or value is not specified") + self.logger.error("Channel or value is not specified") return self.ur_connection.set_digital_out(channel, value) @@ -301,17 +349,22 @@ def gripper_transfer( """ if not source or not target: - raise Exception("Please provide both the source and target locations to make a transfer") - - self.home(home) + raise ValueError("Please provide both the source and target locations to make a transfer") + gripper_controller = None try: + self.logger.info(f"Starting gripper transfer from {source} to {target}") + self.home(home) + + self.logger.info("Initializing gripper controller") gripper_controller = FingerGripperController( hostname=self.hostname, ur=self.ur_connection, resource_client=self.resource_client, gripper_resource_id=self.tool_resource_id, + logger=self.logger, ) + self.logger.info("Connecting to gripper...") gripper_controller.connect_gripper() gripper_controller.velocity = self.velocity gripper_controller.acceleration = self.acceleration @@ -323,6 +376,7 @@ def gripper_transfer( if gripper_close: gripper_controller.gripper_close = gripper_close + self.logger.info("Executing gripper transfer...") gripper_controller.transfer( home=home, source=source, @@ -332,15 +386,27 @@ def gripper_transfer( source_approach_distance=source_approach_distance, target_approach_distance=target_approach_distance, ) - print("Finished transfer") - gripper_controller.disconnect_gripper() + self.logger.info("Gripper transfer completed successfully") + except socket.timeout as e: + self.logger.error(f"Socket timeout during gripper transfer: {e}\n{traceback.format_exc()}") + raise GripperError(f"Gripper communication timed out: {e}") # noqa - except Exception as err: - print(err) + except Exception as e: + self.logger.error(f"Error in gripper transfer action: {e}\n{traceback.format_exc()}") + raise GripperError(f"Gripper transfer failed: {e}") # noqa finally: - gripper_controller.disconnect_gripper() - self.home(home) + if gripper_controller: + try: + self.logger.info("Disconnecting gripper...") + gripper_controller.disconnect_gripper() + except Exception as e: + self.logger.error(f"Error disconnecting gripper: {e}\n{traceback.format_exc()}") + + try: + self.home(home) + except Exception as e: + self.logger.error(f"Error returning to home after gripper transfer: {e}\n{traceback.format_exc()}") def gripper_pick( self, @@ -360,20 +426,25 @@ def gripper_pick( gripper_close (int): Gripper min close value (0-255) """ - if not source: - raise Exception("Please provide the source location to make a pick") + raise ValueError("Please provide the source location to make a pick") - self.home(home) + gripper_controller = None try: + self.logger.info(f"Starting gripper pick from {source}") + self.home(home) + + self.logger.info("Initializing gripper controller...") gripper_controller = FingerGripperController( hostname=self.hostname, ur=self.ur_connection, resource_client=self.resource_client, gripper_resource_id=self.tool_resource_id, + logger=self.logger, ) + self.logger.info("Connecting to gripper...") gripper_controller.connect_gripper() gripper_controller.velocity = self.velocity gripper_controller.acceleration = self.acceleration @@ -383,20 +454,34 @@ def gripper_pick( if gripper_close: gripper_controller.gripper_close = gripper_close + self.logger.info("Executing gripper pick...") gripper_controller.pick( source=source, approach_axis=source_approach_axis, approach_distance=source_approach_distance, ) - print("Finished gripper pick") - gripper_controller.disconnect_gripper() + self.logger.info("Gripper pick completed successfully") - except Exception as err: - print(err) + except socket.timeout as e: + self.logger.error(f"Socket timeout during gripper pick: {e}\n{traceback.format_exc()}") + raise GripperError(f"Gripper communication timed out during pick: {e}") # noqa + + except Exception as e: + self.logger.error(f"Error in gripper pick action: {e}\n{traceback.format_exc()}") + raise GripperError(f"Gripper pick failed: {e}") # noqa finally: - gripper_controller.disconnect_gripper() - self.home(home) + if gripper_controller: + try: + self.logger.info("Disconnecting gripper...") + gripper_controller.disconnect_gripper() + except Exception as e: + self.logger.error(f"Error disconnecting gripper: {e}\n{traceback.format_exc()}") + + try: + self.home(home) + except Exception as e: + self.logger.error(f"Error returning to home after gripper pick: {e}\n{traceback.format_exc()}") def gripper_place( self, @@ -418,17 +503,23 @@ def gripper_place( """ if not target: - raise Exception("Please provide the target location to make a place") + raise ValueError("Please provide the target location to make a place") - self.home(home) + gripper_controller = None try: + self.logger.info(f"Starting gripper place to {target}") + self.home(home) + gripper_controller = FingerGripperController( hostname=self.hostname, ur=self.ur_connection, resource_client=self.resource_client, gripper_resource_id=self.tool_resource_id, + logger=self.logger, ) + + self.logger.info("Connecting to gripper...") gripper_controller.connect_gripper() gripper_controller.velocity = self.velocity gripper_controller.acceleration = self.acceleration @@ -438,20 +529,34 @@ def gripper_place( if gripper_open: gripper_controller.gripper_open = gripper_open + self.logger.info("Executing gripper place...") gripper_controller.place( target=target, approach_axis=target_approach_axis, approach_distance=target_approach_distance, ) - print("Finished gripper place") - gripper_controller.disconnect_gripper() + self.logger.info("Gripper place completed successfully") - except Exception as err: - print(err) + except socket.timeout as e: + self.logger.error(f"Socket timeout during gripper place: {e}\n{traceback.format_exc()}") + raise GripperError(f"Gripper communication timed out during place: {e}") # noqa + + except Exception as e: + self.logger.error(f"Error in gripper place action: {e}\n{traceback.format_exc()}") + raise GripperError(f"Gripper place failed: {e}") # noqa finally: - gripper_controller.disconnect_gripper() - self.home(home) + if gripper_controller: + try: + self.logger.info("Disconnecting gripper...") + gripper_controller.disconnect_gripper() + except Exception as e: + self.logger.error(f"Error disconnecting gripper: {e}\n{traceback.format_exc()}") + + try: + self.home(home) + except Exception as e: + self.logger.error(f"Error returning to home after gripper place: {e}\n{traceback.format_exc()}") def gripper_screw_transfer( self, @@ -498,7 +603,9 @@ def gripper_screw_transfer( ) except Exception as err: - print(err) + self.logger.error( + f"Error during gripper screw transfer: {err}\n{traceback.format_exc()}" + ) # Added exc_info=True for detailed logging finally: gripper_controller.disconnect_gripper() @@ -541,7 +648,7 @@ def remove_cap( gripper_controller.disconnect_gripper() except Exception as err: - print(err) + self.logger.error(f"{err}\n{traceback.format_exc()}") def place_cap( self, @@ -574,7 +681,7 @@ def place_cap( gripper_controller.disconnect_gripper() except Exception as err: - print(err) + self.logger.error(f"{err}\n{traceback.format_exc()}") def pick_and_flip_object( self, @@ -617,7 +724,7 @@ def pick_and_flip_object( gripper_controller.disconnect_gripper() self.home(home) except Exception as er: - print(er) + self.logger.error(er) finally: gripper_controller.disconnect_gripper() @@ -662,7 +769,7 @@ def robotiq_screwdriver_transfer( ) sr.screwdriver.disconnect() except Exception as err: - print(err) + self.logger.error(err) self.home(home) @@ -712,9 +819,9 @@ def pipette_transfer( if tip_trash: pipette.eject_tip(eject_tip_loc=tip_trash, approach_axis="y") pipette.disconnect_pipette() - print("Disconnecting from the pipette") + self.logger.info("Disconnecting from the pipette") except Exception as err: - print(err) + self.logger.error(err) def pipette_pick_and_move_sample( self, @@ -761,9 +868,9 @@ def pipette_pick_and_move_sample( volume=volume, ) pipette.disconnect_pipette() - print("Disconnecting from the pipette") + self.logger.info("Disconnecting from the pipette") except Exception as err: - print(err) + self.logger.error(err) def pipette_dispense_and_retrieve( self, @@ -803,9 +910,9 @@ def pipette_dispense_and_retrieve( pipette.eject_tip(eject_tip_loc=tip_trash, approach_axis="y") pipette.disconnect_pipette() self.home(home, linear_motion=linear_motion) - print("Disconnecting from the pipette") + self.logger.info("Disconnecting from the pipette") except Exception as err: - print(err) + self.logger.error(err) def run_droplet( self, @@ -859,7 +966,7 @@ def run_urp_program(self, transfer_file_path: str = None, program_name: str = No self.ur_dashboard.run_program() sleep(5) - print("Running the URP program: ", program_name) + self.logger.info(f"Running the URP program: {program_name}") time_elapsed = 0 program_status = "BUSY" diff --git a/src/ur_interface/ur_error_types.py b/src/ur_interface/ur_error_types.py new file mode 100644 index 0000000..1b3e1ef --- /dev/null +++ b/src/ur_interface/ur_error_types.py @@ -0,0 +1,31 @@ +"""Custom exception types for UR interface errors""" + + +class URConnectionError(Exception): + """Custom exception for UR connection errors""" + + pass + + +class URMovementError(Exception): + """Custom exception for UR movement errors""" + + pass + + +class GripperError(Exception): + """Custom exception for gripper-related errors""" + + pass + + +class GripperConnectionError(Exception): + """Custom exception for gripper connection errors""" + + pass + + +class GripperOperationError(Exception): + """Custom exception for gripper operation errors""" + + pass diff --git a/src/ur_interface/ur_tools/gripper_controller.py b/src/ur_interface/ur_tools/gripper_controller.py index 7fb3fcf..4616cf7 100644 --- a/src/ur_interface/ur_tools/gripper_controller.py +++ b/src/ur_interface/ur_tools/gripper_controller.py @@ -1,5 +1,8 @@ """Controls Various Type of Gripper End Effectors""" +import logging +import socket +import traceback from copy import deepcopy from math import radians from time import sleep @@ -7,6 +10,8 @@ from madsci.common.types.location_types import LocationArgument +from ur_interface.ur_error_types import GripperConnectionError, GripperOperationError + from .robotiq_gripper_driver import RobotiqGripper @@ -20,6 +25,7 @@ def __init__( ur=None, resource_client=None, gripper_resource_id: str = None, + logger: logging.Logger = None, ): """ Constructor for the FingerGripperController class. @@ -32,6 +38,7 @@ def __init__( self.PORT = port self.resource_client = resource_client self.gripper_resource_id = gripper_resource_id + self.logger = logger if not ur: raise Exception("UR connection is not established") @@ -53,50 +60,83 @@ def __init__( self.blend_radius_m = 0.001 self.ref_frame = [0, 0, 0, 0, 0, 0] - def connect_gripper(self): + def __del__(self): + """Destructor for the FingerGripperController class.""" + try: + self.disconnect_gripper() + except Exception as e: + self.logger.error(f"Error during gripper disconnection in destructor: {e}\n{traceback.format_exc()}") + raise e + + def connect_gripper(self, max_retries: int = 2): """ Connect to the gripper """ - for i in range(2): + for attempt in range(max_retries): try: # GRIPPER SETUP: + self.logger.info(f"Connecting to gripper (attempt {attempt + 1}/{max_retries})...") self.gripper = RobotiqGripper() - print("Connecting to gripper...") - self.gripper.connect(hostname=self.host, port=self.PORT) + + self.logger.debug(f"Attempting socket connection to {self.host}:{self.PORT}") + self.gripper.connect(hostname=self.host, port=self.PORT, socket_timeout=5) if self.gripper.is_active(): - print("Gripper already active") + self.logger.info("Gripper already active") else: - print("Activating gripper...") + self.logger.info("Activating gripper...") self.gripper.activate() - print("Opening gripper...") + self.logger.info("Opening gripper...") self.open_gripper() - except Exception as err: - print("Gripper connection failed, try {}: {} ".format(i + 1, err)) - self.ur.set_tool_communication( - baud_rate=115200, - parity=0, - stop_bits=1, - rx_idle_chars=1.5, - tx_idle_chars=3.5, + self.logger.info("Gripper is ready") + return + + except socket.timeout as e: + self.logger.error(f"Socket timeout on attempt {attempt + 1}: {e}") + if attempt < max_retries - 1: + self.logger.info("Attempting to reset tool communication...") + try: + self.ur.set_tool_communication( + baud_rate=115200, + parity=0, + stop_bits=1, + rx_idle_chars=1.5, + tx_idle_chars=3.5, + ) + sleep(4) + except Exception as reset_error: + self.logger.error( + f"Error resetting tool communication: {reset_error}\n{traceback.format_exc()}" + ) + + except socket.error as e: + self.logger.error(f"Socket error on attempt {attempt + 1}: {e}\n{traceback.format_exc()}") + if attempt < max_retries - 1: + self.logger.info("Retrying connection...") + sleep(2) + + except Exception as e: + self.logger.error( + f"Unexpected error connecting to gripper on attempt {attempt + 1}: {e}\n{traceback.format_exc()}" ) - sleep(4) + if attempt < max_retries - 1: + sleep(2) - else: - print("Gripper is ready!") + raise GripperConnectionError(f"Failed to connect to gripper after {max_retries} attempts") # noqa def disconnect_gripper(self): """ Discconect from the gripper """ try: - self.gripper.disconnect() - except Exception as err: - print("Gripper error: ", err) - - else: - print("Gripper connection is closed") + if self.gripper: + self.logger.info("Disconnecting gripper...") + self.gripper.disconnect() + self.logger.info("Gripper connection closed") + except Exception as e: + self.logger.error(f"Error disconnecting gripper: {e}\n{traceback.format_exc()}") + raise def home_robot(self, home: Union[LocationArgument, list] = None) -> None: """ @@ -104,11 +144,20 @@ def home_robot(self, home: Union[LocationArgument, list] = None) -> None: """ if not home: return - if isinstance(home, LocationArgument): - home_location = home.location - elif isinstance(home, list): - home_location = home - self.ur.movej(home_location, self.acceleration, self.velocity) + try: + self.logger.info("Homing robot to specified position...") + if isinstance(home, LocationArgument): + home_location = home.representation + elif isinstance(home, list): + home_location = home + else: + raise Exception("Please provide an accurate home location") + + self.logger.debug(f"Homing robot to: {home_location}") + self.ur.movej(home_location, self.acceleration, self.velocity) + except Exception as e: + self.logger.error(f"Error homing robot: {e}\n{traceback.format_exc()}") + raise def open_gripper( self, @@ -117,19 +166,31 @@ def open_gripper( force: float = None, ) -> None: """Opens the gripper using pose, speed and force variables""" - if pose: - self.gripper_open = pose - if force: - self.gripper_force = force - if speed: - self.gripper_speed = speed - - self.gripper.move_and_wait_for_pos( - self.gripper_open, - self.gripper_speed, - self.gripper_force, - ) - sleep(0.5) + try: + if pose: + self.gripper_open = pose + if force: + self.gripper_force = force + if speed: + self.gripper_speed = speed + + self.logger.info(f"Opening gripper to position: {self.gripper_open}") + + self.gripper.move_and_wait_for_pos( + self.gripper_open, + self.gripper_speed, + self.gripper_force, + ) + sleep(0.5) + self.logger.debug("Gripper opened successfully") + + except socket.timeout as e: + self.logger.error(f"Timeout while opening gripper: {e}") + raise GripperOperationError(f"Gripper open operation timed out: {e}") # noqa + + except Exception as e: + self.logger.error(f"Error opening gripper: {e}\n{traceback.format_exc()}") + raise GripperOperationError(f"Failed to open gripper: {e}") # noqa def close_gripper( self, @@ -138,19 +199,30 @@ def close_gripper( force: float = None, ) -> None: """Closes the gripper using pose, speed and force variables""" - if pose: - self.gripper_close = pose - if force: - self.gripper_force = force - if speed: - self.gripper_speed = speed - - self.gripper.move_and_wait_for_pos( - self.gripper_close, - self.gripper_speed, - self.gripper_force, - ) - sleep(0.5) + try: + if pose: + self.gripper_close = pose + if force: + self.gripper_force = force + if speed: + self.gripper_speed = speed + self.logger.info(f"Closing gripper to position: {self.gripper_close}") + + self.gripper.move_and_wait_for_pos( + self.gripper_close, + self.gripper_speed, + self.gripper_force, + ) + sleep(0.5) + self.logger.debug("Gripper closed successfully") + + except socket.timeout as e: + self.logger.error(f"Timeout while closing gripper: {e}") + raise GripperOperationError(f"Gripper close operation timed out: {e}") # noqa + + except Exception as e: + self.logger.error(f"Error closing gripper: {e}\n{traceback.format_exc()}") + raise GripperOperationError(f"Failed to close gripper: {e}") # noqa def pick( self, @@ -159,52 +231,63 @@ def pick( approach_distance: float = None, ): """Pick up from first goal position""" + try: + if isinstance(source, LocationArgument): + source_location = source.representation + elif isinstance(source, list): + source_location = source + else: + raise Exception("Please provide an accurate source location") - if isinstance(source, LocationArgument): - source_location = source.location - elif isinstance(source, list): - source_location = source - else: - raise Exception("Please provide an accurate source loaction") - - if not approach_distance: - approach_distance = 0.05 - - axis = None - - if not approach_axis or approach_axis.lower() == "z": - axis = 2 - elif approach_axis.lower() == "y": - axis = 1 - elif approach_axis.lower() == "-y": - axis = 1 - approach_distance = -approach_distance - elif approach_axis.lower() == "x": - axis = 0 - elif approach_axis.lower() == "-x": - axis = 0 - approach_distance = -approach_distance + if not approach_distance: + approach_distance = 0.05 - above_goal = deepcopy(source_location) - above_goal[axis] += approach_distance + axis = None - self.open_gripper() + if not approach_axis or approach_axis.lower() == "z": + axis = 2 + elif approach_axis.lower() == "y": + axis = 1 + elif approach_axis.lower() == "-y": + axis = 1 + approach_distance = -approach_distance + elif approach_axis.lower() == "x": + axis = 0 + elif approach_axis.lower() == "-x": + axis = 0 + approach_distance = -approach_distance - print("Moving to above goal position") - self.ur.movel(above_goal, self.acceleration, self.velocity) + above_goal = deepcopy(source_location) + above_goal[axis] += approach_distance - print("Moving to goal position") - self.ur.movel(source_location, self.acceleration, self.velocity) + self.logger.info(f"Starting pick operation from source: {source_location}") - print("Closing gripper") - self.close_gripper() + self.open_gripper() - if self.resource_client and isinstance(source, LocationArgument): # Handle resources if configured - popped_object, updated_resource = self.resource_client.pop(resource=source.resource_id) - self.resource_client.push(resource=self.gripper_resource_id, child=popped_object) + self.logger.debug("Moving to above goal position") + self.ur.movel(above_goal, self.acceleration, self.velocity) - print("Moving back to above goal position") - self.ur.movel(above_goal, self.acceleration, self.velocity) + self.logger.debug("Moving to goal position") + self.ur.movel(source_location, self.acceleration, self.velocity) + + self.close_gripper() + + if self.resource_client and isinstance(source, LocationArgument): # Handle resources if configured + try: + popped_object, updated_resource = self.resource_client.pop(resource=source.resource_id) + self.resource_client.push(resource=self.gripper_resource_id, child=popped_object) + except Exception as e: + self.logger.error(f"Resource client error during pick: {e}\n{traceback.format_exc()}") + + self.logger.debug("Moving back to above goal position") + self.ur.movel(above_goal, self.acceleration, self.velocity) + self.logger.info("Pick operation completed successfully") + + except GripperOperationError: + raise + except Exception as e: + self.logger.error(f"Error during pick operation: {e}\n{traceback.format_exc()}") + raise GripperOperationError(f"Pick operation failed: {e}") # noqa def pick_screw( self, @@ -213,7 +296,7 @@ def pick_screw( """Handles the pick screw request""" if isinstance(screw_loc, LocationArgument): - source_location = screw_loc.location + source_location = screw_loc.representation elif isinstance(screw_loc, list): source_location = screw_loc @@ -230,49 +313,60 @@ def place( approach_distance: float = None, ): """Place down at second goal position""" - - if isinstance(target, LocationArgument): - target_location = target.location - elif isinstance(target, list): - target_location = target - else: - raise Exception("Please provide an accurate target loaction") - - if not approach_distance: - approach_distance = 0.05 - - axis = None - - if not approach_axis or approach_axis.lower() == "z": - axis = 2 - elif approach_axis.lower() == "y": - axis = 1 - elif approach_axis.lower() == "-y": - axis = 1 - approach_distance = -approach_distance - elif approach_axis.lower() == "x": - axis = 0 - elif approach_axis.lower() == "-x": - axis = 0 - approach_distance = -approach_distance - - above_goal = deepcopy(target_location) - above_goal[axis] += approach_distance - - print("Moving to above goal position") - self.ur.movel(above_goal, self.acceleration, self.velocity) - - print("Moving to goal position") - self.ur.movel(target_location, self.acceleration, self.velocity) - - print("Opennig gripper") - self.open_gripper() - - if self.resource_client and isinstance(target, LocationArgument): # Handle resources if configured - popped_object, updated_resource = self.resource_client.pop(resource=self.gripper_resource_id) - self.resource_client.push(resource=target.resource_id, child=popped_object) - print("Moving back to above goal position") - self.ur.movel(above_goal, self.acceleration, self.velocity) + try: + if isinstance(target, LocationArgument): + target_location = target.representation + elif isinstance(target, list): + target_location = target + else: + raise ValueError("Please provide an accurate target location") + + if not approach_distance: + approach_distance = 0.05 + + axis = None + + if not approach_axis or approach_axis.lower() == "z": + axis = 2 + elif approach_axis.lower() == "y": + axis = 1 + elif approach_axis.lower() == "-y": + axis = 1 + approach_distance = -approach_distance + elif approach_axis.lower() == "x": + axis = 0 + elif approach_axis.lower() == "-x": + axis = 0 + approach_distance = -approach_distance + + above_goal = deepcopy(target_location) + above_goal[axis] += approach_distance + + self.logger.info(f"Starting place operation to target: {target_location}") + self.logger.debug("Moving to above goal position") + self.ur.movel(above_goal, self.acceleration, self.velocity) + + self.logger.debug("Moving to goal position") + self.ur.movel(target_location, self.acceleration, self.velocity) + + self.open_gripper() + + if self.resource_client and isinstance(target, LocationArgument): # Handle resources if configured + try: + popped_object, updated_resource = self.resource_client.pop(resource=self.gripper_resource_id) + self.resource_client.push(resource=target.resource_id, child=popped_object) + except Exception as e: + self.logger.error(f"Resource client error during place: {e}\n{traceback.format_exc()}") + + self.logger.debug("Moving back to above goal position") + self.ur.movel(above_goal, self.acceleration, self.velocity) + self.logger.info("Place operation completed successfully") + + except GripperOperationError: + raise + except Exception as e: + self.logger.error(f"Error during place operation: {e}\n{traceback.format_exc()}") + raise GripperOperationError(f"Place operation failed: {e}") # noqa def place_screw( self, @@ -283,7 +377,7 @@ def place_screw( # Move to the target location if isinstance(target, LocationArgument): - target_location = target.location + target_location = target.representation elif isinstance(target, list): target_location = target @@ -293,13 +387,13 @@ def place_screw( self.ur.movel(target_location, 0.2, 0.2) target_pose = [0, 0, 0.00021, 0, 0, 3.14] # Setting the screw drive motion - print("Screwing down") + self.logger.info("Screwing down") self.ur.speedl_tool( target_pose, 2, screw_time ) # This will perform screw driving motion for defined number of seconds sleep(screw_time) - print("Screw drive motion completed") + self.logger.info("Screw drive motion completed") self.ur.translate_tool([0, 0, -0.03], 0.5, 0.5) @@ -312,7 +406,7 @@ def remove_cap( """Handles the remove cap request""" self.open_gripper() if isinstance(source, LocationArgument): - source_location = source.location + source_location = source.representation elif isinstance(source, list): source_location = source @@ -323,8 +417,12 @@ def remove_cap( self.close_gripper() + if self.resource_client and isinstance(source, LocationArgument): # Handle resources if configured + popped_object, updated_resource = self.resource_client.pop(resource=source.resource_id) + self.resource_client.push(resource=self.gripper_resource_id, child=popped_object) + target_pose = [0, 0, -0.001, 0, 0, -3.14] # Setting the screw drive motion - print("Removing cap") + self.logger.info("Removing cap") screw_time = 7 self.ur.speedl_tool( target_pose, 2, screw_time @@ -348,7 +446,7 @@ def place_cap( self.home_robot(home) if isinstance(target, LocationArgument): - target_location = target.location + target_location = target.representation elif isinstance(target, list): target_location = target @@ -360,7 +458,7 @@ def place_cap( # self.close_gripper() target_pose = [0, 0, 0.0001, 0, 0, 2.10] # Setting the screw drive motion - print("Placing cap") + self.logger.info("Placing cap") screw_time = 6 self.ur.speedl_tool( target_pose, 2, screw_time @@ -368,6 +466,11 @@ def place_cap( sleep(screw_time) self.open_gripper() + + if self.resource_client and isinstance(target, LocationArgument): # Handle resources if configured + popped_object, updated_resource = self.resource_client.pop(resource=self.gripper_resource_id) + self.resource_client.push(resource=target.resource_id, child=popped_object) + self.ur.translate_tool([0, 0, -0.03], 0.5, 0.5) self.home_robot(home) @@ -403,19 +506,27 @@ def transfer( target_approach_distance: float = None, ) -> None: """Handles the transfer request""" - self.pick( - source=source, - approach_axis=source_approach_axis, - approach_distance=source_approach_distance, - ) - print("Pick up completed") - self.home_robot(home=home) - self.place( - target=target, - approach_axis=target_approach_axis, - approach_distance=target_approach_distance, - ) - print("Place completed") + try: + self.logger.info("Starting transfer operation") + self.pick( + source=source, + approach_axis=source_approach_axis, + approach_distance=source_approach_distance, + ) + self.logger.info("Pick completed") + + self.home_robot(home=home) + + self.place( + target=target, + approach_axis=target_approach_axis, + approach_distance=target_approach_distance, + ) + self.logger.info("Place completed") + + except Exception as e: + self.logger.error(f"Error during transfer operation: {e}\n{traceback.format_exc()}") + raise def screw_transfer( self, diff --git a/src/ur_interface/ur_tools/robotiq_gripper_driver.py b/src/ur_interface/ur_tools/robotiq_gripper_driver.py index d09e2d1..141a387 100644 --- a/src/ur_interface/ur_tools/robotiq_gripper_driver.py +++ b/src/ur_interface/ur_tools/robotiq_gripper_driver.py @@ -55,11 +55,16 @@ def __init__(self): self._min_force = 0 self._max_force = 255 + def __del__(self): + """Destructor.""" + if self.socket is not None: + self.disconnect() + def connect( self, hostname: str, port: int, - socket_timeout: float = 2.0, + socket_timeout: float = 60.0, ) -> None: """Connects to a gripper at the given address. :param hostname: Hostname or ip. diff --git a/src/ur_rest_node.py b/src/ur_rest_node.py index df385fc..560762b 100644 --- a/src/ur_rest_node.py +++ b/src/ur_rest_node.py @@ -1,22 +1,19 @@ """REST-based node for UR robots""" +import traceback from typing import Optional, Union -from madsci.client.resource_client import ResourceClient from madsci.common.types.action_types import ActionFailed, ActionSucceeded from madsci.common.types.admin_command_types import AdminCommandResponse -from madsci.common.types.auth_types import OwnershipInfo from madsci.common.types.location_types import LocationArgument from madsci.common.types.node_types import RestNodeConfig -from madsci.common.types.resource_types.definitions import ( - PoolResourceDefinition, - SlotResourceDefinition, -) +from madsci.common.types.resource_types import Pool, Slot from madsci.node_module.helpers import action from madsci.node_module.rest_node_module import RestNode from typing_extensions import Annotated from ur_interface.ur import UR +from ur_interface.ur_error_types import GripperError, URMovementError from ur_interface.ur_kinematics import get_pose_from_joint_angles from ur_interface.ur_tools.gripper_controller import FingerGripperController @@ -24,44 +21,108 @@ class URNodeConfig(RestNodeConfig): """Configuration for the UR node module.""" - ur_ip: str + ur_ip: Optional[str] = None tcp_pose: list = [0, 0, 0, 0, 0, 0] - base_reference_frame: list = None + base_reference_frame: Optional[list] = None ur_model: str = "UR5e" + use_resources: bool = True class URNode(RestNode): """A Rest Node object to control UR robots""" ur_interface: UR = None + config: URNodeConfig = URNodeConfig() config_model = URNodeConfig def startup_handler(self) -> None: """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" try: - if self.config.resource_server_url: - self.resource_client = ResourceClient(self.config.resource_server_url) - self.resource_owner = OwnershipInfo(node_id=self.node_definition.node_id) - - else: - self.resource_client = None + # Create templates + if self.config.use_resources: + self._create_ur_templates() self.logger.log("Node initializing...") self.ur_interface = UR( hostname=self.config.ur_ip, - resource_client=self.resource_client, + resource_client=self.resource_client if self.config.use_resources else None, tcp_pose=self.config.tcp_pose, base_reference_frame=self.config.base_reference_frame, + logger=self.logger, ) self.tool_resource = None - + self.current_location = None except Exception as err: - self.logger.log_error(f"Error starting the UR Node: {err}") + self.logger.log_error(f"Error starting the UR Node: {err}\n{traceback.format_exc()}") self.startup_has_run = False else: self.startup_has_run = True self.logger.log("UR node initialized!") + def _create_ur_templates(self) -> None: + """Create all UR-specific resource templates.""" + + # 1. Gripper slot template + gripper_slot = Slot( + resource_name="robotiq_finger_gripper", + resource_class="URGripper", + capacity=1, + attributes={ + "gripper_type": "robotiq_finger", + "max_grip_force": 235.0, + "min_grip_position": 0, + "max_grip_position": 255, + "description": "UR Robotiq finger gripper slot", + }, + ) + + self.resource_client.init_template( + resource=gripper_slot, + template_name="robotiq_finger_gripper_slot", + description="Template for UR Robotiq finger gripper slot. Used to track what the gripper is holding.", + required_overrides=["resource_name"], + tags=["ur", "gripper", "slot", "robotiq"], + created_by=self.node_definition.node_id, + version="1.0.0", + ) + + # Initialize gripper resource from template + self.gripper_resource = self.resource_client.create_resource_from_template( + template_name="robotiq_finger_gripper_slot", + resource_name=f"ur_gripper_{self.node_definition.node_name}", + add_to_database=True, + ) + + # 2. Pipette pool template + pipette_pool = Pool( + resource_name="tricontinent_pipette", + resource_class="URPipette", + capacity=1000.0, + attributes={ + "pipette_type": "tricontinent", + "min_volume": 1.0, + "max_volume": 1000.0, + "default_speed": 150, + "description": "Tricontinent pipette pool for tracking tips and aspirated liquid", + }, + ) + + self.resource_client.init_template( + resource=pipette_pool, + template_name="tricontinent_pipette_pool", + description="Template for Tricontinent pipette pool. Tracks pipette tips and aspirated liquids.", + required_overrides=["resource_name"], + tags=["ur", "pipette", "pool", "liquid-handling"], + created_by=self.node_definition.node_id, + version="1.0.0", + ) + # Initialize pipette resource from template + self.pipette_resource = self.resource_client.create_resource_from_template( + template_name="tricontinent_pipette_pool", + resource_name=f"ur_pipette_{self.node_definition.node_name}", + add_to_database=True, + ) + def shutdown_handler(self) -> None: """Called to shutdown the node. Should be used to close connections to devices or release any other resources.""" try: @@ -74,38 +135,42 @@ def shutdown_handler(self) -> None: except Exception as err: self.logger.log_error(f"Error shutting down the UR Node: {err}") - def state_handler(self) -> None: - """Periodically called to update the current state of the node.""" - if self.ur_interface: - # Getting robot state - self.ur_interface.ur_dashboard.get_overall_robot_status() - movement_state, current_location = self.ur_interface.get_movement_state() - else: - self.logger.log_error("UR interface is not initialized") - return - if "NORMAL" not in self.ur_interface.ur_dashboard.safety_status: - self.node_state = { - "ur_status_code": "ERROR", - "current_joint_angles": current_location, - } - self.logger.log_error(f"UR ERROR: {self.ur_interface.ur_dashboard.safety_status}") + def status_handler(self): + """Periodically called to update the current status of the node.""" + if not self.node_status.busy: + if self.ur_interface: + # Getting robot state + self.ur_interface.ur_dashboard.get_overall_robot_status() + movement_state, self.current_location = self.ur_interface.get_movement_state() + else: + self.logger.log_error("UR interface is not initialized") + return - elif movement_state == "BUSY": - self.node_state = { - "ur_status_code": "BUSY", - "current_joint_angles": current_location, - } - self.logger.info("BUSY") + if "PROTECTIVE_STOP" in self.ur_interface.ur_dashboard.safety_status: + self.node_status.stopped = True + self.logger.log_error("UR is in PROTECTIVE_STOP") - elif movement_state == "READY": - self.node_state = { - "ur_status_code": "READY", - "current_joint_angles": current_location, - } + if "NORMAL" not in self.ur_interface.ur_dashboard.safety_status: + self.node_status.errored = True + self.logger.log_error(f"UR ERROR: {self.ur_interface.ur_dashboard.safety_status}") + else: + self.node_status.errored = False + self.node_status.stopped = False + + if movement_state == "BUSY": + self.node_status.busy = True + self.logger.info("BUSY") + elif movement_state == "READY": + self.node_status.busy = False else: + if len(self.node_status.running_actions) == 0: + self.node_status.busy = False + + def state_handler(self) -> None: + """Periodically called to update the current state of the node.""" + if self.ur_interface: self.node_state = { - "ur_status_code": "UNKOWN", - "current_joint_angles": current_location, + "current_joint_angles": self.current_location, } @action(name="getj", description="Get joint angles") @@ -126,7 +191,6 @@ def getl(self): def set_freedrive(self, timeout: Annotated[int, "how long to do freedrive"] = 60): """set the robot into freedrive""" self.ur_interface.ur_connection.set_freedrive(True, timeout) - return ActionSucceeded() @action(name="set_movement_params", description="Set speed and acceleration parameters") def set_movement_params( @@ -134,8 +198,8 @@ def set_movement_params( tcp_pose: Optional[list] = None, velocity: Optional[float] = None, acceleration: Optional[float] = None, - gripper_speed: Optional[float] = None, - gripper_force: Optional[float] = None, + gripper_speed: Optional[int] = None, + gripper_force: Optional[int] = None, ): """Configure the robot's movement parameters for subsequent transfers""" if tcp_pose is not None: @@ -148,42 +212,40 @@ def set_movement_params( self.ur_interface.gripper_speed = gripper_speed if gripper_force is not None: self.ur_interface.gripper_force = gripper_force - return ActionSucceeded() @action(name="movej", description="Move the robot using joint angles") def movej( self, - joints: Annotated[Union[LocationArgument, list], "Joint angles to move to"], + joints: Annotated[LocationArgument, "Joint angles to move to"], acceleration: Annotated[Optional[float], "Acceleration"] = 0.6, velocity: Annotated[Optional[float], "Velocity"] = 0.6, ): """Move the robot using joint angles""" try: - self.logger.log(f"Move joints: {joints.location}") - self.ur_interface.ur_connection.movej(joints=joints.location, acc=acceleration, vel=velocity) + self.logger.log(f"Move joints: {joints.representation}") + self.ur_interface.ur_connection.movej(joints=joints.representation, acc=acceleration, vel=velocity) except Exception as err: self.logger.log_error(err) - return ActionSucceeded() - @action(name="movel", description="Move the robot using linar motion") def movel( self, - target: Annotated[Union[LocationArgument, list], "Linear location to move to"], + target: Annotated[LocationArgument, "Linear location to move to"], acceleration: Annotated[Optional[float], "Acceleration"] = 0.6, velocity: Annotated[Optional[float], "Velocity"] = 0.6, + joint_angle_locations: Annotated[bool, "Use joint angles for the location"] = True, ): """Move the robot using linear motion""" try: - self.logger.log(f"Move location: {target.location}") - self.ur_interface.ur_connection.movel(tpose=target.location, acc=acceleration, vel=velocity) + self.logger.log(f"Move location: {target.representation}") + if joint_angle_locations: + target.representation = get_pose_from_joint_angles(target.representation) + self.ur_interface.ur_connection.movel(tpose=target.representation, acc=acceleration, vel=velocity) except Exception as err: self.logger.log_error(err) - return ActionSucceeded() - @action(name="toggle_gripper", description="Move the robot gripper") def toggle_gripper( self, @@ -191,26 +253,20 @@ def toggle_gripper( close: Annotated[bool, "Close?"] = False, ): """Open or close the robot gripper.""" - try: - gripper = FingerGripperController(hostname=self.config.ur_ip, ur=self.ur_interface) - self.logger.log("Connecting to gripper...") - gripper.connect_gripper() - self.logger.log("Gripper connected") - if open: - gripper.open_gripper() - self.logger.log("Gripper opened") - elif close: - gripper.close_gripper() - self.logger.log("Gripper closed") - else: - self.logger.log("No action taken") - - except Exception as err: - self.logger.log_error(err) + gripper = FingerGripperController(hostname=self.config.ur_ip, ur=self.ur_interface, logger=self.logger) + self.logger.info("Toggle gripper: Connecting to gripper...") + gripper.connect_gripper() + self.logger.info("Toggle gripper: Gripper connected") + if open: + gripper.open_gripper() + self.logger.info("Toggle gripper: Gripper opened") + elif close: + gripper.close_gripper() + self.logger.info("Toggle gripper: Gripper closed") else: - gripper.disconnect_gripper() - self.logger.log("Gripper disconnected") - return ActionSucceeded() + self.logger.info("Toggle gripper: No action taken") + gripper.disconnect_gripper() + self.logger.info("Toggle gripper: Gripper disconnected") @action( name="gripper_transfer", @@ -231,24 +287,27 @@ def gripper_transfer( ): """Make a transfer using the finger gripper. This function uses linear motions to perform the pick and place movements.""" try: - # Check if the source, target and home locations are provided - if not source or not target or not home: # Return Fail - return ActionFailed(errors="Source, target and home locations must be provided") - - if self.resource_client: - # If the gripper resource is not initialized, initialize it - self.tool_resource = self.resource_client.init_resource( - SlotResourceDefinition( - resource_name="ur_gripper", - owner=self.resource_owner, - ) + if ( + self.config.use_resources + and isinstance(source, LocationArgument) + and isinstance(target, LocationArgument) + ): + self.ur_interface.tool_resource_id = self.gripper_resource.resource_id + source_resource = self.resource_client.get_resource(source.resource_id) + target_resource = self.resource_client.get_resource(target.resource_id) + if source_resource.quantity == 0: + raise ValueError("Resource manager: Source location is empty!") + if target_resource.quantity != 0: + raise ValueError("Resource manager: Target is occupied!") + + if joint_angle_locations and isinstance(source, LocationArgument) and isinstance(target, LocationArgument): + source.representation = get_pose_from_joint_angles( + joints=source.representation, robot_model=self.config.ur_model ) - self.ur_interface.tool_resource_id = self.tool_resource.resource_id - - if joint_angle_locations and isinstance(source, LocationArgument): - source.location = get_pose_from_joint_angles(joints=source.location, robot_model=self.config.ur_model) - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) - elif joint_angle_locations and isinstance(source, list): + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) + elif joint_angle_locations and isinstance(source, list) and isinstance(target, list): source = get_pose_from_joint_angles(joints=source, robot_model=self.config.ur_model) target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) @@ -263,16 +322,24 @@ def gripper_transfer( gripper_open=gripper_open, gripper_close=gripper_close, ) - except Exception as err: - return ActionFailed(errors=err) - return ActionSucceeded() + except GripperError as err: + self.logger.log_error(f"Gripper error during transfer: {err}") + return ActionFailed(errors=str(err)) + + except URMovementError as err: + self.logger.log_error(f"Movement error during transfer: {err}") + return ActionFailed(errors=str(err)) + + except Exception as err: + self.logger.log_error(f"Unexpected error during gripper transfer: {err}") + return ActionFailed(errors=str(err)) @action() def gripper_pick( self, - home: Annotated[Union[LocationArgument, list], "Home location"], - source: Annotated[Union[LocationArgument, list], "Location to transfer sample from"], + home: Annotated[LocationArgument, "Home location"], + source: Annotated[LocationArgument, "Location to transfer sample from"], source_approach_axis: Annotated[Optional[str], "Source location approach axis, (X/Y/Z)"] = "z", source_approach_distance: Annotated[Optional[float], "Approach distance in meters"] = 0.05, gripper_close: Annotated[Optional[int], "Set a min value for the gripper close state"] = 255, @@ -280,21 +347,19 @@ def gripper_pick( ): """Use the gripper to pick a piece of labware from the specified source""" try: - if self.resource_client: - # If the gripper resource is not initialized, initialize it - self.tool_resource = self.resource_client.init_resource( - SlotResourceDefinition( - resource_name="ur_gripper", - owner=self.resource_owner, - ) - ) - self.ur_interface.tool_resource_id = self.tool_resource.resource_id + if self.config.use_resources: + self.ur_interface.tool_resource_id = self.gripper_resource.resource_id + source_resource = self.resource_client.get_resource(source.resource_id) + if source_resource.quantity == 0: + raise ValueError("Resource manager: Source location is empty!") if joint_angle_locations and isinstance(source, LocationArgument): - source.location = get_pose_from_joint_angles(joints=source.location, robot_model=self.config.ur_model) + source.representation = get_pose_from_joint_angles( + joints=source.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(source, list): source = get_pose_from_joint_angles(joints=source, robot_model=self.config.ur_model) - + self.logger.log_info(f"Picking from source: {source.representation}") self.ur_interface.gripper_pick( home=home, source=source, @@ -302,16 +367,24 @@ def gripper_pick( source_approach_axis=source_approach_axis, gripper_close=gripper_close, ) - except Exception as err: - return ActionFailed(errors=err) - return ActionSucceeded() + except GripperError as err: + self.logger.log_error(f"Gripper error during pick: {err}") + return ActionFailed(errors=str(err)) + + except URMovementError as err: + self.logger.log_error(f"Movement error during pick: {err}") + return ActionFailed(errors=str(err)) + + except Exception as err: + self.logger.log_error(f"Unexpected error during gripper pick: {err}") + return ActionFailed(errors=str(err)) @action() def gripper_place( self, - home: Annotated[Union[LocationArgument, list], "Home location"], - target: Annotated[Union[LocationArgument, list], "Location to transfer sample to"], + home: Annotated[LocationArgument, "Home location"], + target: Annotated[LocationArgument, "Location to transfer sample to"], target_approach_axis: Annotated[Optional[str], "Source location approach axis, (X/Y/Z)"] = "z", target_approach_distance: Annotated[Optional[float], "Approach distance in meters"] = 0.05, gripper_open: Annotated[Optional[int], "Set a max value for the gripper open state"] = 0, @@ -319,18 +392,16 @@ def gripper_place( ): """Use the gripper to place a piece of labware at the target.""" try: - if self.resource_client: - # If the gripper resource is not initialized, initialize it - self.tool_resource = self.resource_client.init_resource( - SlotResourceDefinition( - resource_name="ur_gripper", - owner=self.resource_owner, - ) - ) - self.ur_interface.tool_resource_id = self.tool_resource.resource_id + if self.config.use_resources and isinstance(target, LocationArgument): + self.ur_interface.tool_resource_id = self.gripper_resource.resource_id + target_resource = self.resource_client.get_resource(target.resource_id) + if target_resource.quantity != 0: + raise ValueError("Resource manager: Target is occupied!") if joint_angle_locations and isinstance(target, LocationArgument): - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(target, list): target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) @@ -341,9 +412,17 @@ def gripper_place( target_approach_axis=target_approach_axis, gripper_open=gripper_open, ) + except GripperError as err: + self.logger.log_error(f"Gripper error during place: {err}\n{traceback.format_exc()}") + return ActionFailed(errors=str(err)) + + except URMovementError as err: + self.logger.log_error(f"Movement error during place: {err}\n{traceback.format_exc()}") + return ActionFailed(errors=str(err)) + except Exception as err: - return ActionFailed(errors=err) - return ActionSucceeded() + self.logger.log_error(f"Unexpected error during gripper place: {err}\n{traceback.format_exc()}") + return ActionFailed(errors=str(err)) @action( name="pick_tool", @@ -359,27 +438,20 @@ def pick_tool( joint_angle_locations: Annotated[bool, "Use joint angles for all the locations"] = True, ): """Pick a tool with the UR""" - - if not tool_loc or not home: # Return Fail - return ActionFailed(errors="tool_loc and home locations must be provided") - if joint_angle_locations and isinstance(tool_loc, LocationArgument): - tool_loc.location = get_pose_from_joint_angles(joints=tool_loc.location, robot_model=self.config.ur_model) + tool_loc.representation = get_pose_from_joint_angles( + joints=tool_loc.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(tool_loc, list): tool_loc = get_pose_from_joint_angles(joints=tool_loc, robot_model=self.config.ur_model) - try: - self.ur_interface.pick_tool( - home=home, - tool_loc=tool_loc, - docking_axis=docking_axis, - payload=payload, - tool_name=tool_name, - ) - except Exception as err: - return ActionFailed(errors=err) - - return ActionSucceeded() + self.ur_interface.pick_tool( + home=home, + tool_loc=tool_loc, + docking_axis=docking_axis, + payload=payload, + tool_name=tool_name, + ) @action(name="Place_tool", description="Places the attached tool back to the provided tool docking location") def place_tool( @@ -391,24 +463,19 @@ def place_tool( joint_angle_locations: Annotated[bool, "Use joint angles for all the locations"] = True, ): """Place a tool with the UR""" - try: - if joint_angle_locations and isinstance(tool_docking, LocationArgument): - tool_docking.location = get_pose_from_joint_angles( - joints=tool_docking.location, robot_model=self.config.ur_model - ) - elif joint_angle_locations and isinstance(tool_docking, list): - tool_docking = get_pose_from_joint_angles(joints=tool_docking, robot_model=self.config.ur_model) - - self.ur_interface.place_tool( - home=home, - tool_loc=tool_docking, - docking_axis=docking_axis, - tool_name=tool_name, + if joint_angle_locations and isinstance(tool_docking, LocationArgument): + tool_docking.representation = get_pose_from_joint_angles( + joints=tool_docking.representation, robot_model=self.config.ur_model ) - except Exception as err: - return ActionFailed(errors=err) + elif joint_angle_locations and isinstance(tool_docking, list): + tool_docking = get_pose_from_joint_angles(joints=tool_docking, robot_model=self.config.ur_model) - return ActionSucceeded() + self.ur_interface.place_tool( + home=home, + tool_loc=tool_docking, + docking_axis=docking_axis, + tool_name=tool_name, + ) @action( name="gripper_screw_transfer", @@ -427,34 +494,30 @@ def gripper_screw_transfer( ): """Make a screwdriving transfer using Robotiq gripper and custom screwdriving bits with UR""" - if not home or not screwdriver_loc or not screw_loc or not target: - return ActionFailed(errors="screwdriver_loc, screw_loc and home locations must be provided") - if joint_angle_locations and isinstance(screwdriver_loc, LocationArgument): - screwdriver_loc.location = get_pose_from_joint_angles( - joints=screwdriver_loc.location, robot_model=self.config.ur_model + screwdriver_loc.representation = get_pose_from_joint_angles( + joints=screwdriver_loc.representation, robot_model=self.config.ur_model + ) + screw_loc.representation = get_pose_from_joint_angles( + joints=screw_loc.representation, robot_model=self.config.ur_model + ) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model ) - screw_loc.location = get_pose_from_joint_angles(joints=screw_loc.location, robot_model=self.config.ur_model) - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) elif joint_angle_locations and isinstance(screwdriver_loc, list): screwdriver_loc = get_pose_from_joint_angles(joints=screwdriver_loc, robot_model=self.config.ur_model) screw_loc = get_pose_from_joint_angles(joints=screw_loc, robot_model=self.config.ur_model) target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) - try: - self.ur_interface.gripper_screw_transfer( - home=home, - screwdriver_loc=screwdriver_loc, - screw_loc=screw_loc, - screw_time=screw_time, - target=target, - gripper_open=gripper_open, - gripper_close=gripper_close, - ) - except Exception as err: - return ActionFailed(errors=err) - - return ActionSucceeded() + self.ur_interface.gripper_screw_transfer( + home=home, + screwdriver_loc=screwdriver_loc, + screw_loc=screw_loc, + screw_time=screw_time, + target=target, + gripper_open=gripper_open, + gripper_close=gripper_close, + ) @action( name="pipette_transfer", @@ -471,43 +534,34 @@ def pipette_transfer( joint_angle_locations: Annotated[bool, "Use joint angles for all the locations"] = True, ): """Make a pipette transfer for the defined volume with UR""" - if not home or not source or not target or not tip_loc or not tip_trash: - return ActionFailed(errors="home, source, target, tip_loc and tip_trash locations must be provided") if joint_angle_locations and isinstance(source, LocationArgument): - source.location = get_pose_from_joint_angles(joints=source.location, robot_model=self.config.ur_model) - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) - tip_loc.location = get_pose_from_joint_angles(joints=tip_loc.location, robot_model=self.config.ur_model) - tip_trash.location = get_pose_from_joint_angles(joints=tip_trash.location, robot_model=self.config.ur_model) + source.representation = get_pose_from_joint_angles( + joints=source.representation, robot_model=self.config.ur_model + ) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) + tip_loc.representation = get_pose_from_joint_angles( + joints=tip_loc.representation, robot_model=self.config.ur_model + ) + tip_trash.representation = get_pose_from_joint_angles( + joints=tip_trash.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(source, list): source = get_pose_from_joint_angles(joints=source, robot_model=self.config.ur_model) target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) tip_loc = get_pose_from_joint_angles(joints=tip_loc, robot_model=self.config.ur_model) tip_trash = get_pose_from_joint_angles(joints=tip_trash, robot_model=self.config.ur_model) - try: - if self.resource_client: - # If the pipette resource is not initialized, initialize it - self.tool_resource = self.resource_client.init_resource( - PoolResourceDefinition( - resource_name="ur_pipette", - owner=self.resource_owner, - ) - ) - self.ur_interface.tool_resource_id = self.tool_resource.resource_id - - self.ur_interface.pipette_transfer( - home=home, - tip_loc=tip_loc, - tip_trash=tip_trash, - source=source, - target=target, - volume=volume, - ) - except Exception as err: - return ActionFailed(errors=err) - - return ActionSucceeded() + self.ur_interface.pipette_transfer( + home=home, + tip_loc=tip_loc, + tip_trash=tip_trash, + source=source, + target=target, + volume=volume, + ) @action( name="pipette_pick_and_move_sample", @@ -527,12 +581,14 @@ def pipette_pick_and_move_sample( """Picks and moves a sample with UR""" if joint_angle_locations and isinstance(target, LocationArgument): - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) - sample_loc.location = get_pose_from_joint_angles( - joints=sample_loc.location, robot_model=self.config.ur_model + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model ) - tip_loc.location = ( - get_pose_from_joint_angles(joints=tip_loc.location, robot_model=self.config.ur_model) + sample_loc.representation = get_pose_from_joint_angles( + joints=sample_loc.representation, robot_model=self.config.ur_model + ) + tip_loc.representation = ( + get_pose_from_joint_angles(joints=tip_loc.representation, robot_model=self.config.ur_model) if tip_loc else None ) @@ -541,20 +597,17 @@ def pipette_pick_and_move_sample( sample_loc = get_pose_from_joint_angles(joints=sample_loc, robot_model=self.config.ur_model) tip_loc = get_pose_from_joint_angles(joints=tip_loc, robot_model=self.config.ur_model) if tip_loc else None - try: - self.ur_interface.pipette_pick_and_move_sample( - home=home, - safe_waypoint=safe_waypoint, - sample_loc=sample_loc, - target=target, - volume=volume, - tip_loc=tip_loc, - pipette_speed=pipette_speed, - ) - except Exception as err: - return ActionFailed(errors=err) + self.ur_interface.tool_resource_id = self.pipette_resource.resource_id - return ActionSucceeded() + self.ur_interface.pipette_pick_and_move_sample( + home=home, + safe_waypoint=safe_waypoint, + sample_loc=sample_loc, + target=target, + volume=volume, + tip_loc=tip_loc, + pipette_speed=pipette_speed, + ) @action( name="pipette_dispense_and_retrieve", @@ -571,11 +624,12 @@ def pipette_dispense_and_retrieve( pipette_speed: Annotated[Optional[int], "Pipette speed in m/s"] = 150, ): """Dispenses a sample and retrieves the pipette with UR""" - if joint_angle_locations and isinstance(target, LocationArgument): - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) - tip_trash.location = ( - get_pose_from_joint_angles(joints=tip_trash.location, robot_model=self.config.ur_model) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) + tip_trash.representation = ( + get_pose_from_joint_angles(joints=tip_trash.representation, robot_model=self.config.ur_model) if tip_trash else None ) @@ -585,19 +639,16 @@ def pipette_dispense_and_retrieve( get_pose_from_joint_angles(joints=tip_trash, robot_model=self.config.ur_model) if tip_trash else None ) - try: - self.ur_interface.pipette_dispense_and_retrieve( - home=home, - safe_waypoint=safe_waypoint, - target=target, - volume=volume, - tip_trash=tip_trash, - pipette_speed=pipette_speed, - ) - except Exception as err: - return ActionFailed(errors=err) + self.ur_interface.tool_resource_id = self.pipette_resource.resource_id - return ActionSucceeded() + self.ur_interface.pipette_dispense_and_retrieve( + home=home, + safe_waypoint=safe_waypoint, + target=target, + volume=volume, + tip_trash=tip_trash, + pipette_speed=pipette_speed, + ) @action( name="pick_and_flip_object", @@ -615,26 +666,23 @@ def pick_and_flip_object( ): """Picks and flips an object 180 degrees with UR""" - if not home or not target: - return ActionFailed(errors="home and target locations must be provided") if joint_angle_locations and isinstance(target, LocationArgument): - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(target, list): target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) - try: - self.ur_interface.pick_and_flip_object( - home=home, - target=target, - approach_axis=approach_axis, - target_approach_distance=target_approach_distance, - gripper_open=gripper_open, - gripper_close=gripper_close, - ) - except Exception as err: - return ActionFailed(errors=err) + self.ur_interface.tool_resource_id = self.gripper_resource.resource_id - return ActionSucceeded() + self.ur_interface.pick_and_flip_object( + home=home, + target=target, + approach_axis=approach_axis, + target_approach_distance=target_approach_distance, + gripper_open=gripper_open, + gripper_close=gripper_close, + ) @action( name="remove_cap", @@ -653,27 +701,26 @@ def remove_cap( joint_angle_locations: Annotated[bool, "Use joint angles for all the locations"] = True, ): """Remove caps from sample vials with UR""" - if not home or not source or not target: - return ActionFailed(errors="home, source and target locations must be provided") if joint_angle_locations and isinstance(source, LocationArgument): - source.location = get_pose_from_joint_angles(joints=source.location, robot_model=self.config.ur_model) - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) + source.representation = get_pose_from_joint_angles( + joints=source.representation, robot_model=self.config.ur_model + ) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(source, list): source = get_pose_from_joint_angles(joints=source, robot_model=self.config.ur_model) target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) - try: - self.ur_interface.remove_cap( - home=home, - source=source, - target=target, - gripper_open=gripper_open, - gripper_close=gripper_close, - ) - except Exception as err: - return ActionFailed(errors=err) + self.ur_interface.tool_resource_id = self.gripper_resource.resource_id - return ActionSucceeded() + self.ur_interface.remove_cap( + home=home, + source=source, + target=target, + gripper_open=gripper_open, + gripper_close=gripper_close, + ) @action( name="place_cap", @@ -689,27 +736,26 @@ def place_cap( joint_angle_locations: Annotated[bool, "Use joint angles for all the locations"] = True, ): """Places caps back to sample vials with UR""" - if not home or not source or not target: - return ActionFailed(errors="home, source and target locations must be provided") if joint_angle_locations and isinstance(source, LocationArgument): - source.location = get_pose_from_joint_angles(joints=source.location, robot_model=self.config.ur_model) - target.location = get_pose_from_joint_angles(joints=target.location, robot_model=self.config.ur_model) + source.representation = get_pose_from_joint_angles( + joints=source.representation, robot_model=self.config.ur_model + ) + target.representation = get_pose_from_joint_angles( + joints=target.representation, robot_model=self.config.ur_model + ) elif joint_angle_locations and isinstance(source, list): source = get_pose_from_joint_angles(joints=source, robot_model=self.config.ur_model) target = get_pose_from_joint_angles(joints=target, robot_model=self.config.ur_model) - try: - self.ur_interface.place_cap( - home=home, - source=source, - target=target, - gripper_open=gripper_open, - gripper_close=gripper_close, - ) - except Exception as err: - return ActionFailed(errors=err) + self.ur_interface.tool_resource_id = self.gripper_resource.resource_id - return ActionSucceeded() + self.ur_interface.place_cap( + home=home, + source=source, + target=target, + gripper_open=gripper_open, + gripper_close=gripper_close, + ) @action( name="run_urp_program", @@ -721,15 +767,10 @@ def run_urp_program( program_name=Annotated[str, "Program name"], ): """Run an URP program on the UR""" - try: - self.ur_interface.run_urp_program( - transfer_file_path=transfer_file_path, - program_name=program_name, - ) - except Exception as err: - return ActionFailed(errors=err) - - return ActionSucceeded() + self.ur_interface.run_urp_program( + transfer_file_path=transfer_file_path, + program_name=program_name, + ) @action( name="set_digital_io", @@ -741,30 +782,30 @@ def set_digital_io( value=Annotated[bool, "True/False"], ): """Sets a channel IO output on the UR""" - try: - self.ur_interface.set_digital_io(channel=channel, value=value) - except Exception as err: - return ActionFailed(errors=err) - - return ActionSucceeded() + self.ur_interface.set_digital_io(channel=channel, value=value) def get_location(self) -> AdminCommandResponse: """Return the current position of the ur robot""" - try: - return AdminCommandResponse(data=self.ur_interface.ur_connection.getj()) - except Exception: - return AdminCommandResponse(success=False) + return AdminCommandResponse(data=self.ur_interface.ur_connection.getj()) + + def reset(self) -> AdminCommandResponse: + """Reset the ur robot""" + self.logger.log("Resetting node...") + # If resetting startup handler does not work, try re-initializing the dashboard + self.ur_interface.disconnect() + result = super().reset() + self.logger.log("Node reset.") + return result @action(name="e_stop", description="Emergency stop the UR robot") def e_stop(self): """Emergency stop the UR robot""" try: - # self.ur_interface.ur_dashboard.stop_program() self.ur_interface.ur_dashboard.power_off() - self.logger.log_info("EMERRGENCY STOP EXECUTED") - return ActionSucceeded() + self.logger.log_info("EMERGENCY STOP EXECUTED") + except Exception as err: - self.logger.log_error(f"FAILED EMERRGENCY STOP: {err}") + self.logger.log_error(f"FAILED EMERGENCY STOP: {err}") return ActionFailed(errors=str(err)) def safety_stop(self) -> AdminCommandResponse: diff --git a/tests/node_test.ipynb b/tests/node_test.ipynb index e79d1de..c6df7b7 100644 --- a/tests/node_test.ipynb +++ b/tests/node_test.ipynb @@ -10,7 +10,7 @@ "from madsci.common.types.action_types import ActionRequest\n", "from madsci.common.types.location_types import LocationArgument\n", "\n", - "client = RestNodeClient(url=\"http://localhost:3030\")" + "client = RestNodeClient(url=\"http://localhost:2000\")" ] }, { @@ -37,44 +37,91 @@ "metadata": {}, "outputs": [], "source": [ - "request = ActionRequest(\n", - " action_name=\"gripper_transfer\",\n", - " args={\n", - " \"home\": LocationArgument(\n", - " location=[\n", - " -6.1809425989734095,\n", - " -1.9489895306029261,\n", - " 2.05842096010317,\n", - " 6.114075171738424,\n", - " 1.5849356651306152,\n", - " 0.007065845653414726,\n", - " ],\n", - " resource_id=None,\n", - " ).model_dump(mode=\"json\"),\n", - " \"source\": LocationArgument(\n", - " location=[\n", - " -4.626934830342428,\n", - " -0.6786025923541565,\n", - " 1.0300758520709437,\n", - " 5.948157655983724,\n", - " 1.6586629152297974,\n", - " 0.0070164683274924755,\n", - " ],\n", - " resource_id=None,\n", - " ).model_dump(mode=\"json\"),\n", - " \"target\": LocationArgument(\n", - " location=[\n", - " -4.626934830342428,\n", - " -0.6786025923541565,\n", - " 1.0300758520709437,\n", - " 5.948157655983724,\n", - " 1.6586629152297974,\n", - " 0.0070164683274924755,\n", - " ],\n", - " resource_id=None,\n", - " ).model_dump(mode=\"json\"),\n", - " },\n", - ")" + "for _ in range(1):\n", + " request = ActionRequest(\n", + " action_name=\"set_movement_params\",\n", + " args={\n", + " \"gripper_speed\": 50,\n", + " },\n", + " )\n", + " request = ActionRequest(\n", + " action_name=\"gripper_transfer\",\n", + " args={\n", + " \"home\": LocationArgument(\n", + " location=[\n", + " 0.066341333091259,\n", + " -1.7071296177306117,\n", + " 1.2988970915423792,\n", + " 0.3759299951740722,\n", + " 1.339725136756897,\n", + " -0.04073459306825811,\n", + " ],\n", + " resource_id=None,\n", + " ).model_dump(mode=\"json\"),\n", + " \"source\": LocationArgument(\n", + " location=[\n", + " 0.038962624967098236,\n", + " -0.8854280275157471,\n", + " 1.4724286238299769,\n", + " -1.3146923047355195,\n", + " 0.08941895514726639,\n", + " 0.7345041632652283,\n", + " ],\n", + " resource_id=None,\n", + " ).model_dump(mode=\"json\"),\n", + " \"target\": LocationArgument(\n", + " location=[\n", + " -0.07117921510805303,\n", + " -0.8959981960109253,\n", + " 1.3046754042254847,\n", + " -0.4284971517375489,\n", + " 1.5190792083740234,\n", + " -0.0406416098224085,\n", + " ],\n", + " resource_id=None,\n", + " ).model_dump(mode=\"json\"),\n", + " },\n", + " )\n", + " client.send_action(action_request=request)\n", + " request = ActionRequest(\n", + " action_name=\"gripper_transfer\",\n", + " args={\n", + " \"home\": LocationArgument(\n", + " location=[\n", + " 0.066341333091259,\n", + " -1.7071296177306117,\n", + " 1.2988970915423792,\n", + " 0.3759299951740722,\n", + " 1.339725136756897,\n", + " -0.04073459306825811,\n", + " ],\n", + " resource_id=None,\n", + " ).model_dump(mode=\"json\"),\n", + " \"target\": LocationArgument(\n", + " location=[\n", + " 0.038962624967098236,\n", + " -0.8854280275157471,\n", + " 1.4724286238299769,\n", + " -1.3146923047355195,\n", + " 0.08941895514726639,\n", + " 0.7345041632652283,\n", + " ],\n", + " resource_id=None,\n", + " ).model_dump(mode=\"json\"),\n", + " \"source\": LocationArgument(\n", + " location=[\n", + " -0.07117921510805303,\n", + " -0.8959981960109253,\n", + " 1.3046754042254847,\n", + " -0.4284971517375489,\n", + " 1.5190792083740234,\n", + " -0.0406416098224085,\n", + " ],\n", + " resource_id=None,\n", + " ).model_dump(mode=\"json\"),\n", + " },\n", + " )\n", + " client.send_action(action_request=request)" ] }, { @@ -135,7 +182,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tests/ur_gripper_test.py b/tests/ur_gripper_test.py new file mode 100644 index 0000000..c54fd54 --- /dev/null +++ b/tests/ur_gripper_test.py @@ -0,0 +1,22 @@ +# flake8: noqa +from ur_interface.ur import UR + +ur = UR("146.139.45.23") +ur.gripper_pick( + home=[ + 0.019657766446471214, + -1.8048960171141566, + 2.3868096510516565, + -0.4845213455012818, + 0.031074078753590584, + -0.06278688112367803, + ], + source=[ + -0.5000864916687858, + -0.42417523302695737, + -0.03762425045093494, + 1.5699970369382383, + 0.0002244509773422574, + 0.001363621516252746, + ], +)