From 935097cd6006c13f319d188f2836a2f75ea846f7 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 19 Dec 2025 14:40:30 -0600 Subject: [PATCH 1/9] added source and target approach height offsets to remove and replace lid functions --- src/pf400_interface/pf400.py | 13 +++++++++---- src/pf400_rest_node.py | 20 ++++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/pf400_interface/pf400.py b/src/pf400_interface/pf400.py index 3e2e4ec..1cdf989 100644 --- a/src/pf400_interface/pf400.py +++ b/src/pf400_interface/pf400.py @@ -724,9 +724,11 @@ def remove_lid( source_plate_rotation: str = "", target_plate_rotation: str = "", grab_offset: Optional[float] = None, - approach_height_offset: Optional[float] = None, + source_approach_height_offset: Optional[float] = None, + target_approach_height_offset: Optional[float] = None, ) -> None: """Remove the lid from the plate""" + source.representation = copy.deepcopy(source.representation) source.representation[0] += lid_height @@ -738,7 +740,8 @@ def remove_lid( source_plate_rotation=source_plate_rotation, target_plate_rotation=target_plate_rotation, grab_offset=grab_offset, - approach_height_offset=approach_height_offset, + source_approach_height_offset=source_approach_height_offset, + target_approach_height_offset=target_approach_height_offset, ) def replace_lid( @@ -751,7 +754,8 @@ def replace_lid( source_plate_rotation: str = "", target_plate_rotation: str = "", grab_offset: Optional[float] = None, - approach_height_offset: Optional[float] = None, + source_approach_height_offset: Optional[float] = None, + target_approach_height_offset: Optional[float] = None, ) -> None: """Replace the lid on the plate""" target.representation = copy.deepcopy(target.representation) @@ -765,7 +769,8 @@ def replace_lid( source_plate_rotation=source_plate_rotation, target_plate_rotation=target_plate_rotation, grab_offset=grab_offset, - approach_height_offset=approach_height_offset, + source_approach_height_offset=source_approach_height_offset, + target_approach_height_offset=target_approach_height_offset, ) def rotate_plate_on_deck( diff --git a/src/pf400_rest_node.py b/src/pf400_rest_node.py index 30d3487..9ef05c9 100644 --- a/src/pf400_rest_node.py +++ b/src/pf400_rest_node.py @@ -362,8 +362,11 @@ def remove_lid( ] = "", lid_height: Annotated[float, "height of the lid, in steps"] = 7.0, grab_offset: Optional[Annotated[float, "Add grab height offset"]] = None, - approach_height_offset: Optional[ - Annotated[float, "Add approach height offset"] + source_approach_height_offset: Optional[ + Annotated[float, "Add source approach height offset"] + ] = None, + target_approach_height_offset: Optional[ + Annotated[float, "Add target approach height offset"] ] = None, ) -> Optional[ActionFailed]: """Remove a lid from a plate located at location .""" @@ -412,7 +415,8 @@ def remove_lid( source_plate_rotation=source_plate_rotation, target_plate_rotation=target_plate_rotation, grab_offset=grab_offset, - approach_height_offset=approach_height_offset, + source_approach_height_offset=source_approach_height_offset, + target_approach_height_offset=target_approach_height_offset, ) return None @@ -435,8 +439,11 @@ def replace_lid( ] = "", lid_height: Annotated[float, "height of the lid, in steps"] = 7.0, grab_offset: Optional[Annotated[float, "Add grab height offset"]] = None, - approach_height_offset: Optional[ - Annotated[float, "Add approach height offset"] + source_approach_height_offset: Optional[ + Annotated[float, "Add source approach height offset"] + ] = None, + target_approach_height_offset: Optional[ + Annotated[float, "Add target approach height offset"] ] = None, ) -> Optional[ActionFailed]: """A doc string, but not the actual description of the action.""" @@ -471,7 +478,8 @@ def replace_lid( source_plate_rotation=source_plate_rotation, target_plate_rotation=target_plate_rotation, grab_offset=grab_offset, - approach_height_offset=approach_height_offset, + source_approach_height_offset=source_approach_height_offset, + target_approach_height_offset=target_approach_height_offset, ) self.resource_client.remove_resource(lid_resource.resource_id) From 86a0565ea086b86779d2e4ef66b64ce1b2f0924c Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 19 Dec 2025 16:59:45 -0600 Subject: [PATCH 2/9] upgrade to MADSci 0.6.0 --- pdm.lock | 111 ++++++++++++++++++++++++++++++++++++++++++++----- pyproject.toml | 6 +-- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/pdm.lock b/pdm.lock index b8071e7..2cf6e17 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:86150d5573b91103df487cf5fd51723bc45344317025162d81e0f2c3923f363a" +content_hash = "sha256:b1904b7b934161ffac43c3c113e8fc21b88c652a8dd757ad503b279fb0700718" [[metadata.targets]] requires_python = ">=3.9.1,<3.13" @@ -548,6 +548,21 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "httpcore" +version = "1.0.9" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["default"] +dependencies = [ + "certifi", + "h11>=0.16", +] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + [[package]] name = "httptools" version = "0.6.4" @@ -593,6 +608,23 @@ files = [ {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, ] +[[package]] +name = "httpx" +version = "0.28.1" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", +] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + [[package]] name = "identify" version = "2.6.15" @@ -746,9 +778,9 @@ files = [ [[package]] name = "madsci-client" -version = "0.5.0" +version = "0.6.0" 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", @@ -757,13 +789,13 @@ dependencies = [ "trogon>=0.6.0", ] files = [ - {file = "madsci_client-0.5.0-py3-none-any.whl", hash = "sha256:350f63132a8a64a0c238e0b9d946b4a65cf03e744c2971d4fb3ff4e58d9f36fb"}, - {file = "madsci_client-0.5.0.tar.gz", hash = "sha256:1f7eb89ff83d20042f0d6b5d22a0a893cc535a3948c2e7bf487bbea259d3b946"}, + {file = "madsci_client-0.6.0-py3-none-any.whl", hash = "sha256:2ad18496f624eac047549d93d030aa37cf5c6f1527d1c2738599c3d0f22d7972"}, + {file = "madsci_client-0.6.0.tar.gz", hash = "sha256:a0e232b60a5daf3b90b0adc722a035b759b5739006a533c8ca6d7a6e9f230f1b"}, ] [[package]] name = "madsci-common" -version = "0.5.0" +version = "0.6.0" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Common Definitions and Utilities." groups = ["default"] @@ -771,8 +803,11 @@ dependencies = [ "PyYAML>=6.0.2", "aenum>=3.1.15", "classy-fastapi>=0.7.0", + "click>=8.1.0", "fastapi>=0.115.4", + "httpx>=0.28.1", "multiprocess>=0.70.17", + "psycopg2-binary>=2.9.0", "pydantic-extra-types>=2.10.2", "pydantic-settings-export>=1.0.2", "pydantic-settings>=2.9.1", @@ -788,13 +823,13 @@ dependencies = [ "uvicorn[standard]>=0.32.0", ] files = [ - {file = "madsci_common-0.5.0-py3-none-any.whl", hash = "sha256:69f123576a414c046cff5b238ac9b2ab5f3ae895bde69688f1bb3b56bcada3d5"}, - {file = "madsci_common-0.5.0.tar.gz", hash = "sha256:68c7a2fbd27262ea0d228c1a20c18173d68e653908ebb672bbcdd9fe019f2835"}, + {file = "madsci_common-0.6.0-py3-none-any.whl", hash = "sha256:8838e08aaa4fa8d863aa33b171189f97ebdfb72ebbd927e718112f518d542cf1"}, + {file = "madsci_common-0.6.0.tar.gz", hash = "sha256:48a7d6a11a45b085cc4057cf37cc0053baa95f5ea3a8875c5d104dbca3e4307b"}, ] [[package]] name = "madsci-node-module" -version = "0.5.0" +version = "0.6.0" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes." groups = ["default"] @@ -804,8 +839,8 @@ dependencies = [ "regex", ] files = [ - {file = "madsci_node_module-0.5.0-py3-none-any.whl", hash = "sha256:b7353d8633740bf62cfad05ad1a3bd2e9c190f997fac7fed93919361c503c36e"}, - {file = "madsci_node_module-0.5.0.tar.gz", hash = "sha256:77bdea14bf1ce01bc46fe99865cb3294f947e81e613d7b79c4097c6ed8462087"}, + {file = "madsci_node_module-0.6.0-py3-none-any.whl", hash = "sha256:33901d72dfd347b3636dcc4804ef02ce3aa2059a16bbc9c68b6757592ae0833b"}, + {file = "madsci_node_module-0.6.0.tar.gz", hash = "sha256:b081d0454bfb713420d99b2d945dbe4b4b269b241849a0b04d1e42c377e2b33e"}, ] [[package]] @@ -1041,6 +1076,60 @@ files = [ {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +requires_python = ">=3.9" +summary = "psycopg2 - Python-PostgreSQL Database Adapter" +groups = ["default"] +files = [ + {file = "psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" diff --git a/pyproject.toml b/pyproject.toml index 1a1aeba..feaa8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,10 @@ authors = [ {name = "Tobias Ginsburg", email = "tginsburg@anl.gov"}, ] dependencies = [ - "madsci-node-module~=0.5.0", + "madsci-node-module~=0.6.0", "pynput>=1.8.1", - "madsci-client~=0.5.0", - "madsci-common~=0.5.0", + "madsci-client~=0.6.0", + "madsci-common~=0.6.0", ] requires-python = ">=3.9.1,<3.13" readme = "README.md" From ca91e32e516866e3aadfc0ba585f883ed2c80b52 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 31 Mar 2026 15:14:07 -0500 Subject: [PATCH 3/9] added increased rate limit and fixed ActionFailed bugs --- src/pf400_rest_node.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/pf400_rest_node.py b/src/pf400_rest_node.py index 9ef05c9..008671e 100644 --- a/src/pf400_rest_node.py +++ b/src/pf400_rest_node.py @@ -22,6 +22,8 @@ class PF400NodeConfig(RestNodeConfig): """Port to connect to the PF400 robot, default is 10100""" pf400_status_port: int = 10000 """Port to connect to the PF400 status server, default is 10000""" + rate_limit_requests: int = 500 + """Rate limit for requests to the PF400 robot, default is 100 ms""" class PF400Node(RestNode): @@ -339,7 +341,9 @@ def place_plate( approach_height_offset=approach_height_offset, ) if not place_result: - return ActionFailed("Transfer failed: plate not released properly.") + return ActionFailed( + errors=["Transfer failed: plate not released properly."] + ) return None @@ -375,13 +379,17 @@ def remove_lid( source_resource = self.resource_client.get_resource(source.resource_id) if source_resource.quantity == 0: return ActionFailed( - "Resource manager: Plate does not exist at source! Resource_id:{source.resource_id}." + errors=[ + "Resource manager: Plate does not exist at source! Resource_id:{source.resource_id}." + ] ) if target.resource_id: target_resource = self.resource_client.get_resource(target.resource_id) if target_resource.quantity != 0: return ActionFailed( - "Resource manager: Target is occupied by another plate! Resource_id:{target.resource_id}." + errors=[ + "Resource manager: Target is occupied by another plate! Resource_id:{target.resource_id}." + ] ) # Extract id of plate resource at source @@ -452,13 +460,17 @@ def replace_lid( source_resource = self.resource_client.get_resource(source.resource_id) if source_resource.quantity == 0: return ActionFailed( - "Resource manager: Lid does not exist at source! Resource_id:{source.resource_id}." + errors=[ + "Resource manager: Lid does not exist at source! Resource_id:{source.resource_id}." + ] ) if target.resource_id: target_resource = self.resource_client.get_resource(target.resource_id) if target_resource.quantity == 0: return ActionFailed( - "Resource manager: No plate on target! Resource_id:{target.resource_id}." + errors=[ + "Resource manager: No plate on target! Resource_id:{target.resource_id}." + ] ) # Create temporary lid slot from template From 8dfe06a5adc80d43a12d62f0ea19fb4a4ecc7432 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 31 Mar 2026 17:33:22 -0500 Subject: [PATCH 4/9] remove and replace lid check for lid slot and lid attribute, cleanup needed --- src/pf400_interface/resource_helpers.py | 24 +++ src/pf400_rest_node.py | 216 +++++++++++++++++------- 2 files changed, 183 insertions(+), 57 deletions(-) create mode 100644 src/pf400_interface/resource_helpers.py diff --git a/src/pf400_interface/resource_helpers.py b/src/pf400_interface/resource_helpers.py new file mode 100644 index 0000000..0ed3fa8 --- /dev/null +++ b/src/pf400_interface/resource_helpers.py @@ -0,0 +1,24 @@ +"""Helpers for resource management in the PF400 REST node.""" + +from typing import Optional + +from madsci.common.types.action_types import ActionFailed +from madsci.common.types.resource_types import Resource +from pydantic import BaseModel + + +class LidResult(BaseModel): + """Pydantic model for defining the response from pf400 REST node _get_lid_from_slot function.""" + + lid: Optional[Resource] = None + conforms: bool + error: Optional[ActionFailed] = None + + +class LidSlotResult(BaseModel): + """Pydantic model for defining the response from pf400 REST node _get_lid_slot_from_target function.""" + + lid_slot: Optional[Resource] = None + target_resource: Optional[Resource] = None + conforms: bool + error: Optional[ActionFailed] = None diff --git a/src/pf400_rest_node.py b/src/pf400_rest_node.py index 008671e..ed67246 100644 --- a/src/pf400_rest_node.py +++ b/src/pf400_rest_node.py @@ -11,6 +11,7 @@ from madsci.node_module.rest_node_module import RestNode from pf400_interface.pf400 import PF400 +from pf400_interface.resource_helpers import LidResult, LidSlotResult class PF400NodeConfig(RestNodeConfig): @@ -312,7 +313,7 @@ def place_plate( if target_resource.quantity != 0: return ActionFailed( errors=[ - "Resource manager: Target is occupied by another plate! Resource_id:{target.resource_id}." + f"Resource manager: Target is occupied by another plate! Resource_id:{target.resource_id}." ] ) @@ -380,7 +381,7 @@ def remove_lid( if source_resource.quantity == 0: return ActionFailed( errors=[ - "Resource manager: Plate does not exist at source! Resource_id:{source.resource_id}." + f"Resource manager: Plate does not exist at source! Resource_id:{source.resource_id}." ] ) if target.resource_id: @@ -388,44 +389,82 @@ def remove_lid( if target_resource.quantity != 0: return ActionFailed( errors=[ - "Resource manager: Target is occupied by another plate! Resource_id:{target.resource_id}." + f"Resource manager: Target is occupied by another plate! Resource_id:{target.resource_id}." ] ) # Extract id of plate resource at source - plate_resource_id = self.resource_client.get_resource( - source.resource_id - ).child.resource_id - - # Create temporary lid slot from template - lid_resource = self.resource_client.create_resource_from_template( - template_name="pf400_lid_slot", - resource_name="pf400_lid_slot", - add_to_database=True, - ) - - # Create lid asset from template - lid = self.resource_client.create_resource_from_template( - template_name="plate_lid", - resource_name=f"Lid_from_{plate_resource_id}", - add_to_database=True, - ) + source_resource = self.resource_client.get_resource(source.resource_id) + plate_resource = source_resource.child + plate_resource_id = plate_resource.resource_id + + # Check for plate resource format compliance + # Is there a lid slot? Is there a lid in that lid slot? + lid_resource = None + lid_slot_resource = None + if "lid_slot" in plate_resource.children: + lid_slot_child_value = plate_resource.children["lid_slot"] + if isinstance(lid_slot_child_value, Slot): + if len(lid_slot_child_value.children) == 1: + lid_slot_resource = lid_slot_child_value + lid_resource = lid_slot_child_value.child # collect lid resource + self.logger.log_info( + f"Identified lid resource {lid_resource.resource_id} for removal." + ) + else: + return ActionFailed( + errors=[ + f"No lid resource exists in the lid slot. {lid_slot_child_value}" + ] + ) + else: + return ActionFailed( + errors=[ + f"Lid slot child value is not of type Slot. {lid_slot_child_value=}" + ] + ) + else: + # The plate resource does not have a compliant format. Create a new lid from scratch + # Create temporary lid slot from template + self.logger.log_warning( + "No lid_slot exists on plate resource at source indicating a non-compliant labware resource definition was used. Creating a lid from scratch." + ) + lid_slot_resource = self.resource_client.create_resource_from_template( + template_name="pf400_lid_slot", + resource_name="pf400_lid_slot", + add_to_database=True, + ) + # Create lid asset from template + lid_resource = self.resource_client.create_resource_from_template( + template_name="plate_lid", + resource_name=f"Lid_from_{plate_resource_id}", + add_to_database=True, + ) + # put newly created lid in newly created lid slot + self.resource_client.push(resource=lid_slot_resource, child=lid_resource) + + if lid_slot_resource is not None and target_resource is not None: + source.resource_id = lid_slot_resource.resource_id # critical! + self.pf400_interface.remove_lid( + source=source, + target=target, + lid_height=lid_height, + source_approach=source_approach, + target_approach=target_approach, + source_plate_rotation=source_plate_rotation, + target_plate_rotation=target_plate_rotation, + grab_offset=grab_offset, + source_approach_height_offset=source_approach_height_offset, + target_approach_height_offset=target_approach_height_offset, + ) - lid_resource = self.resource_client.push(resource=lid_resource, child=lid) - source.resource_id = lid_resource.resource_id + else: + return ActionFailed( + errors=[ + f"lid_resource or target_resource is None. \n{lid_resource=} \n{target_resource=}" + ] + ) - self.pf400_interface.remove_lid( - source=source, - target=target, - lid_height=lid_height, - source_approach=source_approach, - target_approach=target_approach, - source_plate_rotation=source_plate_rotation, - target_plate_rotation=target_plate_rotation, - grab_offset=grab_offset, - source_approach_height_offset=source_approach_height_offset, - target_approach_height_offset=target_approach_height_offset, - ) return None @action(name="replace_lid", description="Replace a lid on a plate") @@ -456,30 +495,30 @@ def replace_lid( ) -> Optional[ActionFailed]: """A doc string, but not the actual description of the action.""" - if source.resource_id: - source_resource = self.resource_client.get_resource(source.resource_id) - if source_resource.quantity == 0: - return ActionFailed( - errors=[ - "Resource manager: Lid does not exist at source! Resource_id:{source.resource_id}." - ] - ) - if target.resource_id: - target_resource = self.resource_client.get_resource(target.resource_id) - if target_resource.quantity == 0: - return ActionFailed( - errors=[ - "Resource manager: No plate on target! Resource_id:{target.resource_id}." - ] - ) + lid_resource = None + lid_slot_resource = None - # Create temporary lid slot from template - lid_resource = self.resource_client.create_resource_from_template( - template_name="pf400_lid_slot", - resource_name="pf400_lid_slot", - add_to_database=True, + # Collect lid from source, checking for lid resource conformity to standard. + lid_resource, source_ok, err = self._get_lid_from_source(source=source) + if err: + return err + + # Collect lid slot resource from target, checking for plate resource conformity to standard. + lid_slot_resource, target_ok, target_resource, err = ( + self._get_lid_slot_from_target(target) ) - target.resource_id = lid_resource.resource_id + if err: + return err + + conforms_to_standard = source_ok and target_ok + + # Return action failed if we failed to collect any variables. + if not (lid_resource and lid_slot_resource and target_resource): + return ActionFailed( + errors=[f"{lid_slot_resource=}, {lid_resource=}, {target_resource=}"] + ) + + target.resource_id = lid_slot_resource.resource_id # critical! self.pf400_interface.replace_lid( source=source, @@ -494,10 +533,73 @@ def replace_lid( target_approach_height_offset=target_approach_height_offset, ) - self.resource_client.remove_resource(lid_resource.resource_id) + if not conforms_to_standard: + self.resource_client.remove_resource(lid_resource.resource_id) return None + def _get_lid_from_source(self, source: LocationArgument) -> LidResult: + """Retreives the lid from the source location. Called from replace_lid.""" + + if not source.resource_id: + return None, True, None + source_resource = self.resource_client.get_resource(source.resource_id) + if not source_resource.children: + return ( + None, + True, + ActionFailed(errors=["No lid resource exists at source location."]), + ) + child = source_resource.children[-1] + if "lid" not in child.attributes: + self.logger.log_warning( + f"Resource Manager: Lid resource found does not conform to standard. {child}" + ) + return child, False, None + if not child.attributes["lid"]: + return ( + None, + True, + ActionFailed(errors=['Resource Manager: "lid" attribute is False.']), + ) + return LidResult(child, True, None) + + def _get_lid_slot_from_target(self, target: LocationArgument) -> LidSlotResult: + """Retreives the lid slot from the plate on the target location. Called from replace_lid.""" + + if not target.resource_id: + return None, True, None, None + target_resource = self.resource_client.get_resource(target.resource_id) + if not target_resource.children: + return ( + None, + True, + target_resource, + ActionFailed( + errors=[ + f"No plate resource exists at the target location {target.name}." + ] + ), + ) + plate = target_resource.children[-1] + if "lid_slot" not in plate.children: + self.logger.log_warning("Target plate has no lid slot. Creating one.") + lid_slot = self.resource_client.create_resource_from_template( + template_name="pf400_lid_slot", + resource_name="pf400_lid_slot", + add_to_database=True, + ) + return lid_slot, False, target_resource, None + lid_slot = plate.children["lid_slot"] + if lid_slot.children: + return ( + None, + True, + target_resource, + ActionFailed(errors=["A lid already exists on the target plate."]), + ) + return LidSlotResult(lid_slot, True, target_resource, None) + def pause(self) -> None: """Pause the node.""" self.logger.log("Pausing node...") From 128fb016ff2e8ce20e6ae6581f0f961d14c20b78 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Thu, 2 Apr 2026 17:41:56 -0500 Subject: [PATCH 5/9] fixed replace_lid bug --- src/pf400_interface/resource_helpers.py | 4 +- src/pf400_rest_node.py | 73 +++++++++++++++---------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/pf400_interface/resource_helpers.py b/src/pf400_interface/resource_helpers.py index 0ed3fa8..27bade1 100644 --- a/src/pf400_interface/resource_helpers.py +++ b/src/pf400_interface/resource_helpers.py @@ -3,7 +3,7 @@ from typing import Optional from madsci.common.types.action_types import ActionFailed -from madsci.common.types.resource_types import Resource +from madsci.common.types.resource_types import Resource, Slot from pydantic import BaseModel @@ -19,6 +19,6 @@ class LidSlotResult(BaseModel): """Pydantic model for defining the response from pf400 REST node _get_lid_slot_from_target function.""" lid_slot: Optional[Resource] = None - target_resource: Optional[Resource] = None + target_resource: Optional[Slot] = None conforms: bool error: Optional[ActionFailed] = None diff --git a/src/pf400_rest_node.py b/src/pf400_rest_node.py index ed67246..6bd1f40 100644 --- a/src/pf400_rest_node.py +++ b/src/pf400_rest_node.py @@ -499,16 +499,19 @@ def replace_lid( lid_slot_resource = None # Collect lid from source, checking for lid resource conformity to standard. - lid_resource, source_ok, err = self._get_lid_from_source(source=source) - if err: - return err + source_result = self._get_lid_from_source(source=source) + if source_result.error is not None: + return source_result.error + lid_resource = source_result.lid + source_ok = source_result.conforms # Collect lid slot resource from target, checking for plate resource conformity to standard. - lid_slot_resource, target_ok, target_resource, err = ( - self._get_lid_slot_from_target(target) - ) - if err: - return err + target_result = self._get_lid_slot_from_target(target) + if target_result.error is not None: + return target_result.error + lid_slot_resource = target_result.lid_slot + target_ok = target_result.conforms + target_resource = target_result.target_resource conforms_to_standard = source_ok and target_ok @@ -545,24 +548,29 @@ def _get_lid_from_source(self, source: LocationArgument) -> LidResult: return None, True, None source_resource = self.resource_client.get_resource(source.resource_id) if not source_resource.children: - return ( - None, - True, - ActionFailed(errors=["No lid resource exists at source location."]), + return LidResult( + lid=None, + conforms=True, + error=ActionFailed( + errors=["No lid resource exists at source location."] + ), ) child = source_resource.children[-1] if "lid" not in child.attributes: self.logger.log_warning( f"Resource Manager: Lid resource found does not conform to standard. {child}" ) - return child, False, None + return LidResult(lid=child, conforms=False, error=None) + if not child.attributes["lid"]: - return ( - None, - True, - ActionFailed(errors=['Resource Manager: "lid" attribute is False.']), + return LidResult( + lid=None, + conforms=True, + error=ActionFailed( + errors=['Resource Manager: "lid" attribute is False.'] + ), ) - return LidResult(child, True, None) + return LidResult(lid=child, conforms=True, error=None) def _get_lid_slot_from_target(self, target: LocationArgument) -> LidSlotResult: """Retreives the lid slot from the plate on the target location. Called from replace_lid.""" @@ -571,11 +579,11 @@ def _get_lid_slot_from_target(self, target: LocationArgument) -> LidSlotResult: return None, True, None, None target_resource = self.resource_client.get_resource(target.resource_id) if not target_resource.children: - return ( - None, - True, - target_resource, - ActionFailed( + return LidSlotResult( + lid_slot=None, + conforms=True, + target_resource=target_resource, + error=ActionFailed( errors=[ f"No plate resource exists at the target location {target.name}." ] @@ -592,13 +600,20 @@ def _get_lid_slot_from_target(self, target: LocationArgument) -> LidSlotResult: return lid_slot, False, target_resource, None lid_slot = plate.children["lid_slot"] if lid_slot.children: - return ( - None, - True, - target_resource, - ActionFailed(errors=["A lid already exists on the target plate."]), + return LidSlotResult( + lid_slot=None, + conforms=True, + target_resource=target_resource, + error=ActionFailed( + errors=["A lid already exists on the target plate."] + ), ) - return LidSlotResult(lid_slot, True, target_resource, None) + return LidSlotResult( + lid_slot=lid_slot, + conforms=True, + target_resource=target_resource, + error=None, + ) def pause(self) -> None: """Pause the node.""" From 8d55c5633dd0069f4e25fca107c31e6f8456c7ec Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Wed, 27 May 2026 15:57:19 -0500 Subject: [PATCH 6/9] Update to MADSci v0.8 Lifts the module from v0.6 to v0.8 for the RAPID 446 grimm rebuild. - pyproject.toml: pin madsci-{node-module,client,common}~=0.8.0; bump requires-python to >=3.10,<3.13. - src/pf400_rest_node.py: rename self.node_definition.{node_id,node_name} -> self.node_info.* at 4 sites in startup_handler() (3x created_by= for gripper / lid_slot / plate_lid templates, 1x f"{...node_name}.gripper" resource name). - Dockerfile rewritten on the v0.8 pattern: - Pin base to ghcr.io/ad-sdl/madsci:v0.8.0. - `uv pip install --python ${MADSCI_VENV}/bin/python -e ./pf400_module` so the editable install lands in the venv the runtime actually uses. - Fix broken CMD: scripts/pf400_rest_node.py -> src/pf400_rest_node.py (scripts/ never existed). - No dialout groups -- pf400 is network-only. Verified end-to-end on grimm with pf400_piper: /status -> ready: true /state -> pf400_status_code: READY, live joint angles [861.943, -1.451, 172.64, 175.601, 126.43, -644.028] --- Dockerfile | 6 +++--- pyproject.toml | 8 ++++---- src/pf400_rest_node.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index be3a32b..b359f7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/ad-sdl/madsci:latest +FROM ghcr.io/ad-sdl/madsci:v0.8.0 LABEL org.opencontainers.image.source=https://github.com/AD-SDL/pf400_module LABEL org.opencontainers.image.description="Drivers and REST API's for the PF400 plate handler robots" @@ -15,8 +15,8 @@ COPY ./README.md pf400_module/README.md COPY ./pyproject.toml pf400_module/pyproject.toml RUN --mount=type=cache,target=/root/.cache \ - pip install -e ./pf400_module + uv pip install --python ${MADSCI_VENV}/bin/python -e ./pf400_module -CMD ["python", "pf400_module/scripts/pf400_rest_node.py"] +CMD ["python", "pf400_module/src/pf400_rest_node.py"] ######################################### diff --git a/pyproject.toml b/pyproject.toml index feaa8ce..25103ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,12 @@ authors = [ {name = "Tobias Ginsburg", email = "tginsburg@anl.gov"}, ] dependencies = [ - "madsci-node-module~=0.6.0", + "madsci-node-module~=0.8.0", "pynput>=1.8.1", - "madsci-client~=0.6.0", - "madsci-common~=0.6.0", + "madsci-client~=0.8.0", + "madsci-common~=0.8.0", ] -requires-python = ">=3.9.1,<3.13" +requires-python = ">=3.10,<3.13" readme = "README.md" license = {text = "MIT"} diff --git a/src/pf400_rest_node.py b/src/pf400_rest_node.py index 6bd1f40..d64968d 100644 --- a/src/pf400_rest_node.py +++ b/src/pf400_rest_node.py @@ -57,13 +57,13 @@ def startup_handler(self) -> None: description="Template for PF400 robot gripper slot. Used to track what the robot is currently holding.", required_overrides=["resource_name"], tags=["pf400", "gripper", "slot"], - created_by=self.node_definition.node_id, + created_by=self.node_info.node_id, version="1.0.0", ) self.gripper_resource = self.resource_client.create_resource_from_template( template_name="pf400_gripper", - resource_name=f"{self.node_definition.node_name}.gripper", + resource_name=f"{self.node_info.node_name}.gripper", add_to_database=True, ) self.logger.log_info( @@ -87,7 +87,7 @@ def startup_handler(self) -> None: description="Template for temporary lid storage slot. Used when removing/replacing lids from plates.", required_overrides=["resource_name"], tags=["pf400", "lid", "slot", "temporary"], - created_by=self.node_definition.node_id, + created_by=self.node_info.node_id, version="1.0.0", ) @@ -109,7 +109,7 @@ def startup_handler(self) -> None: description="Template for plate lids. Used to track lids during lid operations.", required_overrides=["resource_name"], tags=["lid", "plate", "asset"], - created_by=self.node_definition.node_id, + created_by=self.node_info.node_id, version="1.0.0", ) From 515c3119dfacc23ad9506a61c315b02fc8e39f96 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Wed, 27 May 2026 16:07:46 -0500 Subject: [PATCH 7/9] chore: update pre-commit hooks and apply auto-fixes Co-Authored-By: Claude Opus 4.7 --- .pre-commit-config.yaml | 4 ++-- src/pf400_rest_node.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e2d661..3faa11a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,12 +12,12 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/kynan/nbstripout - rev: 0.8.1 + rev: 0.9.1 hooks: - id: nbstripout - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.5 + rev: v0.15.14 hooks: # Run the linter. - id: ruff diff --git a/src/pf400_rest_node.py b/src/pf400_rest_node.py index d64968d..9eaa86b 100644 --- a/src/pf400_rest_node.py +++ b/src/pf400_rest_node.py @@ -216,11 +216,11 @@ def transfer( transfer_result = self.pf400_interface.transfer( source=source, target=target, - source_approach=source_approach if source_approach else None, - target_approach=target_approach if target_approach else None, + source_approach=source_approach or None, + target_approach=target_approach or None, source_plate_rotation=source_plate_rotation, target_plate_rotation=target_plate_rotation, - rotation_deck=rotation_deck if rotation_deck else None, + rotation_deck=rotation_deck or None, grab_offset=grab_offset, source_approach_height_offset=source_approach_height_offset, target_approach_height_offset=target_approach_height_offset, @@ -278,7 +278,7 @@ def pick_plate( pick_result = self.pf400_interface.pick_plate( source=source, - source_approach=source_approach if source_approach else None, + source_approach=source_approach or None, grab_offset=grab_offset, approach_height_offset=approach_height_offset, ) @@ -337,7 +337,7 @@ def place_plate( place_result = self.pf400_interface.place_plate( target=target, - target_approach=target_approach if target_approach else None, + target_approach=target_approach or None, grab_offset=grab_offset, approach_height_offset=approach_height_offset, ) From c39bdbd596ba8865d2c7edab8429ed8837c05ec3 Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Fri, 29 May 2026 10:21:31 -0500 Subject: [PATCH 8/9] Remove unused resource_helpers module LidResult and LidSlotResult were only used by the v0.8 branch's lid-handling code, which was superseded by main's has_lid-attribute approach during the merge. --- src/pf400_interface/resource_helpers.py | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 src/pf400_interface/resource_helpers.py diff --git a/src/pf400_interface/resource_helpers.py b/src/pf400_interface/resource_helpers.py deleted file mode 100644 index 27bade1..0000000 --- a/src/pf400_interface/resource_helpers.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Helpers for resource management in the PF400 REST node.""" - -from typing import Optional - -from madsci.common.types.action_types import ActionFailed -from madsci.common.types.resource_types import Resource, Slot -from pydantic import BaseModel - - -class LidResult(BaseModel): - """Pydantic model for defining the response from pf400 REST node _get_lid_from_slot function.""" - - lid: Optional[Resource] = None - conforms: bool - error: Optional[ActionFailed] = None - - -class LidSlotResult(BaseModel): - """Pydantic model for defining the response from pf400 REST node _get_lid_slot_from_target function.""" - - lid_slot: Optional[Resource] = None - target_resource: Optional[Slot] = None - conforms: bool - error: Optional[ActionFailed] = None From 02be87e7bb754bd5309bf512b6b5163b05bf292b Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Fri, 29 May 2026 10:59:23 -0500 Subject: [PATCH 9/9] Run settings export --- .env.example | 2 +- docs/Configuration.md | 46 +++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index e1d402c..3ec73f0 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ # NODE_URL="http://127.0.0.1:2000/" # NODE_UVICORN_KWARGS={"limit_concurrency":10} # NODE_ENABLE_RATE_LIMITING=true -# NODE_RATE_LIMIT_REQUESTS=100 +# NODE_RATE_LIMIT_REQUESTS=500 # NODE_RATE_LIMIT_WINDOW=60 # NODE_RATE_LIMIT_SHORT_REQUESTS=50 # NODE_RATE_LIMIT_SHORT_WINDOW=1 diff --git a/docs/Configuration.md b/docs/Configuration.md index 0ae8b32..1b1377c 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -8,26 +8,26 @@ Configuration for the pf400 node module. **Environment Prefix**: `NODE_` -| Name | Type | Default | Description | Example | -|------------------------------------|--------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| -| `NODE_STATUS_UPDATE_INTERVAL` | `number` \| `NoneType` | `2.0` | The interval in seconds at which the node should update its status. | `2.0` | -| `NODE_STATE_UPDATE_INTERVAL` | `number` \| `NoneType` | `2.0` | The interval in seconds at which the node should update its state. | `2.0` | -| `NODE_NAME` \| `NODE_NAME` | `string` \| `NoneType` | `null` | Name for this node. If not set, defaults to the class name. | `null` | -| `NODE_ID` \| `NODE_ID` | `string` \| `NoneType` | `null` | Unique ID for this node. If not set, a new ULID is generated. | `null` | -| `NODE_TYPE` \| `NODE_TYPE` | `NodeType` \| `NoneType` | `null` | The type of thing this node provides an interface for. | `null` | -| `NODE_MODULE_NAME` | `string` \| `NoneType` | `null` | Name of the node module implementation. | `null` | -| `NODE_MODULE_VERSION` | `string` \| `NoneType` | `null` | Version of the node module implementation. | `null` | -| `NODE_ENABLE_REGISTRY_RESOLUTION` | `boolean` | `true` | When true, resolve node_id from the ID Registry at startup for stable identity across restarts. | `true` | -| `NODE_LAB_URL` | `AnyUrl` \| `NoneType` | `null` | Lab Manager URL for distributed registry coordination. | `null` | -| `NODE_REGISTRY_LOCK_TIMEOUT` | `number` | `60.0` | Seconds to retry registry lock acquisition on contention at startup. Should be at least 2x the lock TTL (30s) to survive ungraceful container restarts. | `60.0` | -| `NODE_URL` \| `NODE_URL` | `AnyUrl` | `"http://127.0.0.1:2000/"` | The URL used to communicate with the node. This is the base URL for the REST API. | `"http://127.0.0.1:2000/"` | -| `NODE_UVICORN_KWARGS` | `object` | `{"limit_concurrency":10}` | Configuration for the Uvicorn server that runs the REST API. By default, sets limit_concurrency=10 to protect against connection exhaustion attacks. | `{"limit_concurrency":10}` | -| `NODE_ENABLE_RATE_LIMITING` | `boolean` | `true` | Enable rate limiting middleware for the REST API. | `true` | -| `NODE_RATE_LIMIT_REQUESTS` | `integer` | `100` | Maximum number of requests allowed per long time window (only used if enable_rate_limiting is True). | `100` | -| `NODE_RATE_LIMIT_WINDOW` | `integer` | `60` | Long time window in seconds for rate limiting (only used if enable_rate_limiting is True). | `60` | -| `NODE_RATE_LIMIT_SHORT_REQUESTS` | `integer` \| `NoneType` | `50` | Maximum number of requests allowed per short time window for burst protection (only used if enable_rate_limiting is True). If None, short window limiting is disabled. | `50` | -| `NODE_RATE_LIMIT_SHORT_WINDOW` | `integer` \| `NoneType` | `1` | Short time window for burst protection in seconds (only used if enable_rate_limiting is True). If None, short window limiting is disabled. | `1` | -| `NODE_RATE_LIMIT_CLEANUP_INTERVAL` | `integer` | `300` | Interval in seconds between cleanup operations to prevent memory leaks (only used if enable_rate_limiting is True). | `300` | -| `NODE_PF400_IP` | `string` \| `NoneType` | `null` | | `null` | -| `NODE_PF400_PORT` | `integer` | `10100` | | `10100` | -| `NODE_PF400_STATUS_PORT` | `integer` | `10000` | | `10000` | +| Name | Type | Default | Description | Example | +|------------------------------------|----------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| `NODE_STATUS_UPDATE_INTERVAL` | `number` \| `null` | `2.0` | The interval in seconds at which the node should update its status. | `2.0` | +| `NODE_STATE_UPDATE_INTERVAL` | `number` \| `null` | `2.0` | The interval in seconds at which the node should update its state. | `2.0` | +| `NODE_NAME` | `string` \| `null` | `null` | Name for this node. If not set, defaults to the class name. | `null` | +| `NODE_ID` | `string` \| `null` | `null` | Unique ID for this node. If not set, a new ULID is generated. | `null` | +| `NODE_TYPE` | `NodeType` \| `null` | `null` | The type of thing this node provides an interface for. | `null` | +| `NODE_MODULE_NAME` | `string` \| `null` | `null` | Name of the node module implementation. | `null` | +| `NODE_MODULE_VERSION` | `string` \| `null` | `null` | Version of the node module implementation. | `null` | +| `NODE_ENABLE_REGISTRY_RESOLUTION` | `boolean` | `true` | When true, resolve node_id from the ID Registry at startup for stable identity across restarts. | `true` | +| `NODE_LAB_URL` | `AnyUrl` \| `null` | `null` | Lab Manager URL for distributed registry coordination. | `null` | +| `NODE_REGISTRY_LOCK_TIMEOUT` | `number` | `60.0` | Seconds to retry registry lock acquisition on contention at startup. Should be at least 2x the lock TTL (30s) to survive ungraceful container restarts. | `60.0` | +| `NODE_URL` | `AnyUrl` | `"http://127.0.0.1:2000/"` | The URL used to communicate with the node. This is the base URL for the REST API. | `"http://127.0.0.1:2000/"` | +| `NODE_UVICORN_KWARGS` | `object` | `{"limit_concurrency":10}` | Configuration for the Uvicorn server that runs the REST API. By default, sets limit_concurrency=10 to protect against connection exhaustion attacks. | `{"limit_concurrency":10}` | +| `NODE_ENABLE_RATE_LIMITING` | `boolean` | `true` | Enable rate limiting middleware for the REST API. | `true` | +| `NODE_RATE_LIMIT_REQUESTS` | `integer` | `500` | | `500` | +| `NODE_RATE_LIMIT_WINDOW` | `integer` | `60` | Long time window in seconds for rate limiting (only used if enable_rate_limiting is True). | `60` | +| `NODE_RATE_LIMIT_SHORT_REQUESTS` | `integer` \| `null` | `50` | Maximum number of requests allowed per short time window for burst protection (only used if enable_rate_limiting is True). If None, short window limiting is disabled. | `50` | +| `NODE_RATE_LIMIT_SHORT_WINDOW` | `integer` \| `null` | `1` | Short time window for burst protection in seconds (only used if enable_rate_limiting is True). If None, short window limiting is disabled. | `1` | +| `NODE_RATE_LIMIT_CLEANUP_INTERVAL` | `integer` | `300` | Interval in seconds between cleanup operations to prevent memory leaks (only used if enable_rate_limiting is True). | `300` | +| `NODE_PF400_IP` | `string` \| `null` | `null` | | `null` | +| `NODE_PF400_PORT` | `integer` | `10100` | | `10100` | +| `NODE_PF400_STATUS_PORT` | `integer` | `10000` | | `10000` |