From 9215d9a7a500251e565bf006a0d22d3fc6b5ca0b Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Tue, 9 Jul 2024 09:52:18 +0200 Subject: [PATCH 01/35] Add boilerplate code for a web server This will mimic the real gripper's web server. --- .gitignore | 3 ++ schunk_egu_egk_dummy/README.md | 28 ++++++++++++++++++ schunk_egu_egk_dummy/main.py | 37 ++++++++++++++++++++++++ schunk_egu_egk_dummy/src/__init__.py | 0 schunk_egu_egk_dummy/src/dummy.py | 29 +++++++++++++++++++ schunk_egu_egk_dummy/tests/__init__.py | 0 schunk_egu_egk_dummy/tests/test_dummy.py | 27 +++++++++++++++++ 7 files changed, 124 insertions(+) create mode 100644 schunk_egu_egk_dummy/README.md create mode 100644 schunk_egu_egk_dummy/main.py create mode 100644 schunk_egu_egk_dummy/src/__init__.py create mode 100644 schunk_egu_egk_dummy/src/dummy.py create mode 100644 schunk_egu_egk_dummy/tests/__init__.py create mode 100644 schunk_egu_egk_dummy/tests/test_dummy.py diff --git a/.gitignore b/.gitignore index a9d2efb..aff6c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /install/ /log/ /.vscode + +# Ignore test coverage artifacts +*.coverage diff --git a/schunk_egu_egk_dummy/README.md b/schunk_egu_egk_dummy/README.md new file mode 100644 index 0000000..1405add --- /dev/null +++ b/schunk_egu_egk_dummy/README.md @@ -0,0 +1,28 @@ +# Schunk EGU/EGK Dummy +A minimalist protocol simulator for system tests. + +## Install +Use a virtual environment and +```bash +pip install fastapi uvicorn +``` + +## Getting started +1. In a new terminal, start the dummy + ```bash + uvicorn main:server --port 8000 --reload + ``` + +## Run tests locally + +Inside a virtual environment (.venv) + +```bash +pip install pytest coverage +``` + +And then inside this repository +```bash +coverage run -m pytest tests/ +coverage report +``` diff --git a/schunk_egu_egk_dummy/main.py b/schunk_egu_egk_dummy/main.py new file mode 100644 index 0000000..f9e6839 --- /dev/null +++ b/schunk_egu_egk_dummy/main.py @@ -0,0 +1,37 @@ +from src.dummy import Dummy + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from typing import Optional +from pydantic import BaseModel + +# Components +dummy = Dummy() +dummy.start() +server = FastAPI() +client = ["http://localhost:8001"] + + +server.add_middleware( + CORSMiddleware, + allow_origins=client, + allow_credentials=True, + allow_methods=["*"], + allow_headers=[""], +) + + +class Message(BaseModel): + message: str + optional: Optional[str] = None + + +@server.put("/") +async def put(msg: Message): + dummy.process(msg.message) + return True + + +@server.get("/") +async def get(): + return "Dummy ready" diff --git a/schunk_egu_egk_dummy/src/__init__.py b/schunk_egu_egk_dummy/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schunk_egu_egk_dummy/src/dummy.py b/schunk_egu_egk_dummy/src/dummy.py new file mode 100644 index 0000000..5e9d2b6 --- /dev/null +++ b/schunk_egu_egk_dummy/src/dummy.py @@ -0,0 +1,29 @@ +from threading import Thread +import time + + +class Dummy(object): + def __init__(self): + self.thread = Thread(target=self._run) + self.running = False + self.done = False + + def start(self) -> None: + if self.running: + return + self.thread.start() + self.running = True + + def stop(self) -> None: + self.done = True + self.thread.join() + self.running = False + + def _run(self): + while not self.done: + time.sleep(1) + print("Done") + + def process(self, msg: str) -> bool: + print(msg) + return True diff --git a/schunk_egu_egk_dummy/tests/__init__.py b/schunk_egu_egk_dummy/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schunk_egu_egk_dummy/tests/test_dummy.py b/schunk_egu_egk_dummy/tests/test_dummy.py new file mode 100644 index 0000000..8cb76f3 --- /dev/null +++ b/schunk_egu_egk_dummy/tests/test_dummy.py @@ -0,0 +1,27 @@ +from src.dummy import Dummy + + +def test_dummy_starts_a_background_thread(): + dummy = Dummy() + assert not dummy.running + dummy.start() + assert dummy.running + dummy.stop() + assert not dummy.running + + +def test_dummy_survives_repeated_starts_and_stops(): + dummy = Dummy() + for _ in range(3): + dummy.start() + assert dummy.running + + for _ in range(3): + dummy.stop() + assert not dummy.running + + +def test_dummy_processes_messages(): + dummy = Dummy() + msg = "Hello simulator!" + assert dummy.process(msg) From 95d8ab23403da4c8f06fe2092bf51042fe461f3f Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Tue, 9 Jul 2024 18:16:42 +0200 Subject: [PATCH 02/35] Add a launch parameter for the driver's TCP/IP port This makes testing easier with non-default ports in the dummy server. The default port (`80`) would require sudo privileges. --- schunk_egu_egk_gripper_driver/launch/schunk.launch.py | 8 +++++++- .../src/schunk_gripper_wrapper.cpp | 5 ++++- .../schunk_egu_egk_gripper_library/communication.hpp | 2 +- .../schunk_egu_egk_gripper_library/schunk_gripper_lib.hpp | 2 +- schunk_egu_egk_gripper_library/src/communication.cpp | 7 +++---- schunk_egu_egk_gripper_library/src/schunk_gripper_lib.cpp | 4 ++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/schunk_egu_egk_gripper_driver/launch/schunk.launch.py b/schunk_egu_egk_gripper_driver/launch/schunk.launch.py index 901d178..4c30518 100644 --- a/schunk_egu_egk_gripper_driver/launch/schunk.launch.py +++ b/schunk_egu_egk_gripper_driver/launch/schunk.launch.py @@ -27,7 +27,12 @@ def generate_launch_description(): default_value="10.49.60.86", description="IP address of the gripper on your network", ) - args = [ip] + port = DeclareLaunchArgument( + "port", + default_value="80", + description="TCP/IP port of the gripper", + ) + args = [ip, port] container = Node( name="gripper_container", @@ -47,6 +52,7 @@ def generate_launch_description(): namespace="EGK_50_M_B", parameters=[ {"IP": LaunchConfiguration("IP")}, + {"port": LaunchConfiguration("port")}, {"state_frq": 60.0}, {"rate": 10.0}, {"use_brk": False}, diff --git a/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp b/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp index 5f4f68e..44e59ee 100644 --- a/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp +++ b/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp @@ -44,7 +44,10 @@ std::map inst_param = //Initialize the ROS Driver SchunkGripperNode::SchunkGripperNode(const rclcpp::NodeOptions &options) : rclcpp::Node("schunk_gripper_driver", options), - Gripper(this->declare_parameter("IP", "0.0.0.0", parameter_descriptor("IP-Address of the gripper"))), + Gripper( + this->declare_parameter("IP", "0.0.0.0", parameter_descriptor("IP-Address of the gripper")), + this->declare_parameter("port", 80, parameter_descriptor("TCP/IP port of the gripper")) + ), limiting_rate(1000) //limiting_rate for loops { //Callback groups diff --git a/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp b/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp index 56e37ec..0fc2040 100644 --- a/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp +++ b/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp @@ -243,7 +243,7 @@ class AnybusCom void performCurlRequest(std::string post); - AnybusCom(const std::string &ip); + AnybusCom(const std::string &ip, int port); ~AnybusCom(); }; //Get something with Instance diff --git a/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/schunk_gripper_lib.hpp b/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/schunk_gripper_lib.hpp index 15e6bad..a52b69c 100644 --- a/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/schunk_gripper_lib.hpp +++ b/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/schunk_gripper_lib.hpp @@ -87,7 +87,7 @@ class Gripper : protected AnybusCom public: - Gripper(const std::string &ip); //Gripper initialisation + Gripper(const std::string &ip, int port); //Gripper initialisation ~Gripper(); }; diff --git a/schunk_egu_egk_gripper_library/src/communication.cpp b/schunk_egu_egk_gripper_library/src/communication.cpp index a65bbf4..de50abc 100644 --- a/schunk_egu_egk_gripper_library/src/communication.cpp +++ b/schunk_egu_egk_gripper_library/src/communication.cpp @@ -37,12 +37,11 @@ size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp) } //Initialize the plcs and Addresses -AnybusCom::AnybusCom(const std::string &ip) : ip(ip) +AnybusCom::AnybusCom(const std::string &ip, int port) : ip(ip) { - initAddresses(); //Init addresses for post and get - + initAddresses(); // for post and get curl = curl_easy_init(); - + curl_easy_setopt(curl, CURLOPT_PORT, port); } //Split a hexadecimal String, which represents an Array into its parts (HERE THE DATATYPE IS ALWAYS 4 Bytes) diff --git a/schunk_egu_egk_gripper_library/src/schunk_gripper_lib.cpp b/schunk_egu_egk_gripper_library/src/schunk_gripper_lib.cpp index 24ed23c..d3c4234 100644 --- a/schunk_egu_egk_gripper_library/src/schunk_gripper_lib.cpp +++ b/schunk_egu_egk_gripper_library/src/schunk_gripper_lib.cpp @@ -44,8 +44,8 @@ std::map commands_str {"SOFT RESET", SOFT_RESET} }; //Start th gripper, so it is ready to operate -Gripper::Gripper(const std::string &ip): -AnybusCom(ip), +Gripper::Gripper(const std::string &ip, int port): +AnybusCom(ip, port), post_requested(false) { try From 07b69ab36cc8fc159ea59c6871b94fa536c1151c Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Tue, 9 Jul 2024 18:58:18 +0200 Subject: [PATCH 03/35] Add boilerplate code for GET requests --- schunk_egu_egk_dummy/main.py | 16 +++++++++++++--- schunk_egu_egk_dummy/src/dummy.py | 9 +++++++++ schunk_egu_egk_dummy/tests/test_dummy.py | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/schunk_egu_egk_dummy/main.py b/schunk_egu_egk_dummy/main.py index f9e6839..4a09021 100644 --- a/schunk_egu_egk_dummy/main.py +++ b/schunk_egu_egk_dummy/main.py @@ -32,6 +32,16 @@ async def put(msg: Message): return True -@server.get("/") -async def get(): - return "Dummy ready" +@server.get("/adi/info.json") +async def get_info(): + return dummy.get_info() + + +@server.get("/adi/enum.json") +async def get_enum(): + return dummy.get_enum() + + +@server.get("/adi/data.json") +async def get_data(): + return dummy.get_data() diff --git a/schunk_egu_egk_dummy/src/dummy.py b/schunk_egu_egk_dummy/src/dummy.py index 5e9d2b6..0688b37 100644 --- a/schunk_egu_egk_dummy/src/dummy.py +++ b/schunk_egu_egk_dummy/src/dummy.py @@ -27,3 +27,12 @@ def _run(self): def process(self, msg: str) -> bool: print(msg) return True + + def get_info(self) -> dict: + return {"dataformat": 0, "numadis": 123, "webversion": 1} + + def get_enum(self) -> list: + return [] + + def get_data(self) -> list: + return [] diff --git a/schunk_egu_egk_dummy/tests/test_dummy.py b/schunk_egu_egk_dummy/tests/test_dummy.py index 8cb76f3..dcc0ced 100644 --- a/schunk_egu_egk_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_dummy/tests/test_dummy.py @@ -25,3 +25,19 @@ def test_dummy_processes_messages(): dummy = Dummy() msg = "Hello simulator!" assert dummy.process(msg) + + +def test_dummy_returns_valid_info(): + dummy = Dummy() + info = {"dataformat": 0, "numadis": 123, "webversion": 1} + assert dummy.get_info() == info + + +def test_dummy_returns_valid_enums(): + dummy = Dummy() + assert dummy.get_enum() == [] + + +def test_dummy_returns_valid_data(): + dummy = Dummy() + assert dummy.get_data() == [] From b53a09f5a538a63f432ca9827f701fd3ac833c32 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Tue, 9 Jul 2024 18:59:23 +0200 Subject: [PATCH 04/35] Rename the dummy sub package That's now more consistent with the rest. --- {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/README.md | 0 {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/main.py | 0 .../src/__init__.py | 0 .../src/dummy.py | 0 .../tests/__init__.py | 0 .../tests/test_dummy.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/README.md (100%) rename {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/main.py (100%) rename {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/src/__init__.py (100%) rename {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/src/dummy.py (100%) rename {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/tests/__init__.py (100%) rename {schunk_egu_egk_dummy => schunk_egu_egk_gripper_dummy}/tests/test_dummy.py (100%) diff --git a/schunk_egu_egk_dummy/README.md b/schunk_egu_egk_gripper_dummy/README.md similarity index 100% rename from schunk_egu_egk_dummy/README.md rename to schunk_egu_egk_gripper_dummy/README.md diff --git a/schunk_egu_egk_dummy/main.py b/schunk_egu_egk_gripper_dummy/main.py similarity index 100% rename from schunk_egu_egk_dummy/main.py rename to schunk_egu_egk_gripper_dummy/main.py diff --git a/schunk_egu_egk_dummy/src/__init__.py b/schunk_egu_egk_gripper_dummy/src/__init__.py similarity index 100% rename from schunk_egu_egk_dummy/src/__init__.py rename to schunk_egu_egk_gripper_dummy/src/__init__.py diff --git a/schunk_egu_egk_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py similarity index 100% rename from schunk_egu_egk_dummy/src/dummy.py rename to schunk_egu_egk_gripper_dummy/src/dummy.py diff --git a/schunk_egu_egk_dummy/tests/__init__.py b/schunk_egu_egk_gripper_dummy/tests/__init__.py similarity index 100% rename from schunk_egu_egk_dummy/tests/__init__.py rename to schunk_egu_egk_gripper_dummy/tests/__init__.py diff --git a/schunk_egu_egk_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py similarity index 100% rename from schunk_egu_egk_dummy/tests/test_dummy.py rename to schunk_egu_egk_gripper_dummy/tests/test_dummy.py From 70409aaa883f7acf6db658f76f45dba0c39e1093 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 10 Jul 2024 11:03:09 +0200 Subject: [PATCH 05/35] Handle all get requests via a single API call --- schunk_egu_egk_gripper_dummy/main.py | 20 ++++--------- schunk_egu_egk_gripper_dummy/src/dummy.py | 27 +++++++++--------- .../tests/test_dummy.py | 28 ++++++++++--------- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/main.py index 4a09021..393f546 100644 --- a/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/main.py @@ -1,6 +1,6 @@ from src.dummy import Dummy -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from typing import Optional from pydantic import BaseModel @@ -28,20 +28,10 @@ class Message(BaseModel): @server.put("/") async def put(msg: Message): - dummy.process(msg.message) + print(msg) return True -@server.get("/adi/info.json") -async def get_info(): - return dummy.get_info() - - -@server.get("/adi/enum.json") -async def get_enum(): - return dummy.get_enum() - - -@server.get("/adi/data.json") -async def get_data(): - return dummy.get_data() +@server.get("/adi/{path}") +async def get(request: Request): + return dummy.process_get(request.path_params["path"], request.query_params) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 0688b37..b9298bf 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -1,5 +1,6 @@ from threading import Thread import time +from urllib.parse import parse_qs class Dummy(object): @@ -19,20 +20,20 @@ def stop(self) -> None: self.thread.join() self.running = False - def _run(self): + def _run(self) -> None: while not self.done: time.sleep(1) print("Done") - def process(self, msg: str) -> bool: - print(msg) - return True - - def get_info(self) -> dict: - return {"dataformat": 0, "numadis": 123, "webversion": 1} - - def get_enum(self) -> list: - return [] - - def get_data(self) -> list: - return [] + def process_get(self, path: str, query: dict[str, list[str]]) -> dict | list | None: + print(f"path: {path}") + query = parse_qs(str(query)) + print(f"query: {query}") + + if path == "info.json": + return {} + if path == "enum.json": + return [] + if path == "data.json": + return [] + return None diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index dcc0ced..fd7808a 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -21,23 +21,25 @@ def test_dummy_survives_repeated_starts_and_stops(): assert not dummy.running -def test_dummy_processes_messages(): +def test_dummy_responds_correctly_to_info_requests(): dummy = Dummy() - msg = "Hello simulator!" - assert dummy.process(msg) + path = "info.json" + query = "" + expected = {} + assert dummy.process_get(path, query) == expected -def test_dummy_returns_valid_info(): +def test_dummy_responds_correctly_to_enum_requests(): dummy = Dummy() - info = {"dataformat": 0, "numadis": 123, "webversion": 1} - assert dummy.get_info() == info + path = "enum.json" + query = "" + expected = [] + assert dummy.process_get(path, query) == expected -def test_dummy_returns_valid_enums(): +def test_dummy_responds_correctly_to_data_requests(): dummy = Dummy() - assert dummy.get_enum() == [] - - -def test_dummy_returns_valid_data(): - dummy = Dummy() - assert dummy.get_data() == [] + path = "data.json" + query = "" + expected = [] + assert dummy.process_get(path, query) == expected From 9116e12e8f7a0977c51cdf2c9cd7147e85bb13f3 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 10 Jul 2024 15:00:29 +0200 Subject: [PATCH 06/35] Add the TCP/IP port in the `curl` calls --- schunk_egu_egk_gripper_dummy/src/dummy.py | 2 +- .../tests/test_dummy.py | 2 +- .../communication.hpp | 3 +++ .../src/communication.cpp | 21 +++++++------------ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index b9298bf..c84844b 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -31,7 +31,7 @@ def process_get(self, path: str, query: dict[str, list[str]]) -> dict | list | N print(f"query: {query}") if path == "info.json": - return {} + return {"dataformat": 0} # 0: Little endian, 1: Big endian if path == "enum.json": return [] if path == "data.json": diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index fd7808a..0523fb7 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -25,7 +25,7 @@ def test_dummy_responds_correctly_to_info_requests(): dummy = Dummy() path = "info.json" query = "" - expected = {} + expected = {"dataformat": 0} assert dummy.process_get(path, query) == expected diff --git a/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp b/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp index 0fc2040..469d725 100644 --- a/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp +++ b/schunk_egu_egk_gripper_library/include/schunk_egu_egk_gripper_library/communication.hpp @@ -171,6 +171,7 @@ class AnybusCom void initAddresses(); std::string ip; //IP to the connected gripper + int port = 80; //TCP/IP Port of the gripper uint8_t endian_format; //Flag for big/little Endian bool not_double_word; //Flag if double word is requested (double words are always big Endian) @@ -266,6 +267,7 @@ inline void AnybusCom::getWithInstance(const char inst[7], paramtype *param, con curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L); + curl_easy_setopt(curl, CURLOPT_PORT, port); res = curl_easy_perform(curl); if (res != CURLE_OK) @@ -302,6 +304,7 @@ inline void AnybusCom::getWithOffset(const std::string &offset, const size_t & c curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1); + curl_easy_setopt(curl, CURLOPT_PORT, port); res = curl_easy_perform(curl); diff --git a/schunk_egu_egk_gripper_library/src/communication.cpp b/schunk_egu_egk_gripper_library/src/communication.cpp index de50abc..026906f 100644 --- a/schunk_egu_egk_gripper_library/src/communication.cpp +++ b/schunk_egu_egk_gripper_library/src/communication.cpp @@ -37,7 +37,7 @@ size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp) } //Initialize the plcs and Addresses -AnybusCom::AnybusCom(const std::string &ip, int port) : ip(ip) +AnybusCom::AnybusCom(const std::string &ip, int port) : ip(ip), port(port) { initAddresses(); // for post and get curl = curl_easy_init(); @@ -172,20 +172,12 @@ void AnybusCom::postParameter(std::string inst, std::string value) //Inits used Addresses with the ip void AnybusCom::initAddresses() { - data_address = "http:///adi/data.json?"; - update_address = "http:///adi/update.json"; - enum_address = "http:///adi/enum.json?"; - info_address = "http:///adi/info.json"; - metadata_address = "http:///adi/metadata.json?"; - if(ip.size() >= 100) ip = "0.0.0.0"; - - data_address.insert(7, ip); - update_address.insert(7, ip); - enum_address.insert(7,ip); - info_address.insert(7,ip); - metadata_address.insert(7,ip); - + data_address = "http://" + ip + ":" + std::to_string(port) + "/adi/data.json?"; + update_address = "http://" + ip + ":" + std::to_string(port) + "/adi/update.json"; + enum_address = "http://" + ip + ":" + std::to_string(port) + "/adi/enum.json?"; + info_address = "http://" + ip + ":" + std::to_string(port) + "/adi/info.json"; + metadata_address = "http://" + ip + ":" + std::to_string(port) + "/adi/metadata.json?"; } //Translates the received string of double_word to an integer[4] an saves it in plc_sync_input @@ -271,6 +263,7 @@ void AnybusCom::getEnums(const char inst[7], const uint16_t &enumNum) curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L); + curl_easy_setopt(curl, CURLOPT_PORT, port); res = curl_easy_perform(curl); From 405641d8faee779bf2c289bb1626048716ef5ad7 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 12 Jul 2024 16:07:01 +0200 Subject: [PATCH 07/35] Make the dummy available for gripper tests - Move the launch fixture into `conftest.py`. This will probably be used by several test files. - Add a test to check whether the gripper's joint state topic gets published. --- schunk_egu_egk_gripper_tests/test/conftest.py | 38 +++++++++++++++++++ .../test/test_http_interface.py | 29 ++++++++++++++ .../test/test_launch.py | 23 +---------- 3 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 schunk_egu_egk_gripper_tests/test/test_http_interface.py diff --git a/schunk_egu_egk_gripper_tests/test/conftest.py b/schunk_egu_egk_gripper_tests/test/conftest.py index 0b0f1c0..09d1b3a 100644 --- a/schunk_egu_egk_gripper_tests/test/conftest.py +++ b/schunk_egu_egk_gripper_tests/test/conftest.py @@ -1,5 +1,11 @@ import pytest import rclpy +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription +from launch.substitutions import PathJoinSubstitution +from launch_ros.substitutions import FindPackageShare +import launch_pytest +import subprocess # We avoid black's F811, F401 linting warnings # by using pytest's special conftest.py file. @@ -12,3 +18,35 @@ def isolated(): rclpy.init() yield rclpy.shutdown() + + +@pytest.fixture() +def gripper_dummy(): + dummy_dir = "/home/stefan/src/ros2_workspaces/egu-egk/src/schunk_egu_egk_gripper/schunk_egu_egk_gripper_dummy" # noqa: E501 + start_cmd = " uvicorn main:server --port 8000" + p = subprocess.Popen( + "exec" + start_cmd, stdin=subprocess.PIPE, cwd=dummy_dir, shell=True + ) + + print("Started gripper dummy") + yield + p.kill() + print("Stopped gripper dummy") + + +@launch_pytest.fixture +def launch_description(): + setup = IncludeLaunchDescription( + PathJoinSubstitution( + [ + FindPackageShare("schunk_egu_egk_gripper_driver"), + "launch", + "schunk.launch.py", + ] + ), + launch_arguments={ + "IP": "127.0.0.1", + "port": "8000", + }.items(), + ) + return LaunchDescription([setup, launch_pytest.actions.ReadyToTest()]) diff --git a/schunk_egu_egk_gripper_tests/test/test_http_interface.py b/schunk_egu_egk_gripper_tests/test/test_http_interface.py new file mode 100644 index 0000000..783c313 --- /dev/null +++ b/schunk_egu_egk_gripper_tests/test/test_http_interface.py @@ -0,0 +1,29 @@ +import pytest +from test.conftest import launch_description +import time +from threading import Thread, Event +import rclpy +from rclpy.node import Node +from sensor_msgs.msg import JointState +from typing import Any + + +class CheckTopic(Node): + def __init__(self, topic: str, type: Any): + super().__init__("check_topic") + self.event = Event() + self.sub = self.create_subscription(type, topic, self.msg_cb, 3) + self.thread = Thread(target=lambda node: rclpy.spin(node), args=(self,)) + self.thread.start() + + def msg_cb(self, data: Any) -> None: + self.event.set() + + +@pytest.mark.launch(fixture=launch_description) +def test_driver_connnects_to_gripper_dummy(launch_context, isolated, gripper_dummy): + until_dummy_ready = 3 + timeout = 3 + + time.sleep(until_dummy_ready) + assert CheckTopic("/joint_states", JointState).event.wait(timeout) diff --git a/schunk_egu_egk_gripper_tests/test/test_launch.py b/schunk_egu_egk_gripper_tests/test/test_launch.py index e8c3ae5..3b85f46 100644 --- a/schunk_egu_egk_gripper_tests/test/test_launch.py +++ b/schunk_egu_egk_gripper_tests/test/test_launch.py @@ -1,31 +1,12 @@ #!/usr/bin/env python3 import pytest -from launch import LaunchDescription -from launch.actions import IncludeLaunchDescription -from launch.substitutions import PathJoinSubstitution -from launch_ros.substitutions import FindPackageShare -import launch_pytest - from rclpy.node import Node import time - - -@launch_pytest.fixture -def launch_description(): - setup = IncludeLaunchDescription( - PathJoinSubstitution( - [ - FindPackageShare("schunk_egu_egk_gripper_driver"), - "launch", - "schunk.launch.py", - ] - ) - ) - return LaunchDescription([setup, launch_pytest.actions.ReadyToTest()]) +from test.conftest import launch_description @pytest.mark.launch(fixture=launch_description) -def test_normal_startup_works(launch_context, isolated): +def test_normal_startup_works(launch_context, isolated, gripper_dummy): node = Node("test_startup") until_ready = 2.0 # sec time.sleep(until_ready) From ece126682b7b56ff910e1766131cb15822d1074f Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Sat, 13 Jul 2024 10:23:34 +0200 Subject: [PATCH 08/35] Make the gripper dummy a ROS2 package That's easier for installation and usage in the driver's test repository. The idea is, still, to keep the dummy ROS2-free in case we want to ship it separately. --- schunk_egu_egk_gripper_dummy/README.md | 11 +++----- schunk_egu_egk_gripper_dummy/package.xml | 13 +++++++++ .../resource/schunk_egu_egk_gripper_dummy | 0 .../schunk_egu_egk_gripper_dummy/__init__.py | 0 .../main.py | 0 schunk_egu_egk_gripper_dummy/setup.cfg | 4 +++ schunk_egu_egk_gripper_dummy/setup.py | 27 +++++++++++++++++++ schunk_egu_egk_gripper_tests/test/conftest.py | 5 +++- 8 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 schunk_egu_egk_gripper_dummy/package.xml create mode 100644 schunk_egu_egk_gripper_dummy/resource/schunk_egu_egk_gripper_dummy create mode 100644 schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/__init__.py rename schunk_egu_egk_gripper_dummy/{ => schunk_egu_egk_gripper_dummy}/main.py (100%) create mode 100644 schunk_egu_egk_gripper_dummy/setup.cfg create mode 100644 schunk_egu_egk_gripper_dummy/setup.py diff --git a/schunk_egu_egk_gripper_dummy/README.md b/schunk_egu_egk_gripper_dummy/README.md index 1405add..f799ea8 100644 --- a/schunk_egu_egk_gripper_dummy/README.md +++ b/schunk_egu_egk_gripper_dummy/README.md @@ -1,27 +1,24 @@ # Schunk EGU/EGK Dummy A minimalist protocol simulator for system tests. -## Install -Use a virtual environment and +## Dependencies + ```bash pip install fastapi uvicorn ``` ## Getting started -1. In a new terminal, start the dummy +1. Start the dummy standalone with ```bash - uvicorn main:server --port 8000 --reload + uvicorn schunk_egu_egk_gripper_dummy.main:server --port 8000 --reload ``` ## Run tests locally -Inside a virtual environment (.venv) - ```bash pip install pytest coverage ``` -And then inside this repository ```bash coverage run -m pytest tests/ coverage report diff --git a/schunk_egu_egk_gripper_dummy/package.xml b/schunk_egu_egk_gripper_dummy/package.xml new file mode 100644 index 0000000..2191cf0 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/package.xml @@ -0,0 +1,13 @@ + + + + schunk_egu_egk_gripper_dummy + 0.0.0 + A minimalist dummy for simulating the gripper's communication + Stefan Scherzinger + GPL-3.0-or-later + + + ament_python + + diff --git a/schunk_egu_egk_gripper_dummy/resource/schunk_egu_egk_gripper_dummy b/schunk_egu_egk_gripper_dummy/resource/schunk_egu_egk_gripper_dummy new file mode 100644 index 0000000..e69de29 diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/__init__.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py similarity index 100% rename from schunk_egu_egk_gripper_dummy/main.py rename to schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py diff --git a/schunk_egu_egk_gripper_dummy/setup.cfg b/schunk_egu_egk_gripper_dummy/setup.cfg new file mode 100644 index 0000000..31b0841 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/schunk_egu_egk_gripper_dummy +[install] +install_scripts=$base/lib/schunk_egu_egk_gripper_dummy diff --git a/schunk_egu_egk_gripper_dummy/setup.py b/schunk_egu_egk_gripper_dummy/setup.py new file mode 100644 index 0000000..4e52814 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/setup.py @@ -0,0 +1,27 @@ +from setuptools import find_packages, setup +import os +from glob import glob + +package_name = "schunk_egu_egk_gripper_dummy" + +setup( + name=package_name, + version="0.0.0", + packages=find_packages(exclude=["tests"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + (os.path.join("share", package_name), [package_name + "/main.py"]), + (os.path.join("share", package_name, "src"), glob("src/*.py")), + ], + install_requires=["setuptools"], + zip_safe=True, + maintainer="Stefan Scherzinger", + maintainer_email="stefan.scherzinger@de.schunk.com", + description="A minimalist dummy for simulating the gripper's communication", + license="GPL-3.0-or-later", + tests_require=["pytest", "coverage"], + entry_points={ + "console_scripts": [], + }, +) diff --git a/schunk_egu_egk_gripper_tests/test/conftest.py b/schunk_egu_egk_gripper_tests/test/conftest.py index 09d1b3a..cd37991 100644 --- a/schunk_egu_egk_gripper_tests/test/conftest.py +++ b/schunk_egu_egk_gripper_tests/test/conftest.py @@ -6,6 +6,8 @@ from launch_ros.substitutions import FindPackageShare import launch_pytest import subprocess +from ament_index_python.packages import get_package_share_directory + # We avoid black's F811, F401 linting warnings # by using pytest's special conftest.py file. @@ -22,7 +24,8 @@ def isolated(): @pytest.fixture() def gripper_dummy(): - dummy_dir = "/home/stefan/src/ros2_workspaces/egu-egk/src/schunk_egu_egk_gripper/schunk_egu_egk_gripper_dummy" # noqa: E501 + package_name = "schunk_egu_egk_gripper_dummy" + dummy_dir = get_package_share_directory(package_name) start_cmd = " uvicorn main:server --port 8000" p = subprocess.Popen( "exec" + start_cmd, stdin=subprocess.PIPE, cwd=dummy_dir, shell=True From 98bf9ca0a3e5a00a0cbb635348d014828e606a75 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 17 Jul 2024 09:49:01 +0200 Subject: [PATCH 09/35] Provide system parameters in a config folder Also add a script to read `data.json` from real gripper hardware. --- schunk_egu_egk_gripper_dummy/config/README.md | 16 + schunk_egu_egk_gripper_dummy/config/data.json | 161 ++ schunk_egu_egk_gripper_dummy/config/enum.json | 2144 +++++++++++++++++ .../config/read_system_parameters.py | 45 + .../config/system_parameter_codes | 58 + 5 files changed, 2424 insertions(+) create mode 100644 schunk_egu_egk_gripper_dummy/config/README.md create mode 100644 schunk_egu_egk_gripper_dummy/config/data.json create mode 100644 schunk_egu_egk_gripper_dummy/config/enum.json create mode 100755 schunk_egu_egk_gripper_dummy/config/read_system_parameters.py create mode 100644 schunk_egu_egk_gripper_dummy/config/system_parameter_codes diff --git a/schunk_egu_egk_gripper_dummy/config/README.md b/schunk_egu_egk_gripper_dummy/config/README.md new file mode 100644 index 0000000..dd0068f --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/config/README.md @@ -0,0 +1,16 @@ +# Reading system parameters from a gripper +The system parameters are mentioned in the gripper's datasheet [Commissioning Instructions, Firmware 5.2 EGU with EtherNet/IP interface](https://stb.cloud.schunk.com/media/IM0046706.PDF). + +## Enums +The easiest approach is reading enums from a web browser. +Enums need to be accessed via the _DEC_ identifier. +For instance, reading available error codes for the enum `HEX=0x0118` (`DEC=280`) would be +```browser +http:///adi/enum.json?inst=280 +``` + +## Data +Use this script that reads the data directly from the gripper: +```bash +./read_system_parameters.py +``` diff --git a/schunk_egu_egk_gripper_dummy/config/data.json b/schunk_egu_egk_gripper_dummy/config/data.json new file mode 100644 index 0000000..f34d7a9 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/config/data.json @@ -0,0 +1,161 @@ +{ + "0x0040": [ + "210000800000A2800000000000000000" + ], + "0x0048": [ + "05000000000000000000000000000000" + ], + "0x0100": [ + "0000" + ], + "0x0118": [ + "00" + ], + "0x0120": [ + "00" + ], + "0x0128": [ + "001F" + ], + "0x0130": [ + "3238333A34303A3534203C307837323E206C6F67696320766F6C746167652031362E343336203C2031392E3230302056202876616C75655F6D6F6E69746F722E6370703A3A32312900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "0x0230": [ + "4226667D" + ], + "0x0238": [ + "00000000" + ], + "0x0380": [ + "0000" + ], + "0x03A8": [ + "3F828F5C" + ], + "0x03B0": [ + "000000000000000042A70000000000000000000000000000" + ], + "0x03B8": [ + "BF251EB8BEAD844D423A7AE1448EC57E44E16CEE45066271" + ], + "0x0500": [ + "12" + ], + "0x0528": [ + "3F800000" + ], + "0x0540": [ + "40A00000" + ], + "0x0580": [ + "40000000" + ], + "0x0588": [ + "42A60000" + ], + "0x05A8": [ + "40A00000" + ], + "0x0600": [ + "00000000" + ], + "0x0608": [ + "42A60000" + ], + "0x0610": [ + "00000000" + ], + "0x0628": [ + "40B00000" + ], + "0x0630": [ + "42E60000" + ], + "0x0650": [ + "41B00000" + ], + "0x0658": [ + "42960000" + ], + "0x0660": [ + "43160000" + ], + "0x06A8": [ + "00000000" + ], + "0x0800": [ + "4199999A" + ], + "0x0808": [ + "41F00000" + ], + "0x0810": [ + "4199999A" + ], + "0x0818": [ + "41E66666" + ], + "0x0820": [ + "C0A00000" + ], + "0x0828": [ + "42AA0000" + ], + "0x0840": [ + "41F5DC60" + ], + "0x0870": [ + "41BA7D40" + ], + "0x0878": [ + "41C04897" + ], + "0x0880": [ + "41ACCCCD" + ], + "0x0888": [ + "41D33333" + ], + "0x0890": [ + "41ACCCCD" + ], + "0x0898": [ + "41D33333" + ], + "0x08A0": [ + "40A00000" + ], + "0x08A8": [ + "42960000" + ], + "0x1000": [ + "4E534E53303731390000000000000000" + ], + "0x1008": [ + "31323334350000000000000000000000" + ], + "0x1020": [ + "3B036ECF" + ], + "0x1100": [ + "536570202034203230323300" + ], + "0x1108": [ + "31383A30343A313400" + ], + "0x1110": [ + "01F6" + ], + "0x1118": [ + "352E322E302E38313839360000000000000000000000" + ], + "0x1138": [ + "0030114844C7" + ], + "0x1330": [ + "01" + ], + "0x1400": [ + "00000080" + ] +} diff --git a/schunk_egu_egk_gripper_dummy/config/enum.json b/schunk_egu_egk_gripper_dummy/config/enum.json new file mode 100644 index 0000000..faa406e --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/config/enum.json @@ -0,0 +1,2144 @@ +{ + "0x0118": [ + { + "value": 0, + "string": "ERR_NONE" + }, + { + "value": 1, + "string": "" + }, + { + "value": 2, + "string": "" + }, + { + "value": 3, + "string": "ERR_NO_RIGHTS" + }, + { + "value": 4, + "string": "INF_UNKNOWN_CMD" + }, + { + "value": 5, + "string": "INF_FAILED" + }, + { + "value": 6, + "string": "" + }, + { + "value": 7, + "string": "" + }, + { + "value": 8, + "string": "" + }, + { + "value": 9, + "string": "" + }, + { + "value": 10, + "string": "" + }, + { + "value": 11, + "string": "" + }, + { + "value": 12, + "string": "" + }, + { + "value": 13, + "string": "" + }, + { + "value": 14, + "string": "" + }, + { + "value": 15, + "string": "" + }, + { + "value": 16, + "string": "" + }, + { + "value": 17, + "string": "" + }, + { + "value": 18, + "string": "INF_WRONG_TYPE" + }, + { + "value": 19, + "string": "" + }, + { + "value": 20, + "string": "INF_NO_AUTHORITY" + }, + { + "value": 21, + "string": "" + }, + { + "value": 22, + "string": "" + }, + { + "value": 23, + "string": "" + }, + { + "value": 24, + "string": "" + }, + { + "value": 25, + "string": "" + }, + { + "value": 26, + "string": "" + }, + { + "value": 27, + "string": "INF_VAL_LIM_MAX" + }, + { + "value": 28, + "string": "INF_VAL_LIM_MIN" + }, + { + "value": 29, + "string": "" + }, + { + "value": 30, + "string": "" + }, + { + "value": 31, + "string": "" + }, + { + "value": 32, + "string": "" + }, + { + "value": 33, + "string": "" + }, + { + "value": 34, + "string": "" + }, + { + "value": 35, + "string": "" + }, + { + "value": 36, + "string": "" + }, + { + "value": 37, + "string": "" + }, + { + "value": 38, + "string": "" + }, + { + "value": 39, + "string": "" + }, + { + "value": 40, + "string": "ERR_BT_FAILED" + }, + { + "value": 41, + "string": "" + }, + { + "value": 42, + "string": "" + }, + { + "value": 43, + "string": "" + }, + { + "value": 44, + "string": "" + }, + { + "value": 45, + "string": "" + }, + { + "value": 46, + "string": "" + }, + { + "value": 47, + "string": "" + }, + { + "value": 48, + "string": "" + }, + { + "value": 49, + "string": "" + }, + { + "value": 50, + "string": "" + }, + { + "value": 51, + "string": "" + }, + { + "value": 52, + "string": "" + }, + { + "value": 53, + "string": "" + }, + { + "value": 54, + "string": "" + }, + { + "value": 55, + "string": "" + }, + { + "value": 56, + "string": "" + }, + { + "value": 57, + "string": "" + }, + { + "value": 58, + "string": "" + }, + { + "value": 59, + "string": "" + }, + { + "value": 60, + "string": "" + }, + { + "value": 61, + "string": "" + }, + { + "value": 62, + "string": "" + }, + { + "value": 63, + "string": "" + }, + { + "value": 64, + "string": "" + }, + { + "value": 65, + "string": "" + }, + { + "value": 66, + "string": "" + }, + { + "value": 67, + "string": "" + }, + { + "value": 68, + "string": "" + }, + { + "value": 69, + "string": "" + }, + { + "value": 70, + "string": "" + }, + { + "value": 71, + "string": "" + }, + { + "value": 72, + "string": "" + }, + { + "value": 73, + "string": "" + }, + { + "value": 74, + "string": "" + }, + { + "value": 75, + "string": "" + }, + { + "value": 76, + "string": "" + }, + { + "value": 77, + "string": "" + }, + { + "value": 78, + "string": "" + }, + { + "value": 79, + "string": "" + }, + { + "value": 80, + "string": "" + }, + { + "value": 81, + "string": "" + }, + { + "value": 82, + "string": "" + }, + { + "value": 83, + "string": "" + }, + { + "value": 84, + "string": "" + }, + { + "value": 85, + "string": "" + }, + { + "value": 86, + "string": "" + }, + { + "value": 87, + "string": "" + }, + { + "value": 88, + "string": "" + }, + { + "value": 89, + "string": "" + }, + { + "value": 90, + "string": "" + }, + { + "value": 91, + "string": "" + }, + { + "value": 92, + "string": "" + }, + { + "value": 93, + "string": "" + }, + { + "value": 94, + "string": "" + }, + { + "value": 95, + "string": "" + }, + { + "value": 96, + "string": "" + }, + { + "value": 97, + "string": "" + }, + { + "value": 98, + "string": "" + }, + { + "value": 99, + "string": "" + }, + { + "value": 100, + "string": "" + }, + { + "value": 101, + "string": "" + }, + { + "value": 102, + "string": "" + }, + { + "value": 103, + "string": "" + }, + { + "value": 104, + "string": "" + }, + { + "value": 105, + "string": "" + }, + { + "value": 106, + "string": "" + }, + { + "value": 107, + "string": "" + }, + { + "value": 108, + "string": "ERR_MOT_TEMP_LO" + }, + { + "value": 109, + "string": "ERR_MOT_TEMP_HI" + }, + { + "value": 110, + "string": "" + }, + { + "value": 111, + "string": "" + }, + { + "value": 112, + "string": "ERR_LGC_TEMP_LO" + }, + { + "value": 113, + "string": "ERR_LGC_TEMP_HI" + }, + { + "value": 114, + "string": "ERR_LGC_VOLT_LO" + }, + { + "value": 115, + "string": "ERR_LGC_VOLT_HI" + }, + { + "value": 116, + "string": "ERR_MOT_VOLT_LO" + }, + { + "value": 117, + "string": "ERR_MOT_VOLT_HI" + }, + { + "value": 118, + "string": "" + }, + { + "value": 119, + "string": "" + }, + { + "value": 120, + "string": "" + }, + { + "value": 121, + "string": "" + }, + { + "value": 122, + "string": "" + }, + { + "value": 123, + "string": "" + }, + { + "value": 124, + "string": "ERR_REBOOT" + }, + { + "value": 125, + "string": "" + }, + { + "value": 126, + "string": "ERR_POWER_STAGE" + }, + { + "value": 127, + "string": "" + }, + { + "value": 128, + "string": "" + }, + { + "value": 129, + "string": "" + }, + { + "value": 130, + "string": "" + }, + { + "value": 131, + "string": "" + }, + { + "value": 132, + "string": "" + }, + { + "value": 133, + "string": "" + }, + { + "value": 134, + "string": "" + }, + { + "value": 135, + "string": "" + }, + { + "value": 136, + "string": "" + }, + { + "value": 137, + "string": "ERR_ZV_NOT_FOUND" + }, + { + "value": 138, + "string": "ERR_ENC_PHASE" + }, + { + "value": 139, + "string": "ERR_ENC_SIN_LO" + }, + { + "value": 140, + "string": "ERR_ENC_SIN_HI" + }, + { + "value": 141, + "string": "ERR_ENC_COS_LO" + }, + { + "value": 142, + "string": "ERR_ENC_COS_HI" + }, + { + "value": 143, + "string": "ERR_ENC_SHORTCUT" + }, + { + "value": 144, + "string": "WRN_LGC_TEMP_LO" + }, + { + "value": 145, + "string": "WRN_LGC_TEMP_HI" + }, + { + "value": 146, + "string": "WRN_MOT_TEMP_LO" + }, + { + "value": 147, + "string": "WRN_MOT_TEMP_HI" + }, + { + "value": 148, + "string": "WRN_NOT_FEASIBLE" + }, + { + "value": 149, + "string": "WRN_POS_LIMIT" + }, + { + "value": 150, + "string": "WRN_LGC_VOLT_LO" + }, + { + "value": 151, + "string": "WRN_LGC_VOLT_HI" + }, + { + "value": 152, + "string": "WRN_MOT_VOLT_LO" + }, + { + "value": 153, + "string": "WRN_MOT_VOLT_HI" + }, + { + "value": 154, + "string": "" + }, + { + "value": 155, + "string": "WRN_FLASH_FAILED" + }, + { + "value": 156, + "string": "WRN_ERASE_FAILED" + }, + { + "value": 157, + "string": "WRN_FACT_FAILED" + }, + { + "value": 158, + "string": "WRN_AUTH_FAILED" + }, + { + "value": 159, + "string": "WRN_SD_NOT_PREP" + }, + { + "value": 160, + "string": "" + }, + { + "value": 161, + "string": "WRN_NO_PART" + }, + { + "value": 162, + "string": "WRN_BCKUP_RSTORE" + }, + { + "value": 163, + "string": "" + }, + { + "value": 164, + "string": "" + }, + { + "value": 165, + "string": "" + }, + { + "value": 166, + "string": "" + }, + { + "value": 167, + "string": "" + }, + { + "value": 168, + "string": "" + }, + { + "value": 169, + "string": "" + }, + { + "value": 170, + "string": "" + }, + { + "value": 171, + "string": "" + }, + { + "value": 172, + "string": "" + }, + { + "value": 173, + "string": "" + }, + { + "value": 174, + "string": "" + }, + { + "value": 175, + "string": "" + }, + { + "value": 176, + "string": "" + }, + { + "value": 177, + "string": "" + }, + { + "value": 178, + "string": "" + }, + { + "value": 179, + "string": "" + }, + { + "value": 180, + "string": "ERR_ENC_PULSES" + }, + { + "value": 181, + "string": "ERR_ENC_NO_INDEX" + }, + { + "value": 182, + "string": "ERR_ENC_MN_INDEX" + }, + { + "value": 183, + "string": "ERR_ENC_HALL_ILL" + }, + { + "value": 184, + "string": "ERR_ENC_NO_HALL" + }, + { + "value": 185, + "string": "ERR_ENC_ABS_FAIL" + }, + { + "value": 186, + "string": "ERR_CHP_TIMEOUT" + }, + { + "value": 187, + "string": "ERR_CHP_FAILED" + }, + { + "value": 188, + "string": "" + }, + { + "value": 189, + "string": "" + }, + { + "value": 190, + "string": "" + }, + { + "value": 191, + "string": "" + }, + { + "value": 192, + "string": "" + }, + { + "value": 193, + "string": "" + }, + { + "value": 194, + "string": "" + }, + { + "value": 195, + "string": "" + }, + { + "value": 196, + "string": "" + }, + { + "value": 197, + "string": "" + }, + { + "value": 198, + "string": "" + }, + { + "value": 199, + "string": "" + }, + { + "value": 200, + "string": "" + }, + { + "value": 201, + "string": "" + }, + { + "value": 202, + "string": "" + }, + { + "value": 203, + "string": "" + }, + { + "value": 204, + "string": "" + }, + { + "value": 205, + "string": "" + }, + { + "value": 206, + "string": "" + }, + { + "value": 207, + "string": "" + }, + { + "value": 208, + "string": "" + }, + { + "value": 209, + "string": "" + }, + { + "value": 210, + "string": "" + }, + { + "value": 211, + "string": "" + }, + { + "value": 212, + "string": "ERR_INVAL_PHRASE" + }, + { + "value": 213, + "string": "ERR_SOFT_LOW" + }, + { + "value": 214, + "string": "ERR_SOFT_HIGH" + }, + { + "value": 215, + "string": "" + }, + { + "value": 216, + "string": "" + }, + { + "value": 217, + "string": "ERR_FAST_STOP" + }, + { + "value": 218, + "string": "" + }, + { + "value": 219, + "string": "" + }, + { + "value": 220, + "string": "" + }, + { + "value": 221, + "string": "" + }, + { + "value": 222, + "string": "ERR_CURRENT" + }, + { + "value": 223, + "string": "ERR_I2T" + }, + { + "value": 224, + "string": "" + }, + { + "value": 225, + "string": "ERR_INTERNAL" + }, + { + "value": 226, + "string": "" + }, + { + "value": 227, + "string": "" + }, + { + "value": 228, + "string": "ERR_TOO_FAST" + }, + { + "value": 229, + "string": "" + }, + { + "value": 230, + "string": "" + }, + { + "value": 231, + "string": "ERR_FLASH_FAILED" + }, + { + "value": 232, + "string": "" + }, + { + "value": 233, + "string": "" + }, + { + "value": 234, + "string": "" + }, + { + "value": 235, + "string": "" + }, + { + "value": 236, + "string": "" + }, + { + "value": 237, + "string": "" + }, + { + "value": 238, + "string": "" + }, + { + "value": 239, + "string": "ERR_COMM_LOST" + }, + { + "value": 240, + "string": "ERR_REF_ABORT_TO" + }, + { + "value": 241, + "string": "ERR_MOV_ABORT_TO" + }, + { + "value": 242, + "string": "ERR_NO_REF" + }, + { + "value": 243, + "string": "" + }, + { + "value": 244, + "string": "ERR_MOVE_BLOCKED" + }, + { + "value": 245, + "string": "ERR_UNKNOWN_HW" + }, + { + "value": 246, + "string": "ERR_BLOCK_FAILED" + }, + { + "value": 247, + "string": "ERR_NO_COMM" + }, + { + "value": 248, + "string": "ERR_WRONG_HW" + }, + { + "value": 249, + "string": "ERR_WRONG_MODULE" + }, + { + "value": 250, + "string": "ERR_SD_FAILED" + }, + { + "value": 251, + "string": "ERR_FLASH_LOST" + }, + { + "value": 252, + "string": "ERR_SW_COMM" + } + ], + "0x120": [ + { + "value": 0, + "string": "ERR_NONE" + }, + { + "value": 1, + "string": "" + }, + { + "value": 2, + "string": "" + }, + { + "value": 3, + "string": "ERR_NO_RIGHTS" + }, + { + "value": 4, + "string": "INF_UNKNOWN_CMD" + }, + { + "value": 5, + "string": "INF_FAILED" + }, + { + "value": 6, + "string": "" + }, + { + "value": 7, + "string": "" + }, + { + "value": 8, + "string": "" + }, + { + "value": 9, + "string": "" + }, + { + "value": 10, + "string": "" + }, + { + "value": 11, + "string": "" + }, + { + "value": 12, + "string": "" + }, + { + "value": 13, + "string": "" + }, + { + "value": 14, + "string": "" + }, + { + "value": 15, + "string": "" + }, + { + "value": 16, + "string": "" + }, + { + "value": 17, + "string": "" + }, + { + "value": 18, + "string": "INF_WRONG_TYPE" + }, + { + "value": 19, + "string": "" + }, + { + "value": 20, + "string": "INF_NO_AUTHORITY" + }, + { + "value": 21, + "string": "" + }, + { + "value": 22, + "string": "" + }, + { + "value": 23, + "string": "" + }, + { + "value": 24, + "string": "" + }, + { + "value": 25, + "string": "" + }, + { + "value": 26, + "string": "" + }, + { + "value": 27, + "string": "INF_VAL_LIM_MAX" + }, + { + "value": 28, + "string": "INF_VAL_LIM_MIN" + }, + { + "value": 29, + "string": "" + }, + { + "value": 30, + "string": "" + }, + { + "value": 31, + "string": "" + }, + { + "value": 32, + "string": "" + }, + { + "value": 33, + "string": "" + }, + { + "value": 34, + "string": "" + }, + { + "value": 35, + "string": "" + }, + { + "value": 36, + "string": "" + }, + { + "value": 37, + "string": "" + }, + { + "value": 38, + "string": "" + }, + { + "value": 39, + "string": "" + }, + { + "value": 40, + "string": "ERR_BT_FAILED" + }, + { + "value": 41, + "string": "" + }, + { + "value": 42, + "string": "" + }, + { + "value": 43, + "string": "" + }, + { + "value": 44, + "string": "" + }, + { + "value": 45, + "string": "" + }, + { + "value": 46, + "string": "" + }, + { + "value": 47, + "string": "" + }, + { + "value": 48, + "string": "" + }, + { + "value": 49, + "string": "" + }, + { + "value": 50, + "string": "" + }, + { + "value": 51, + "string": "" + }, + { + "value": 52, + "string": "" + }, + { + "value": 53, + "string": "" + }, + { + "value": 54, + "string": "" + }, + { + "value": 55, + "string": "" + }, + { + "value": 56, + "string": "" + }, + { + "value": 57, + "string": "" + }, + { + "value": 58, + "string": "" + }, + { + "value": 59, + "string": "" + }, + { + "value": 60, + "string": "" + }, + { + "value": 61, + "string": "" + }, + { + "value": 62, + "string": "" + }, + { + "value": 63, + "string": "" + }, + { + "value": 64, + "string": "" + }, + { + "value": 65, + "string": "" + }, + { + "value": 66, + "string": "" + }, + { + "value": 67, + "string": "" + }, + { + "value": 68, + "string": "" + }, + { + "value": 69, + "string": "" + }, + { + "value": 70, + "string": "" + }, + { + "value": 71, + "string": "" + }, + { + "value": 72, + "string": "" + }, + { + "value": 73, + "string": "" + }, + { + "value": 74, + "string": "" + }, + { + "value": 75, + "string": "" + }, + { + "value": 76, + "string": "" + }, + { + "value": 77, + "string": "" + }, + { + "value": 78, + "string": "" + }, + { + "value": 79, + "string": "" + }, + { + "value": 80, + "string": "" + }, + { + "value": 81, + "string": "" + }, + { + "value": 82, + "string": "" + }, + { + "value": 83, + "string": "" + }, + { + "value": 84, + "string": "" + }, + { + "value": 85, + "string": "" + }, + { + "value": 86, + "string": "" + }, + { + "value": 87, + "string": "" + }, + { + "value": 88, + "string": "" + }, + { + "value": 89, + "string": "" + }, + { + "value": 90, + "string": "" + }, + { + "value": 91, + "string": "" + }, + { + "value": 92, + "string": "" + }, + { + "value": 93, + "string": "" + }, + { + "value": 94, + "string": "" + }, + { + "value": 95, + "string": "" + }, + { + "value": 96, + "string": "" + }, + { + "value": 97, + "string": "" + }, + { + "value": 98, + "string": "" + }, + { + "value": 99, + "string": "" + }, + { + "value": 100, + "string": "" + }, + { + "value": 101, + "string": "" + }, + { + "value": 102, + "string": "" + }, + { + "value": 103, + "string": "" + }, + { + "value": 104, + "string": "" + }, + { + "value": 105, + "string": "" + }, + { + "value": 106, + "string": "" + }, + { + "value": 107, + "string": "" + }, + { + "value": 108, + "string": "ERR_MOT_TEMP_LO" + }, + { + "value": 109, + "string": "ERR_MOT_TEMP_HI" + }, + { + "value": 110, + "string": "" + }, + { + "value": 111, + "string": "" + }, + { + "value": 112, + "string": "ERR_LGC_TEMP_LO" + }, + { + "value": 113, + "string": "ERR_LGC_TEMP_HI" + }, + { + "value": 114, + "string": "ERR_LGC_VOLT_LO" + }, + { + "value": 115, + "string": "ERR_LGC_VOLT_HI" + }, + { + "value": 116, + "string": "ERR_MOT_VOLT_LO" + }, + { + "value": 117, + "string": "ERR_MOT_VOLT_HI" + }, + { + "value": 118, + "string": "" + }, + { + "value": 119, + "string": "" + }, + { + "value": 120, + "string": "" + }, + { + "value": 121, + "string": "" + }, + { + "value": 122, + "string": "" + }, + { + "value": 123, + "string": "" + }, + { + "value": 124, + "string": "ERR_REBOOT" + }, + { + "value": 125, + "string": "" + }, + { + "value": 126, + "string": "ERR_POWER_STAGE" + }, + { + "value": 127, + "string": "" + }, + { + "value": 128, + "string": "" + }, + { + "value": 129, + "string": "" + }, + { + "value": 130, + "string": "" + }, + { + "value": 131, + "string": "" + }, + { + "value": 132, + "string": "" + }, + { + "value": 133, + "string": "" + }, + { + "value": 134, + "string": "" + }, + { + "value": 135, + "string": "" + }, + { + "value": 136, + "string": "" + }, + { + "value": 137, + "string": "ERR_ZV_NOT_FOUND" + }, + { + "value": 138, + "string": "ERR_ENC_PHASE" + }, + { + "value": 139, + "string": "ERR_ENC_SIN_LO" + }, + { + "value": 140, + "string": "ERR_ENC_SIN_HI" + }, + { + "value": 141, + "string": "ERR_ENC_COS_LO" + }, + { + "value": 142, + "string": "ERR_ENC_COS_HI" + }, + { + "value": 143, + "string": "ERR_ENC_SHORTCUT" + }, + { + "value": 144, + "string": "WRN_LGC_TEMP_LO" + }, + { + "value": 145, + "string": "WRN_LGC_TEMP_HI" + }, + { + "value": 146, + "string": "WRN_MOT_TEMP_LO" + }, + { + "value": 147, + "string": "WRN_MOT_TEMP_HI" + }, + { + "value": 148, + "string": "WRN_NOT_FEASIBLE" + }, + { + "value": 149, + "string": "WRN_POS_LIMIT" + }, + { + "value": 150, + "string": "WRN_LGC_VOLT_LO" + }, + { + "value": 151, + "string": "WRN_LGC_VOLT_HI" + }, + { + "value": 152, + "string": "WRN_MOT_VOLT_LO" + }, + { + "value": 153, + "string": "WRN_MOT_VOLT_HI" + }, + { + "value": 154, + "string": "" + }, + { + "value": 155, + "string": "WRN_FLASH_FAILED" + }, + { + "value": 156, + "string": "WRN_ERASE_FAILED" + }, + { + "value": 157, + "string": "WRN_FACT_FAILED" + }, + { + "value": 158, + "string": "WRN_AUTH_FAILED" + }, + { + "value": 159, + "string": "WRN_SD_NOT_PREP" + }, + { + "value": 160, + "string": "" + }, + { + "value": 161, + "string": "WRN_NO_PART" + }, + { + "value": 162, + "string": "WRN_BCKUP_RSTORE" + }, + { + "value": 163, + "string": "" + }, + { + "value": 164, + "string": "" + }, + { + "value": 165, + "string": "" + }, + { + "value": 166, + "string": "" + }, + { + "value": 167, + "string": "" + }, + { + "value": 168, + "string": "" + }, + { + "value": 169, + "string": "" + }, + { + "value": 170, + "string": "" + }, + { + "value": 171, + "string": "" + }, + { + "value": 172, + "string": "" + }, + { + "value": 173, + "string": "" + }, + { + "value": 174, + "string": "" + }, + { + "value": 175, + "string": "" + }, + { + "value": 176, + "string": "" + }, + { + "value": 177, + "string": "" + }, + { + "value": 178, + "string": "" + }, + { + "value": 179, + "string": "" + }, + { + "value": 180, + "string": "ERR_ENC_PULSES" + }, + { + "value": 181, + "string": "ERR_ENC_NO_INDEX" + }, + { + "value": 182, + "string": "ERR_ENC_MN_INDEX" + }, + { + "value": 183, + "string": "ERR_ENC_HALL_ILL" + }, + { + "value": 184, + "string": "ERR_ENC_NO_HALL" + }, + { + "value": 185, + "string": "ERR_ENC_ABS_FAIL" + }, + { + "value": 186, + "string": "ERR_CHP_TIMEOUT" + }, + { + "value": 187, + "string": "ERR_CHP_FAILED" + }, + { + "value": 188, + "string": "" + }, + { + "value": 189, + "string": "" + }, + { + "value": 190, + "string": "" + }, + { + "value": 191, + "string": "" + }, + { + "value": 192, + "string": "" + }, + { + "value": 193, + "string": "" + }, + { + "value": 194, + "string": "" + }, + { + "value": 195, + "string": "" + }, + { + "value": 196, + "string": "" + }, + { + "value": 197, + "string": "" + }, + { + "value": 198, + "string": "" + }, + { + "value": 199, + "string": "" + }, + { + "value": 200, + "string": "" + }, + { + "value": 201, + "string": "" + }, + { + "value": 202, + "string": "" + }, + { + "value": 203, + "string": "" + }, + { + "value": 204, + "string": "" + }, + { + "value": 205, + "string": "" + }, + { + "value": 206, + "string": "" + }, + { + "value": 207, + "string": "" + }, + { + "value": 208, + "string": "" + }, + { + "value": 209, + "string": "" + }, + { + "value": 210, + "string": "" + }, + { + "value": 211, + "string": "" + }, + { + "value": 212, + "string": "ERR_INVAL_PHRASE" + }, + { + "value": 213, + "string": "ERR_SOFT_LOW" + }, + { + "value": 214, + "string": "ERR_SOFT_HIGH" + }, + { + "value": 215, + "string": "" + }, + { + "value": 216, + "string": "" + }, + { + "value": 217, + "string": "ERR_FAST_STOP" + }, + { + "value": 218, + "string": "" + }, + { + "value": 219, + "string": "" + }, + { + "value": 220, + "string": "" + }, + { + "value": 221, + "string": "" + }, + { + "value": 222, + "string": "ERR_CURRENT" + }, + { + "value": 223, + "string": "ERR_I2T" + }, + { + "value": 224, + "string": "" + }, + { + "value": 225, + "string": "ERR_INTERNAL" + }, + { + "value": 226, + "string": "" + }, + { + "value": 227, + "string": "" + }, + { + "value": 228, + "string": "ERR_TOO_FAST" + }, + { + "value": 229, + "string": "" + }, + { + "value": 230, + "string": "" + }, + { + "value": 231, + "string": "ERR_FLASH_FAILED" + }, + { + "value": 232, + "string": "" + }, + { + "value": 233, + "string": "" + }, + { + "value": 234, + "string": "" + }, + { + "value": 235, + "string": "" + }, + { + "value": 236, + "string": "" + }, + { + "value": 237, + "string": "" + }, + { + "value": 238, + "string": "" + }, + { + "value": 239, + "string": "ERR_COMM_LOST" + }, + { + "value": 240, + "string": "ERR_REF_ABORT_TO" + }, + { + "value": 241, + "string": "ERR_MOV_ABORT_TO" + }, + { + "value": 242, + "string": "ERR_NO_REF" + }, + { + "value": 243, + "string": "" + }, + { + "value": 244, + "string": "ERR_MOVE_BLOCKED" + }, + { + "value": 245, + "string": "ERR_UNKNOWN_HW" + }, + { + "value": 246, + "string": "ERR_BLOCK_FAILED" + }, + { + "value": 247, + "string": "ERR_NO_COMM" + }, + { + "value": 248, + "string": "ERR_WRONG_HW" + }, + { + "value": 249, + "string": "ERR_WRONG_MODULE" + }, + { + "value": 250, + "string": "ERR_SD_FAILED" + }, + { + "value": 251, + "string": "ERR_FLASH_LOST" + }, + { + "value": 252, + "string": "ERR_SW_COMM" + } + ], + "0x0500": [ + { + "value": 0, + "string": "EGI_40" + }, + { + "value": 1, + "string": "EGI_80" + }, + { + "value": 2, + "string": "EGL2_90" + }, + { + "value": 3, + "string": "UG4_DIO_80" + }, + { + "value": 4, + "string": "EGL_C" + }, + { + "value": 5, + "string": "EGH" + }, + { + "value": 6, + "string": "EGU_50_N_B" + }, + { + "value": 7, + "string": "EGU_60_N_B" + }, + { + "value": 8, + "string": "EGU_70_N_B" + }, + { + "value": 9, + "string": "EGU_80_N_B" + }, + { + "value": 10, + "string": "EGU_50_M_B" + }, + { + "value": 11, + "string": "EGU_60_M_B" + }, + { + "value": 12, + "string": "EGU_70_M_B" + }, + { + "value": 13, + "string": "EGU_80_M_B" + }, + { + "value": 14, + "string": "EGK_25_N_B" + }, + { + "value": 15, + "string": "EGK_40_N_B" + }, + { + "value": 16, + "string": "EGK_50_N_B" + }, + { + "value": 17, + "string": "EGK_25_M_B" + }, + { + "value": 18, + "string": "EGK_40_M_B" + }, + { + "value": 19, + "string": "EGK_50_M_B" + }, + { + "value": 20, + "string": "EGU_50_N_SD" + }, + { + "value": 21, + "string": "EGU_60_N_SD" + }, + { + "value": 22, + "string": "EGU_70_N_SD" + }, + { + "value": 23, + "string": "EGU_80_N_SD" + }, + { + "value": 24, + "string": "EGU_50_M_SD" + }, + { + "value": 25, + "string": "EGU_60_M_SD" + }, + { + "value": 26, + "string": "EGU_70_M_SD" + }, + { + "value": 27, + "string": "EGU_80_M_SD" + } + ] +} diff --git a/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py b/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py new file mode 100755 index 0000000..302d282 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import requests # type: ignore +import json +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument( + "ip", help="The IP address of the gripper on the network.", type=str +) +args = parser.parse_args() + +parameter_codes = "./system_parameter_codes" +parameters = "./data.json" + + +def read_parameter_codes(filepath: str) -> list[str]: + def contains_hex(line: str) -> bool: + return True if "0x" in line and line[0] != "#" else False + + with open(filepath, "r") as f: + lines = f.read().split("\n") + lines = list(filter(contains_hex, lines)) + return lines + + +def main(): + codes = read_parameter_codes(parameter_codes) + + values = {} + for code in codes: + print(f"reading code {code}") + try: + response = requests.get( + f"http://{args.ip}/adi/data.json?inst={int(code, 16)}&count=1" + ) + values[code] = response.json() + except Exception as e: + print(f"error reading code {code}: {e}") + + with open(parameters, "w") as f: + json.dump(values, f, indent=4) + + +if __name__ == "__main__": + main() diff --git a/schunk_egu_egk_gripper_dummy/config/system_parameter_codes b/schunk_egu_egk_gripper_dummy/config/system_parameter_codes new file mode 100644 index 0000000..446e853 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/config/system_parameter_codes @@ -0,0 +1,58 @@ +# See Commissioning Instructions, Firmware 5.2 EGU with EtherNet/IP interface +# Universal gripper, electric +# https://stb.cloud.schunk.com/media/IM0046706.PDF + + +0x0040 +0x0048 +0x0100 +0x0118 +0x0120 +0x0128 +0x0130 +0x0230 +0x0238 +0x0380 +0x03A8 +0x03B0 +0x03B8 +0x0500 +0x0528 +0x0540 +0x0580 +0x0588 +0x05A8 +0x0600 +0x0608 +0x0610 +0x0628 +0x0630 +0x0650 +0x0658 +0x0660 +0x06A8 +0x0800 +0x0808 +0x0810 +0x0818 +0x0820 +0x0828 +0x0840 +0x0870 +0x0878 +0x0880 +0x0888 +0x0890 +0x0898 +0x08A0 +0x08A8 +0x1000 +0x1008 +0x1020 +0x1100 +0x1108 +0x1110 +0x1118 +0x1138 +0x1330 +0x1400 From 99751f735b2f557451ee1c3883cb841e9b4d0b0e Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 17 Jul 2024 17:11:43 +0200 Subject: [PATCH 10/35] Use metadata to process `data.json` requests By keeping realistic metadata in the dummy, we can process _offset_-based requests and requests for several instances at the same time. Also implement enum requests and add more tests. --- schunk_egu_egk_gripper_dummy/config/README.md | 6 + .../config/metadata.json | 1679 +++++++++++++++++ .../schunk_egu_egk_gripper_dummy/main.py | 2 +- schunk_egu_egk_gripper_dummy/setup.py | 1 + schunk_egu_egk_gripper_dummy/src/dummy.py | 50 +- .../tests/test_dummy.py | 45 +- 6 files changed, 1772 insertions(+), 11 deletions(-) create mode 100644 schunk_egu_egk_gripper_dummy/config/metadata.json diff --git a/schunk_egu_egk_gripper_dummy/config/README.md b/schunk_egu_egk_gripper_dummy/config/README.md index dd0068f..186aab9 100644 --- a/schunk_egu_egk_gripper_dummy/config/README.md +++ b/schunk_egu_egk_gripper_dummy/config/README.md @@ -9,6 +9,12 @@ For instance, reading available error codes for the enum `HEX=0x0118` (`DEC=280` http:///adi/enum.json?inst=280 ``` +## Metadata + +```bash +http:///adi/metadata.json?offset=0&count=300 +``` + ## Data Use this script that reads the data directly from the gripper: ```bash diff --git a/schunk_egu_egk_gripper_dummy/config/metadata.json b/schunk_egu_egk_gripper_dummy/config/metadata.json new file mode 100644 index 0000000..78f0eb1 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/config/metadata.json @@ -0,0 +1,1679 @@ +[ + { + "instance": 64, + "name": "plc_sync_input", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 4, + "access": 9 + }, + { + "instance": 72, + "name": "plc_sync_output", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 4, + "access": 19 + }, + { + "instance": 256, + "name": "command_code", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 264, + "name": "system_state", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 272, + "name": "ctrl_authority", + "min": "00", + "max": "92", + "datatype": 8, + "numelements": 1, + "access": 3 + }, + { + "instance": 280, + "name": "err_code", + "min": "00", + "max": "FC", + "datatype": 8, + "numelements": 1, + "access": 1 + }, + { + "instance": 288, + "name": "wrn_code", + "min": "00", + "max": "FC", + "datatype": 8, + "numelements": 1, + "access": 1 + }, + { + "instance": 296, + "name": "sys_msg_req", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 304, + "name": "sys_msg_buffer", + "min": null, + "max": null, + "datatype": 7, + "numelements": 214, + "access": 1 + }, + { + "instance": 512, + "name": "set_pos", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 520, + "name": "set_vel", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 528, + "name": "used_cur_limit", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 536, + "name": "set_acc", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 544, + "name": "set_force", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 552, + "name": "grp_dir", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 560, + "name": "actual_pos", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 568, + "name": "actual_vel", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 576, + "name": "actual_cur", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 584, + "name": "actual_pos_mse", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 592, + "name": "grp_pos_lock", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 768, + "name": "ds_sin_ofs_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 776, + "name": "ds_cos_ofs_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 784, + "name": "ds_line_count", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 792, + "name": "ds_vel", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 800, + "name": "ds_angle", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 808, + "name": "pole_pairs", + "min": "0001", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 816, + "name": "mse_gear_ratio", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 824, + "name": "brk_lag_time", + "min": "0000", + "max": "01F4", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 832, + "name": "zv_angle", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 840, + "name": "ds_cur_filter_k1", + "min": "00000000", + "max": "3F800000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 856, + "name": "ds_pos_sens_inv", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 864, + "name": "ds_vel_ma_len", + "min": "3DCCCCCD", + "max": "40000000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 872, + "name": "zvs_ofs", + "min": "00000000", + "max": "40C90FDB", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 880, + "name": "zvs_volt_len", + "min": "00000000", + "max": "41C00000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 888, + "name": "brk_delay_time", + "min": "000A", + "max": "01F4", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 896, + "name": "grp_prehold_time", + "min": "0000", + "max": "EA60", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 904, + "name": "ofs_del_time", + "min": "000A", + "max": "01F4", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 912, + "name": "chp_ofs_on", + "min": "00000000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 920, + "name": "chp_ofs_off", + "min": "00000000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 928, + "name": "chp_mode", + "min": "00", + "max": "01", + "datatype": 8, + "numelements": 1, + "access": 3 + }, + { + "instance": 936, + "name": "dead_load_kg", + "min": "3DCCCCCD", + "max": "41700000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 944, + "name": "tool_cent_point", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 6, + "access": 3 + }, + { + "instance": 952, + "name": "cent_of_mass", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 6, + "access": 3 + }, + { + "instance": 960, + "name": "brk_release_time", + "min": "0000", + "max": "0708", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1024, + "name": "ctrl_cur_kr", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1032, + "name": "ctrl_cur_tn", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1040, + "name": "ctrl_cur_td", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1056, + "name": "ctrl_vel_kr", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1064, + "name": "ctrl_vel_tn", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1072, + "name": "ctrl_vel_td", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1088, + "name": "ctrl_pos_kr", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1096, + "name": "ctrl_pos_tn", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1104, + "name": "ctrl_pos_td", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1120, + "name": "inv_driving_dir", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 1128, + "name": "max_vector_len", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1136, + "name": "ctrl_ffwd_vel", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1280, + "name": "module_type", + "min": "00", + "max": "1B", + "datatype": 8, + "numelements": 1, + "access": 3 + }, + { + "instance": 1296, + "name": "set_vector_len", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1304, + "name": "set_vector_vel", + "min": "8000", + "max": "7FFF", + "datatype": 2, + "numelements": 1, + "access": 3 + }, + { + "instance": 1312, + "name": "mb_cur_threshold", + "min": "3C23D70A", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1320, + "name": "wp_lost_dst", + "min": "3DCCCCCD", + "max": "42480000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1328, + "name": "target_pos_win", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1336, + "name": "ctrl_extra_time", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1344, + "name": "wp_release_delta", + "min": "3F800000", + "max": "42480000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1352, + "name": "easing_time", + "min": "0000", + "max": "07D0", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1368, + "name": "ref_dst", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1376, + "name": "mot_const_kt", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1384, + "name": "grp_calib_param", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 4, + "access": 3 + }, + { + "instance": 1400, + "name": "mb_dst_threshold", + "min": "38D1B717", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1408, + "name": "grp_pos_margin", + "min": "3F800000", + "max": "41200000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1416, + "name": "max_phys_stroke", + "min": "3F800000", + "max": "43FA0000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1424, + "name": "ref_type", + "min": "00", + "max": "03", + "datatype": 8, + "numelements": 1, + "access": 3 + }, + { + "instance": 1432, + "name": "zvs_step_req", + "min": "00000000", + "max": "40C90FDB", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 1440, + "name": "zvs_step_done", + "min": "00000000", + "max": "40C90FDB", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 1448, + "name": "grp_prepos_delta", + "min": "3F800000", + "max": "42480000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1456, + "name": "mb_detect_time", + "min": "0050", + "max": "01F4", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1536, + "name": "min_pos", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1544, + "name": "max_pos", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1552, + "name": "zero_pos_ofs", + "min": "C61C4000", + "max": "461C4000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1560, + "name": "max_mot_cur", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1568, + "name": "nom_mot_cur", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1576, + "name": "min_vel", + "min": "3F800000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1584, + "name": "max_vel", + "min": "41700000", + "max": "42E60000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1592, + "name": "min_acc", + "min": "42C80000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1600, + "name": "max_acc", + "min": "437A0000", + "max": "447A0000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1616, + "name": "max_grp_vel", + "min": "3F800000", + "max": "42480000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1624, + "name": "min_grp_force", + "min": "41700000", + "max": "43160000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1632, + "name": "max_grp_force", + "min": "42DC0000", + "max": "43960000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1664, + "name": "ds_sin_min_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1672, + "name": "ds_sin_max_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1680, + "name": "ds_cos_min_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1688, + "name": "ds_cos_max_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 1696, + "name": "max_grp_force_sm", + "min": "00000000", + "max": "00000000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1704, + "name": "max_allow_force", + "min": "00000000", + "max": "00000000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 1712, + "name": "min_pos_base", + "min": "00000000", + "max": "42A60000", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 1720, + "name": "max_pos_base", + "min": "00000000", + "max": "42A60000", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 2048, + "name": "min_err_mot_volt", + "min": "41980000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2056, + "name": "max_err_mot_volt", + "min": "41980000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2064, + "name": "min_err_lgc_volt", + "min": "40A00000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2072, + "name": "max_err_lgc_volt", + "min": "40A00000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2080, + "name": "min_err_lgc_temp", + "min": "C1A00000", + "max": "42B20000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2088, + "name": "max_err_lgc_temp", + "min": "C1980000", + "max": "42B40000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2096, + "name": "min_err_mot_temp", + "min": "C1A00000", + "max": "42EE0000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2104, + "name": "max_err_mot_temp", + "min": "C1980000", + "max": "43480000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2112, + "name": "meas_lgc_temp", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 2120, + "name": "meas_mot_temp", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 2128, + "name": "min_rec_lgc_temp", + "min": "C2200000", + "max": "42F00000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2136, + "name": "max_rec_lgc_temp", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2144, + "name": "min_rec_mot_temp", + "min": "C2200000", + "max": "43480000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2152, + "name": "max_rec_mot_temp", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2160, + "name": "meas_lgc_volt", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 2168, + "name": "meas_mot_volt", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 2176, + "name": "min_wrn_mot_volt", + "min": "41980000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2184, + "name": "max_wrn_mot_volt", + "min": "41980000", + "max": "42100000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2192, + "name": "min_wrn_lgc_volt", + "min": "41300000", + "max": "42400000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2200, + "name": "max_wrn_lgc_volt", + "min": "41300000", + "max": "42400000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2208, + "name": "min_wrn_lgc_temp", + "min": "C1A00000", + "max": "42B20000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2216, + "name": "max_wrn_lgc_temp", + "min": "C1980000", + "max": "42B40000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2224, + "name": "min_wrn_mot_temp", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2232, + "name": "max_wrn_mot_temp", + "min": "C1980000", + "max": "42F00000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2240, + "name": "mot_resistor", + "min": "00000000", + "max": "447A0000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 2248, + "name": "mot_tp_volt_off", + "min": "C1C00000", + "max": "41C00000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 4096, + "name": "serial_no_txt", + "min": null, + "max": null, + "datatype": 7, + "numelements": 16, + "access": 3 + }, + { + "instance": 4104, + "name": "order_no_txt", + "min": null, + "max": null, + "datatype": 7, + "numelements": 16, + "access": 3 + }, + { + "instance": 4112, + "name": "production_date", + "min": null, + "max": null, + "datatype": 7, + "numelements": 16, + "access": 3 + }, + { + "instance": 4120, + "name": "processor_id", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 4128, + "name": "serial_no_num", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 3 + }, + { + "instance": 4352, + "name": "sw_build_date", + "min": null, + "max": null, + "datatype": 7, + "numelements": 12, + "access": 1 + }, + { + "instance": 4360, + "name": "sw_build_time", + "min": null, + "max": null, + "datatype": 7, + "numelements": 9, + "access": 1 + }, + { + "instance": 4368, + "name": "sw_version_num", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 1 + }, + { + "instance": 4376, + "name": "sw_version_txt", + "min": null, + "max": null, + "datatype": 7, + "numelements": 22, + "access": 1 + }, + { + "instance": 4384, + "name": "comm_version_txt", + "min": null, + "max": null, + "datatype": 7, + "numelements": 12, + "access": 1 + }, + { + "instance": 4392, + "name": "build_info", + "min": null, + "max": null, + "datatype": { + "error": 2 + }, + "numelements": 21, + "access": { + "error": 2 + } + }, + { + "instance": 4400, + "name": "fieldbus_type", + "min": "00", + "max": "07", + "datatype": 8, + "numelements": 1, + "access": 1 + }, + { + "instance": 4408, + "name": "mac_addr", + "min": "00", + "max": "FF", + "datatype": 4, + "numelements": 6, + "access": 1 + }, + { + "instance": 4528, + "name": "compat_mode", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 4608, + "name": "actual_user", + "min": "00", + "max": "03", + "datatype": 8, + "numelements": 1, + "access": 1 + }, + { + "instance": 4616, + "name": "login", + "min": null, + "max": null, + "datatype": 7, + "numelements": 8, + "access": 2 + }, + { + "instance": 4624, + "name": "vary_service_pwd", + "min": null, + "max": null, + "datatype": 7, + "numelements": 8, + "access": 2 + }, + { + "instance": 4632, + "name": "vary_root_pwd", + "min": null, + "max": null, + "datatype": 7, + "numelements": 8, + "access": 2 + }, + { + "instance": 4864, + "name": "start_fw_flash", + "min": "00", + "max": "05", + "datatype": 8, + "numelements": 1, + "access": 2 + }, + { + "instance": 4872, + "name": "firmware_path", + "min": null, + "max": null, + "datatype": 7, + "numelements": 30, + "access": 1 + }, + { + "instance": 4888, + "name": "ref_angle", + "min": "C0C90FDB", + "max": "40C90FDB", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 4896, + "name": "reset_reasons", + "min": "000000000000000000000000000000000000000000000000", + "max": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "datatype": { + "error": 2 + }, + "numelements": 6, + "access": { + "error": 2 + } + }, + { + "instance": 4904, + "name": "ref_pos", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 4912, + "name": "enable_softreset", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 4920, + "name": "ref_ofs", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 4928, + "name": "ref_vel", + "min": "3F800000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 4936, + "name": "ref_acc", + "min": "42C80000", + "max": "447A0000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 4992, + "name": "jog_acc", + "min": "42C80000", + "max": "447A0000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5112, + "name": "init_ended", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 1 + }, + { + "instance": 5120, + "name": "system_uptime", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 5128, + "name": "busy_h", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 5136, + "name": "ctrl_active_h", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 5144, + "name": "busy_h_s", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 1 + }, + { + "instance": 5152, + "name": "ctrl_active_h_s", + "min": "FF7FFFFF", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 1 + }, + { + "instance": 5160, + "name": "statistics", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 24, + "access": 1 + }, + { + "instance": 5168, + "name": "summed_dst", + "min": "0000000000000000", + "max": "FFFFFFFFFFFFFFFF", + "datatype": 17, + "numelements": 1, + "access": 1 + }, + { + "instance": 5176, + "name": "zv_hall_sect", + "min": "0000", + "max": "0008", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5192, + "name": "zvs_to_idx_ofs", + "min": "C0C90FDB", + "max": "40C90FDB", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5400, + "name": "pos_sens_sin_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 1 + }, + { + "instance": 5408, + "name": "pos_sens_cos_raw", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 1 + }, + { + "instance": 5416, + "name": "cur_u_comp", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5424, + "name": "cur_v_comp", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5432, + "name": "cur_w_comp", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5440, + "name": "max_angle_dev", + "min": "00000000", + "max": "40C9999A", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5448, + "name": "max_mse_chk_viol", + "min": "0000", + "max": "FFFE", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5456, + "name": "mse_ticks", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5464, + "name": "mse_diff", + "min": "3727C5AC", + "max": "3F7FFF58", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5472, + "name": "gse_zero_pos", + "min": "00000000", + "max": "00007FFF", + "datatype": 3, + "numelements": 1, + "access": 3 + }, + { + "instance": 5480, + "name": "gse_gear_ratio", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5496, + "name": "zero_pos_fix_dst", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5504, + "name": "gse_ticks", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5520, + "name": "idx_lost_limit", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5528, + "name": "idx_exs_limit", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5536, + "name": "gs_to_ms_lag", + "min": "00000000", + "max": "7F7FFFFF", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5632, + "name": "bt_cur", + "min": "3DCCCCCD", + "max": "402147AE", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 5640, + "name": "bt_duration", + "min": "01F4", + "max": "03E8", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 5648, + "name": "bt_result", + "min": "00", + "max": "01", + "datatype": 0, + "numelements": 1, + "access": 3 + }, + { + "instance": 5888, + "name": "hw_platform", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 5928, + "name": "internal_params", + "min": "00000000000000000000000000000000000000000000000000000000000000000000000000", + "max": "FFFFFFFFFFFFFFFF01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "datatype": { + "error": 2 + }, + "numelements": 11, + "access": { + "error": 2 + } + }, + { + "instance": 5936, + "name": "mvm_int", + "min": "00000000", + "max": "FFFFFFFF", + "datatype": 6, + "numelements": 1, + "access": 1 + }, + { + "instance": 5952, + "name": "mech_efficiency", + "min": "3727C5AC", + "max": "3F800000", + "datatype": 18, + "numelements": 1, + "access": 3 + }, + { + "instance": 8488, + "name": "dbg_msg_req", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 1, + "access": 3 + }, + { + "instance": 8496, + "name": "dbg_msg_buffer", + "min": "0000", + "max": "FFFF", + "datatype": 5, + "numelements": 128, + "access": 1 + } +] diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py index 393f546..5f913c2 100644 --- a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py @@ -34,4 +34,4 @@ async def put(msg: Message): @server.get("/adi/{path}") async def get(request: Request): - return dummy.process_get(request.path_params["path"], request.query_params) + return dummy.get(request.path_params["path"], request.query_params) diff --git a/schunk_egu_egk_gripper_dummy/setup.py b/schunk_egu_egk_gripper_dummy/setup.py index 4e52814..1614c37 100644 --- a/schunk_egu_egk_gripper_dummy/setup.py +++ b/schunk_egu_egk_gripper_dummy/setup.py @@ -13,6 +13,7 @@ ("share/" + package_name, ["package.xml"]), (os.path.join("share", package_name), [package_name + "/main.py"]), (os.path.join("share", package_name, "src"), glob("src/*.py")), + (os.path.join("share", package_name, "config"), glob("config/*.json")), ], install_requires=["setuptools"], zip_safe=True, diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index c84844b..07d9ac6 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -1,6 +1,8 @@ from threading import Thread import time -from urllib.parse import parse_qs +import os +from pathlib import Path +import json class Dummy(object): @@ -8,6 +10,25 @@ def __init__(self): self.thread = Thread(target=self._run) self.running = False self.done = False + self.enum = None + self.metadata = None + self.data = None + + enum_config = os.path.join( + Path(__file__).resolve().parents[1], "config/enum.json" + ) + metadata_config = os.path.join( + Path(__file__).resolve().parents[1], "config/metadata.json" + ) + data_config = os.path.join( + Path(__file__).resolve().parents[1], "config/data.json" + ) + with open(enum_config, "r") as f: + self.enum = json.load(f) + with open(metadata_config, "r") as f: + self.metadata = json.load(f) + with open(data_config, "r") as f: + self.data = json.load(f) def start(self) -> None: if self.running: @@ -25,15 +46,34 @@ def _run(self) -> None: time.sleep(1) print("Done") - def process_get(self, path: str, query: dict[str, list[str]]) -> dict | list | None: + def get(self, path: str, query: dict[str, str]) -> dict | list | None: print(f"path: {path}") - query = parse_qs(str(query)) print(f"query: {query}") if path == "info.json": return {"dataformat": 0} # 0: Little endian, 1: Big endian + if path == "enum.json": - return [] + inst = query["inst"] + value = int(query["value"]) + if inst in self.enum: + string = self.enum[inst][value]["string"] + return [{"string": string, "value": value}] + else: + return [] + if path == "data.json": - return [] + result: list = [] + if "offset" in query and "count" in query: + offset = int(query["offset"]) + count = int(query["count"]) + if offset < 0 or count < 0: + return result + if offset + count >= len(self.metadata): + return result + for i in range(count): + result.append(self.metadata[offset + i]) + return result + else: + return [] return None diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index 0523fb7..9bd26ea 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -21,25 +21,60 @@ def test_dummy_survives_repeated_starts_and_stops(): assert not dummy.running +def test_dummy_reads_configuration_on_startup(): + dummy = Dummy() + assert dummy.enum is not None + assert dummy.data is not None + assert dummy.metadata is not None + + def test_dummy_responds_correctly_to_info_requests(): dummy = Dummy() path = "info.json" query = "" expected = {"dataformat": 0} - assert dummy.process_get(path, query) == expected + assert dummy.get(path, query) == expected def test_dummy_responds_correctly_to_enum_requests(): dummy = Dummy() path = "enum.json" - query = "" + inst = "0x0118" + value = 0 + query = {"inst": inst, "value": value} + expected = [dummy.enum[inst][value]] + assert dummy.get(path, query) == expected + + +def test_dummy_survives_invalid_enum_requests(): + dummy = Dummy() + path = "enum.json" + invalid_inst = "0x0" + query = {"inst": invalid_inst, "value": 0} expected = [] - assert dummy.process_get(path, query) == expected + assert dummy.get(path, query) == expected def test_dummy_responds_correctly_to_data_requests(): dummy = Dummy() path = "data.json" - query = "" + query = {"offset": 15, "count": 3} + expected = [dummy.metadata[15], dummy.metadata[16], dummy.metadata[17]] + assert dummy.get(path, query) == expected + + +def test_dummy_survives_invalid_data_requests(): + dummy = Dummy() + path = "data.json" + query = {"offset": 1000, "count": "2"} + expected = [] + assert dummy.get(path, query) == expected + query = {"offset": 100, "count": "90"} + expected = [] + assert dummy.get(path, query) == expected + query = {"offset": 1000, "count": "-1"} + expected = [] + assert dummy.get(path, query) == expected + query = {"offset": -1, "count": "1000"} expected = [] - assert dummy.process_get(path, query) == expected + assert dummy.get(path, query) == expected From 7d6f2d9b0f8be0cb3ab5a72f18ed59bb62b97cc4 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Thu, 18 Jul 2024 15:29:55 +0200 Subject: [PATCH 11/35] Implement instance-based `data.jon` get requests Also split the processing of GET requests in the server depending on the path. This makes the individual functions smaller and better to read. --- .../schunk_egu_egk_gripper_dummy/main.py | 10 +++- schunk_egu_egk_gripper_dummy/src/dummy.py | 58 ++++++++++--------- .../tests/test_dummy.py | 37 +++++++----- 3 files changed, 63 insertions(+), 42 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py index 5f913c2..0692ccc 100644 --- a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py @@ -34,4 +34,12 @@ async def put(msg: Message): @server.get("/adi/{path}") async def get(request: Request): - return dummy.get(request.path_params["path"], request.query_params) + path = request.path_params["path"] + params = request.query_params + if path == "info.json": + return dummy.get_info(params) + if path == "enum.json": + return dummy.get_enum(params) + if path == "data.json": + return dummy.get_data(params) + return None diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 07d9ac6..73948ef 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -46,34 +46,38 @@ def _run(self) -> None: time.sleep(1) print("Done") - def get(self, path: str, query: dict[str, str]) -> dict | list | None: - print(f"path: {path}") - print(f"query: {query}") + def get_info(self, query: dict[str, str]) -> dict: + return {"dataformat": 0} # 0: Little endian, 1: Big endian - if path == "info.json": - return {"dataformat": 0} # 0: Little endian, 1: Big endian + def get_enum(self, query: dict[str, str]) -> list: + inst = query["inst"] + value = int(query["value"]) + if inst in self.enum: + string = self.enum[inst][value]["string"] + return [{"string": string, "value": value}] + else: + return [] - if path == "enum.json": - inst = query["inst"] - value = int(query["value"]) - if inst in self.enum: - string = self.enum[inst][value]["string"] - return [{"string": string, "value": value}] - else: - return [] + def get_data(self, query: dict[str, str]) -> list: + result: list = [] + if "offset" in query and "count" in query: + offset = int(query["offset"]) + count = int(query["count"]) + if offset < 0 or count < 0: + return result + if offset + count >= len(self.metadata): + return result + for i in range(count): + result.append(self.metadata[offset + i]) + return result - if path == "data.json": - result: list = [] - if "offset" in query and "count" in query: - offset = int(query["offset"]) - count = int(query["count"]) - if offset < 0 or count < 0: - return result - if offset + count >= len(self.metadata): - return result - for i in range(count): - result.append(self.metadata[offset + i]) + if "inst" in query and "count" in query: + inst = query["inst"] + count = int(query["count"]) + if count != 1: + return result + if inst not in self.data: return result - else: - return [] - return None + return self.data[inst] + else: + return [] diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index 9bd26ea..747b44a 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -30,51 +30,60 @@ def test_dummy_reads_configuration_on_startup(): def test_dummy_responds_correctly_to_info_requests(): dummy = Dummy() - path = "info.json" query = "" expected = {"dataformat": 0} - assert dummy.get(path, query) == expected + assert dummy.get_info(query) == expected def test_dummy_responds_correctly_to_enum_requests(): dummy = Dummy() - path = "enum.json" inst = "0x0118" value = 0 query = {"inst": inst, "value": value} expected = [dummy.enum[inst][value]] - assert dummy.get(path, query) == expected + assert dummy.get_enum(query) == expected def test_dummy_survives_invalid_enum_requests(): dummy = Dummy() - path = "enum.json" invalid_inst = "0x0" query = {"inst": invalid_inst, "value": 0} expected = [] - assert dummy.get(path, query) == expected + assert dummy.get_enum(query) == expected -def test_dummy_responds_correctly_to_data_requests(): +def test_dummy_responds_correctly_to_data_offset_requests(): dummy = Dummy() - path = "data.json" query = {"offset": 15, "count": 3} expected = [dummy.metadata[15], dummy.metadata[16], dummy.metadata[17]] - assert dummy.get(path, query) == expected + assert dummy.get_data(query) == expected + + +def test_dummy_responds_correctly_to_data_instance_requests(): + dummy = Dummy() + inst = "0x0040" + query = {"inst": inst, "count": 1} + expected = dummy.data[inst] + assert dummy.get_data(query) == expected def test_dummy_survives_invalid_data_requests(): dummy = Dummy() - path = "data.json" query = {"offset": 1000, "count": "2"} expected = [] - assert dummy.get(path, query) == expected + assert dummy.get_data(query) == expected query = {"offset": 100, "count": "90"} expected = [] - assert dummy.get(path, query) == expected + assert dummy.get_data(query) == expected query = {"offset": 1000, "count": "-1"} expected = [] - assert dummy.get(path, query) == expected + assert dummy.get_data(query) == expected query = {"offset": -1, "count": "1000"} expected = [] - assert dummy.get(path, query) == expected + assert dummy.get_data(query) == expected + query = {"inst": "0x0040", "count": "0"} + expected = [] + assert dummy.get_data(query) == expected + query = {"inst": "0x0040", "count": "2"} + expected = [] + assert dummy.get_data(query) == expected From 91aa53db5456eb42c0b75f9e29fee4817caae52a Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 19 Jul 2024 10:16:37 +0200 Subject: [PATCH 12/35] Add additional tests for the webserver's startup --- schunk_egu_egk_gripper_dummy/README.md | 2 +- .../schunk_egu_egk_gripper_dummy/main.py | 1 - schunk_egu_egk_gripper_dummy/src/dummy.py | 2 ++ .../tests/test_webserver.py | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 schunk_egu_egk_gripper_dummy/tests/test_webserver.py diff --git a/schunk_egu_egk_gripper_dummy/README.md b/schunk_egu_egk_gripper_dummy/README.md index f799ea8..d77e51f 100644 --- a/schunk_egu_egk_gripper_dummy/README.md +++ b/schunk_egu_egk_gripper_dummy/README.md @@ -16,7 +16,7 @@ pip install fastapi uvicorn ## Run tests locally ```bash -pip install pytest coverage +pip install pytest httpx coverage ``` ```bash diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py index 0692ccc..ea8f176 100644 --- a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py @@ -7,7 +7,6 @@ # Components dummy = Dummy() -dummy.start() server = FastAPI() client = ["http://localhost:8001"] diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 73948ef..3536681 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -50,6 +50,8 @@ def get_info(self, query: dict[str, str]) -> dict: return {"dataformat": 0} # 0: Little endian, 1: Big endian def get_enum(self, query: dict[str, str]) -> list: + if "inst" not in query or "value" not in query: + return [] inst = query["inst"] value = int(query["value"]) if inst in self.enum: diff --git a/schunk_egu_egk_gripper_dummy/tests/test_webserver.py b/schunk_egu_egk_gripper_dummy/tests/test_webserver.py new file mode 100644 index 0000000..2c05bfe --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/tests/test_webserver.py @@ -0,0 +1,17 @@ +from schunk_egu_egk_gripper_dummy.main import server +from fastapi.testclient import TestClient + + +def test_info_route_is_available(): + client = TestClient(server) + assert client.get("/adi/info.json").is_success + + +def test_enum_route_is_available(): + client = TestClient(server) + assert client.get("/adi/enum.json").is_success + + +def test_data_route_is_available(): + client = TestClient(server) + assert client.get("/adi/data.json").is_success From 35984a6e6b8cebba7cd3ca861d1feb5c7b8bcb50 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 19 Jul 2024 11:20:44 +0200 Subject: [PATCH 13/35] Update system parameters Also support comments in the `system_parameter_codes` file when reading system parameters from hardware. --- .flake8 | 3 ++ schunk_egu_egk_gripper_dummy/config/data.json | 23 ++++++++---- schunk_egu_egk_gripper_dummy/config/enum.json | 36 ++++++++++++++++++- .../config/read_system_parameters.py | 7 ++++ .../config/system_parameter_codes | 9 +++-- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/.flake8 b/.flake8 index f578405..f40261f 100644 --- a/.flake8 +++ b/.flake8 @@ -2,3 +2,6 @@ [flake8] # Use black's line length (default 88) instead of the flake8 default of 79: max-line-length = 88 + +per-file-ignores = + schunk_egu_egk_gripper_dummy/*.py:E203 diff --git a/schunk_egu_egk_gripper_dummy/config/data.json b/schunk_egu_egk_gripper_dummy/config/data.json index f34d7a9..6f6eabe 100644 --- a/schunk_egu_egk_gripper_dummy/config/data.json +++ b/schunk_egu_egk_gripper_dummy/config/data.json @@ -1,6 +1,6 @@ { "0x0040": [ - "210000800000A2800000000000000000" + "800000800000A25800000000000000EF" ], "0x0048": [ "05000000000000000000000000000000" @@ -9,7 +9,7 @@ "0000" ], "0x0118": [ - "00" + "EF" ], "0x0120": [ "00" @@ -18,10 +18,13 @@ "001F" ], "0x0130": [ - "3238333A34303A3534203C307837323E206C6F67696320766F6C746167652031362E343336203C2031392E3230302056202876616C75655F6D6F6E69746F722E6370703A3A32312900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "3238343A32363A3532203C307846343E206D6F766520626C6F636B6564206F6363757272656420696E2063757272656E74207374617465203420286170706C69636174696F6E5F73746174655F6D616368696E652E683A3A313334392900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "0x0210": [ + "00000000" ], "0x0230": [ - "4226667D" + "42263D87" ], "0x0238": [ "00000000" @@ -102,10 +105,10 @@ "42AA0000" ], "0x0840": [ - "41F5DC60" + "421EAE6D" ], "0x0870": [ - "41BA7D40" + "41BA5871" ], "0x0878": [ "41C04897" @@ -149,6 +152,12 @@ "0x1118": [ "352E322E302E38313839360000000000000000000000" ], + "0x1120": [ + "322E312E3100000000000000" + ], + "0x1130": [ + "01" + ], "0x1138": [ "0030114844C7" ], @@ -156,6 +165,6 @@ "01" ], "0x1400": [ - "00000080" + "00000A69" ] } diff --git a/schunk_egu_egk_gripper_dummy/config/enum.json b/schunk_egu_egk_gripper_dummy/config/enum.json index faa406e..8fe8663 100644 --- a/schunk_egu_egk_gripper_dummy/config/enum.json +++ b/schunk_egu_egk_gripper_dummy/config/enum.json @@ -1013,7 +1013,7 @@ "string": "ERR_SW_COMM" } ], - "0x120": [ + "0x0120": [ { "value": 0, "string": "ERR_NONE" @@ -2140,5 +2140,39 @@ "value": 27, "string": "EGU_80_M_SD" } + ], + "0x1130": [ + { + "value": 0, + "string": "UNKNOWN" + }, + { + "value": 1, + "string": "PROFINET" + }, + { + "value": 2, + "string": "EtherNet/IP (TM)" + }, + { + "value": 3, + "string": "EtherCAT" + }, + { + "value": 4, + "string": "Modbus TCP" + }, + { + "value": 5, + "string": "Common Ethernet" + }, + { + "value": 6, + "string": "IOLink" + }, + { + "value": 7, + "string": "Modbus RTU" + } ] } diff --git a/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py b/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py index 302d282..dd73ef4 100755 --- a/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py +++ b/schunk_egu_egk_gripper_dummy/config/read_system_parameters.py @@ -17,9 +17,16 @@ def read_parameter_codes(filepath: str) -> list[str]: def contains_hex(line: str) -> bool: return True if "0x" in line and line[0] != "#" else False + def remove_comments(line: str) -> str: + if line.find("#") != -1: + line = line[0 : line.find("#")] + line = line.strip() + return line + with open(filepath, "r") as f: lines = f.read().split("\n") lines = list(filter(contains_hex, lines)) + lines = list(map(remove_comments, lines)) return lines diff --git a/schunk_egu_egk_gripper_dummy/config/system_parameter_codes b/schunk_egu_egk_gripper_dummy/config/system_parameter_codes index 446e853..0b38c21 100644 --- a/schunk_egu_egk_gripper_dummy/config/system_parameter_codes +++ b/schunk_egu_egk_gripper_dummy/config/system_parameter_codes @@ -6,17 +6,18 @@ 0x0040 0x0048 0x0100 -0x0118 -0x0120 +0x0118 # enum +0x0120 # enum 0x0128 0x0130 +0x0210 0x0230 0x0238 0x0380 0x03A8 0x03B0 0x03B8 -0x0500 +0x0500 # enum 0x0528 0x0540 0x0580 @@ -53,6 +54,8 @@ 0x1108 0x1110 0x1118 +0x1120 +0x1130 # enum 0x1138 0x1330 0x1400 From 1e7db9f5eed72900e6b637618a574aae0806bab1 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 19 Jul 2024 18:02:33 +0200 Subject: [PATCH 14/35] Implement the skeleton for the post request --- .../schunk_egu_egk_gripper_dummy/main.py | 23 ++++++++++++------- schunk_egu_egk_gripper_dummy/src/dummy.py | 3 +++ .../tests/test_dummy.py | 8 +++++++ .../tests/test_webserver.py | 6 +++++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py index ea8f176..857964f 100644 --- a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py @@ -1,6 +1,6 @@ from src.dummy import Dummy -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Form from fastapi.middleware.cors import CORSMiddleware from typing import Optional from pydantic import BaseModel @@ -20,15 +20,22 @@ ) -class Message(BaseModel): - message: str - optional: Optional[str] = None +class Update(BaseModel): + inst: str + value: str + elem: Optional[int] = None + callback: Optional[str] = None -@server.put("/") -async def put(msg: Message): - print(msg) - return True +@server.post("/adi/update.json") +async def post( + inst: str = Form(...), + value: str = Form(...), + elem: Optional[int] = Form(None), + callback: Optional[str] = Form(None), +): + msg = Update(inst=inst, value=value, elem=elem, callback=callback) + return dummy.post(msg) @server.get("/adi/{path}") diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 3536681..42216d6 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -46,6 +46,9 @@ def _run(self) -> None: time.sleep(1) print("Done") + def post(self, msg: dict) -> dict: + return {"result": 0} + def get_info(self, query: dict[str, str]) -> dict: return {"dataformat": 0} # 0: Little endian, 1: Big endian diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index 747b44a..e37b4e8 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -87,3 +87,11 @@ def test_dummy_survives_invalid_data_requests(): query = {"inst": "0x0040", "count": "2"} expected = [] assert dummy.get_data(query) == expected + + +def test_dummy_responds_correctly_to_post_requests(): + dummy = Dummy() + inst = "0x0048" + data = {"inst": inst, "value": "01"} + expected = {"result": 0} + assert dummy.post(data) == expected diff --git a/schunk_egu_egk_gripper_dummy/tests/test_webserver.py b/schunk_egu_egk_gripper_dummy/tests/test_webserver.py index 2c05bfe..91444c1 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_webserver.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_webserver.py @@ -15,3 +15,9 @@ def test_enum_route_is_available(): def test_data_route_is_available(): client = TestClient(server) assert client.get("/adi/data.json").is_success + + +def test_update_route_is_available(): + client = TestClient(server) + data = {"inst": 0, "value": "0"} + assert client.post("/adi/update.json", data=data).is_success From d6fd46a4225cd0ef9b39406085a7d36447916d40 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 22 Jul 2024 07:33:12 +0200 Subject: [PATCH 15/35] Update joint states topic in connection test We'll later need something less hard-coded here. --- schunk_egu_egk_gripper_tests/test/test_http_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schunk_egu_egk_gripper_tests/test/test_http_interface.py b/schunk_egu_egk_gripper_tests/test/test_http_interface.py index 783c313..5c7bd5f 100644 --- a/schunk_egu_egk_gripper_tests/test/test_http_interface.py +++ b/schunk_egu_egk_gripper_tests/test/test_http_interface.py @@ -26,4 +26,4 @@ def test_driver_connnects_to_gripper_dummy(launch_context, isolated, gripper_dum timeout = 3 time.sleep(until_dummy_ready) - assert CheckTopic("/joint_states", JointState).event.wait(timeout) + assert CheckTopic("/EGK_50_M_B/joint_states", JointState).event.wait(timeout) From 83adb99279b46af28627a45f9b66f1676f130954 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 22 Jul 2024 07:47:51 +0200 Subject: [PATCH 16/35] Add the dummy's dependencies to CI --- .github/script/install_dependencies.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/script/install_dependencies.sh b/.github/script/install_dependencies.sh index 5b7b55b..7c5e6f9 100755 --- a/.github/script/install_dependencies.sh +++ b/.github/script/install_dependencies.sh @@ -1,3 +1,4 @@ #/usr/bin/bash cd $HOME apt-get install curl libcurl4-openssl-dev +apt-get install python3-fastapi python3-uvicorn From 43af5b53371943ab821dfde46a98c0687c4379f2 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 22 Jul 2024 11:11:34 +0200 Subject: [PATCH 17/35] Test whether the driver advertises all ROS2 interfaces --- .../launch/schunk.launch.py | 2 +- .../test/test_http_interface.py | 2 +- .../test/test_ros_interfaces.py | 59 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 schunk_egu_egk_gripper_tests/test/test_ros_interfaces.py diff --git a/schunk_egu_egk_gripper_driver/launch/schunk.launch.py b/schunk_egu_egk_gripper_driver/launch/schunk.launch.py index 4c30518..cb17c84 100644 --- a/schunk_egu_egk_gripper_driver/launch/schunk.launch.py +++ b/schunk_egu_egk_gripper_driver/launch/schunk.launch.py @@ -49,7 +49,7 @@ def generate_launch_description(): package="schunk_egu_egk_gripper_driver", plugin="SchunkGripperNode", name="schunk_gripper_driver", - namespace="EGK_50_M_B", + namespace="", parameters=[ {"IP": LaunchConfiguration("IP")}, {"port": LaunchConfiguration("port")}, diff --git a/schunk_egu_egk_gripper_tests/test/test_http_interface.py b/schunk_egu_egk_gripper_tests/test/test_http_interface.py index 5c7bd5f..783c313 100644 --- a/schunk_egu_egk_gripper_tests/test/test_http_interface.py +++ b/schunk_egu_egk_gripper_tests/test/test_http_interface.py @@ -26,4 +26,4 @@ def test_driver_connnects_to_gripper_dummy(launch_context, isolated, gripper_dum timeout = 3 time.sleep(until_dummy_ready) - assert CheckTopic("/EGK_50_M_B/joint_states", JointState).event.wait(timeout) + assert CheckTopic("/joint_states", JointState).event.wait(timeout) diff --git a/schunk_egu_egk_gripper_tests/test/test_ros_interfaces.py b/schunk_egu_egk_gripper_tests/test/test_ros_interfaces.py new file mode 100644 index 0000000..48651fe --- /dev/null +++ b/schunk_egu_egk_gripper_tests/test/test_ros_interfaces.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import pytest +from rclpy.node import Node +import time +from test.conftest import launch_description + + +def check_each_in(elements: list, node_method: str) -> None: + node = Node("test") + until_ready = 2.0 # sec + time.sleep(until_ready) + existing = getattr(node, node_method)() + advertised = [i[0] for i in existing] + for element in elements: + assert element in advertised + + +@pytest.mark.launch(fixture=launch_description) +def test_driver_advertices_all_relevant_topics(launch_context, isolated, gripper_dummy): + topic_list = [ + "/diagnostics", + "/joint_states", + "/state", + ] + check_each_in(topic_list, "get_topic_names_and_types") + + +@pytest.mark.launch(fixture=launch_description) +def test_driver_advertices_all_relevant_services( + launch_context, isolated, gripper_dummy +): + service_list = [ + "/acknowledge", + "/brake_test", + "/fast_stop", + "/gripper_info", + "/prepare_for_shutdown", + "/reconnect", + "/release_for_manual_movement", + "/softreset", + "/stop", + ] + check_each_in(service_list, "get_service_names_and_types") + + +@pytest.mark.launch(fixture=launch_description) +def test_driver_advertices_all_relevant_actions( + launch_context, isolated, gripper_dummy +): + action_list = [ + "/grip", + "/grip_with_position", + "/gripper_control", + "/move_to_absolute_position", + "/move_to_relative_position", + "/release_workpiece", + ] + action_list = [a + "/_action/status" for a in action_list] + check_each_in(action_list, "get_topic_names_and_types") From d6657cf2fcb50ee90186b6ee67f3ca629b8863e6 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 24 Jul 2024 06:37:13 +0200 Subject: [PATCH 18/35] Fix the installation of dependencies in the CI --- .github/script/install_dependencies.sh | 19 +++++++++++++++++-- schunk_egu_egk_gripper_dummy/README.md | 4 ++-- schunk_egu_egk_gripper_tests/package.xml | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/script/install_dependencies.sh b/.github/script/install_dependencies.sh index 7c5e6f9..4e83ff2 100755 --- a/.github/script/install_dependencies.sh +++ b/.github/script/install_dependencies.sh @@ -1,4 +1,19 @@ #/usr/bin/bash cd $HOME -apt-get install curl libcurl4-openssl-dev -apt-get install python3-fastapi python3-uvicorn +apt-get install -y curl libcurl4-openssl-dev + +# Python dependencies +python_deps="fastapi uvicorn httpx requests coverage" +os_name=$(lsb_release -cs) + +case $os_name in + jammy) # Ubuntu 22.04 + pip install --user $python_deps + ;; + kinetic) # Ubuntu 24.04 + pip install --break-system-packages $python_deps + ;; + *) # Newer + pip install --break-system-packages $python_deps + ;; +esac diff --git a/schunk_egu_egk_gripper_dummy/README.md b/schunk_egu_egk_gripper_dummy/README.md index d77e51f..66e8d24 100644 --- a/schunk_egu_egk_gripper_dummy/README.md +++ b/schunk_egu_egk_gripper_dummy/README.md @@ -4,7 +4,7 @@ A minimalist protocol simulator for system tests. ## Dependencies ```bash -pip install fastapi uvicorn +pip install --user fastapi uvicorn ``` ## Getting started @@ -16,7 +16,7 @@ pip install fastapi uvicorn ## Run tests locally ```bash -pip install pytest httpx coverage +pip install --user pytest httpx coverage ``` ```bash diff --git a/schunk_egu_egk_gripper_tests/package.xml b/schunk_egu_egk_gripper_tests/package.xml index 98191f3..b04fa99 100644 --- a/schunk_egu_egk_gripper_tests/package.xml +++ b/schunk_egu_egk_gripper_tests/package.xml @@ -12,6 +12,7 @@ ament_lint_auto schunk_egu_egk_gripper_driver + schunk_egu_egk_gripper_dummy launch launch_ros launch_pytest From ffbdebffd20b3701edd08dbd493ce2b9341671fb Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Thu, 25 Jul 2024 17:43:55 +0200 Subject: [PATCH 19/35] Rename the acknowledge service's result variable `Success` is more common and more concise. --- .../src/schunk_gripper_wrapper.cpp | 6 +++--- schunk_egu_egk_gripper_examples/src/gripper_example.cpp | 4 ++-- schunk_egu_egk_gripper_interfaces/srv/Acknowledge.srv | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp b/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp index 44e59ee..5758742 100644 --- a/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp +++ b/schunk_egu_egk_gripper_driver/src/schunk_gripper_wrapper.cpp @@ -1153,12 +1153,12 @@ void SchunkGripperNode::acknowledge_srv(const std::shared_ptracknowledged = true; + res->success = true; RCLCPP_WARN(this->get_logger(),"Acknowledged"); } else { - res->acknowledged = false; + res->success = false; RCLCPP_WARN(this->get_logger(),"Acknowledge failed!"); } } @@ -1166,7 +1166,7 @@ void SchunkGripperNode::acknowledge_srv(const std::shared_ptrget_logger(), "Failed Connection! %s", connection_error.c_str()); - res->acknowledged = false; + res->success = false; RCLCPP_WARN(this->get_logger(), "Acknowledge failed!"); } last_command = 0; diff --git a/schunk_egu_egk_gripper_examples/src/gripper_example.cpp b/schunk_egu_egk_gripper_examples/src/gripper_example.cpp index 1b31f82..2aea3cc 100644 --- a/schunk_egu_egk_gripper_examples/src/gripper_example.cpp +++ b/schunk_egu_egk_gripper_examples/src/gripper_example.cpp @@ -280,7 +280,7 @@ void acknowledge(rclcpp::Client::SharedPtr acknowledge_client) RCLCPP_INFO(rclcpp::get_logger("schunk_gripper_example"), "Call acknowledge-server."); //To end the error: acknowledge - bool acknowledged = acknowledge_client->async_send_request(acknowledge_srv).get()->acknowledged; + bool acknowledged = acknowledge_client->async_send_request(acknowledge_srv).get()->success; RCLCPP_INFO(rclcpp::get_logger("schunk_gripper_example"), "%s", acknowledged ? "Acknowledged" : "Not acknowledged"); } @@ -548,7 +548,7 @@ int main(int argc, char** argv) if(diagnostic_msg.status[0].level == diagnostic_msgs::msg::DiagnosticStatus::ERROR) { auto response = acknowledge_client->async_send_request(acknowledge_req); - if(response.get()->acknowledged) + if(response.get()->success) RCLCPP_INFO(node->get_logger(), "AN ERROR WAS ACKNOWLEDGED!"); } diff --git a/schunk_egu_egk_gripper_interfaces/srv/Acknowledge.srv b/schunk_egu_egk_gripper_interfaces/srv/Acknowledge.srv index 4c2d7d7..410e0f9 100644 --- a/schunk_egu_egk_gripper_interfaces/srv/Acknowledge.srv +++ b/schunk_egu_egk_gripper_interfaces/srv/Acknowledge.srv @@ -1,2 +1,2 @@ --- -bool acknowledged +bool success From 1643d00f8d638b13c056d4cd3d1895bc4acb99a8 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 26 Jul 2024 08:00:08 +0200 Subject: [PATCH 20/35] Move test helpers into a separate module --- schunk_egu_egk_gripper_tests/test/helpers.py | 16 ++++++++++++++++ .../test/test_http_interface.py | 17 +---------------- 2 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 schunk_egu_egk_gripper_tests/test/helpers.py diff --git a/schunk_egu_egk_gripper_tests/test/helpers.py b/schunk_egu_egk_gripper_tests/test/helpers.py new file mode 100644 index 0000000..7e84c50 --- /dev/null +++ b/schunk_egu_egk_gripper_tests/test/helpers.py @@ -0,0 +1,16 @@ +from threading import Thread, Event +import rclpy +from rclpy.node import Node +from typing import Any + + +class CheckTopic(Node): + def __init__(self, topic: str, type: Any): + super().__init__("check_topic") + self.event = Event() + self.sub = self.create_subscription(type, topic, self.msg_cb, 3) + self.thread = Thread(target=lambda node: rclpy.spin(node), args=(self,)) + self.thread.start() + + def msg_cb(self, data: Any) -> None: + self.event.set() diff --git a/schunk_egu_egk_gripper_tests/test/test_http_interface.py b/schunk_egu_egk_gripper_tests/test/test_http_interface.py index 783c313..e3a1562 100644 --- a/schunk_egu_egk_gripper_tests/test/test_http_interface.py +++ b/schunk_egu_egk_gripper_tests/test/test_http_interface.py @@ -1,23 +1,8 @@ import pytest from test.conftest import launch_description import time -from threading import Thread, Event -import rclpy -from rclpy.node import Node from sensor_msgs.msg import JointState -from typing import Any - - -class CheckTopic(Node): - def __init__(self, topic: str, type: Any): - super().__init__("check_topic") - self.event = Event() - self.sub = self.create_subscription(type, topic, self.msg_cb, 3) - self.thread = Thread(target=lambda node: rclpy.spin(node), args=(self,)) - self.thread.start() - - def msg_cb(self, data: Any) -> None: - self.event.set() +from test.helpers import CheckTopic @pytest.mark.launch(fixture=launch_description) From 3e05f2cc9c30f85c28845050ba47d73b3207ea60 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 26 Jul 2024 09:16:49 +0200 Subject: [PATCH 21/35] Add a test for the driver's start state Also use a convenience method for getting specific driver state variables by topic. --- schunk_egu_egk_gripper_tests/test/helpers.py | 15 +++++++++++++-- .../test/test_functionality.py | 8 ++++++++ .../test/test_http_interface.py | 4 ++-- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 schunk_egu_egk_gripper_tests/test/test_functionality.py diff --git a/schunk_egu_egk_gripper_tests/test/helpers.py b/schunk_egu_egk_gripper_tests/test/helpers.py index 7e84c50..966c404 100644 --- a/schunk_egu_egk_gripper_tests/test/helpers.py +++ b/schunk_egu_egk_gripper_tests/test/helpers.py @@ -2,15 +2,26 @@ import rclpy from rclpy.node import Node from typing import Any +from schunk_egu_egk_gripper_interfaces.msg import State # type: ignore[attr-defined] +import uuid -class CheckTopic(Node): +class TopicGetsPublished(Node): def __init__(self, topic: str, type: Any): - super().__init__("check_topic") + node_name = "check_topic" + str(uuid.uuid4()).replace("-", "") + super().__init__(node_name) self.event = Event() + self.data = None self.sub = self.create_subscription(type, topic, self.msg_cb, 3) self.thread = Thread(target=lambda node: rclpy.spin(node), args=(self,)) self.thread.start() def msg_cb(self, data: Any) -> None: + self.data = data self.event.set() + + +def get_current_state(variable: str): + topic = TopicGetsPublished("/state", State) + topic.event.wait() + return getattr(topic.data, variable) diff --git a/schunk_egu_egk_gripper_tests/test/test_functionality.py b/schunk_egu_egk_gripper_tests/test/test_functionality.py new file mode 100644 index 0000000..2d8fb7a --- /dev/null +++ b/schunk_egu_egk_gripper_tests/test/test_functionality.py @@ -0,0 +1,8 @@ +import pytest +from test.conftest import launch_description +from test.helpers import get_current_state + + +@pytest.mark.launch(fixture=launch_description) +def test_driver_starts_in_not_ready_state(launch_context, isolated, gripper_dummy): + assert get_current_state(variable="ready_for_operation") is False diff --git a/schunk_egu_egk_gripper_tests/test/test_http_interface.py b/schunk_egu_egk_gripper_tests/test/test_http_interface.py index e3a1562..7bf76ca 100644 --- a/schunk_egu_egk_gripper_tests/test/test_http_interface.py +++ b/schunk_egu_egk_gripper_tests/test/test_http_interface.py @@ -2,7 +2,7 @@ from test.conftest import launch_description import time from sensor_msgs.msg import JointState -from test.helpers import CheckTopic +from test.helpers import TopicGetsPublished @pytest.mark.launch(fixture=launch_description) @@ -11,4 +11,4 @@ def test_driver_connnects_to_gripper_dummy(launch_context, isolated, gripper_dum timeout = 3 time.sleep(until_dummy_ready) - assert CheckTopic("/joint_states", JointState).event.wait(timeout) + assert TopicGetsPublished("/joint_states", JointState).event.wait(timeout) From e77ce6d9ec5ae5b8b001e6dff33676bff8f1b5e1 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 26 Jul 2024 10:57:22 +0200 Subject: [PATCH 22/35] Keep all of the dummy's request-related tests in a separate file That's cleaner and more concise. --- .../tests/test_dummy.py | 69 ------------------ .../tests/test_requests.py | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 69 deletions(-) create mode 100644 schunk_egu_egk_gripper_dummy/tests/test_requests.py diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index e37b4e8..8ac92e2 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -26,72 +26,3 @@ def test_dummy_reads_configuration_on_startup(): assert dummy.enum is not None assert dummy.data is not None assert dummy.metadata is not None - - -def test_dummy_responds_correctly_to_info_requests(): - dummy = Dummy() - query = "" - expected = {"dataformat": 0} - assert dummy.get_info(query) == expected - - -def test_dummy_responds_correctly_to_enum_requests(): - dummy = Dummy() - inst = "0x0118" - value = 0 - query = {"inst": inst, "value": value} - expected = [dummy.enum[inst][value]] - assert dummy.get_enum(query) == expected - - -def test_dummy_survives_invalid_enum_requests(): - dummy = Dummy() - invalid_inst = "0x0" - query = {"inst": invalid_inst, "value": 0} - expected = [] - assert dummy.get_enum(query) == expected - - -def test_dummy_responds_correctly_to_data_offset_requests(): - dummy = Dummy() - query = {"offset": 15, "count": 3} - expected = [dummy.metadata[15], dummy.metadata[16], dummy.metadata[17]] - assert dummy.get_data(query) == expected - - -def test_dummy_responds_correctly_to_data_instance_requests(): - dummy = Dummy() - inst = "0x0040" - query = {"inst": inst, "count": 1} - expected = dummy.data[inst] - assert dummy.get_data(query) == expected - - -def test_dummy_survives_invalid_data_requests(): - dummy = Dummy() - query = {"offset": 1000, "count": "2"} - expected = [] - assert dummy.get_data(query) == expected - query = {"offset": 100, "count": "90"} - expected = [] - assert dummy.get_data(query) == expected - query = {"offset": 1000, "count": "-1"} - expected = [] - assert dummy.get_data(query) == expected - query = {"offset": -1, "count": "1000"} - expected = [] - assert dummy.get_data(query) == expected - query = {"inst": "0x0040", "count": "0"} - expected = [] - assert dummy.get_data(query) == expected - query = {"inst": "0x0040", "count": "2"} - expected = [] - assert dummy.get_data(query) == expected - - -def test_dummy_responds_correctly_to_post_requests(): - dummy = Dummy() - inst = "0x0048" - data = {"inst": inst, "value": "01"} - expected = {"result": 0} - assert dummy.post(data) == expected diff --git a/schunk_egu_egk_gripper_dummy/tests/test_requests.py b/schunk_egu_egk_gripper_dummy/tests/test_requests.py new file mode 100644 index 0000000..55bbcc1 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/tests/test_requests.py @@ -0,0 +1,70 @@ +from src.dummy import Dummy + + +def test_dummy_responds_correctly_to_info_requests(): + dummy = Dummy() + query = "" + expected = {"dataformat": 0} + assert dummy.get_info(query) == expected + + +def test_dummy_responds_correctly_to_enum_requests(): + dummy = Dummy() + inst = "0x0118" + value = 0 + query = {"inst": inst, "value": value} + expected = [dummy.enum[inst][value]] + assert dummy.get_enum(query) == expected + + +def test_dummy_survives_invalid_enum_requests(): + dummy = Dummy() + invalid_inst = "0x0" + query = {"inst": invalid_inst, "value": 0} + expected = [] + assert dummy.get_enum(query) == expected + + +def test_dummy_responds_correctly_to_data_offset_requests(): + dummy = Dummy() + query = {"offset": 15, "count": 3} + expected = [dummy.metadata[15], dummy.metadata[16], dummy.metadata[17]] + assert dummy.get_data(query) == expected + + +def test_dummy_responds_correctly_to_data_instance_requests(): + dummy = Dummy() + inst = "0x0040" + query = {"inst": inst, "count": 1} + expected = dummy.data[inst] + assert dummy.get_data(query) == expected + + +def test_dummy_survives_invalid_data_requests(): + dummy = Dummy() + query = {"offset": 1000, "count": "2"} + expected = [] + assert dummy.get_data(query) == expected + query = {"offset": 100, "count": "90"} + expected = [] + assert dummy.get_data(query) == expected + query = {"offset": 1000, "count": "-1"} + expected = [] + assert dummy.get_data(query) == expected + query = {"offset": -1, "count": "1000"} + expected = [] + assert dummy.get_data(query) == expected + query = {"inst": "0x0040", "count": "0"} + expected = [] + assert dummy.get_data(query) == expected + query = {"inst": "0x0040", "count": "2"} + expected = [] + assert dummy.get_data(query) == expected + + +def test_dummy_responds_correctly_to_post_requests(): + dummy = Dummy() + inst = "0x0048" + data = {"inst": inst, "value": "01"} + expected = {"result": 0} + assert dummy.post(data) == expected From e79f736204eef0def0fc5f0189aa9844d5f23bd0 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Tue, 30 Jul 2024 12:15:07 +0200 Subject: [PATCH 23/35] Keep plc data buffers separate This makes bit operations on these data easier than with immutable strings. Also add image documentation for the different plc data messages and double words. --- .../doc/plc_control_double_word.png | Bin 0 -> 32366 bytes .../doc/plc_data_exchange.png | Bin 0 -> 33771 bytes .../doc/plc_input_data_frame.png | Bin 0 -> 18128 bytes .../doc/plc_output_data_frame.png | Bin 0 -> 18225 bytes .../doc/plc_status_double_word.png | Bin 0 -> 33263 bytes schunk_egu_egk_gripper_dummy/src/dummy.py | 15 ++++++ .../tests/test_plc_communication.py | 48 ++++++++++++++++++ 7 files changed, 63 insertions(+) create mode 100644 schunk_egu_egk_gripper_dummy/doc/plc_control_double_word.png create mode 100644 schunk_egu_egk_gripper_dummy/doc/plc_data_exchange.png create mode 100644 schunk_egu_egk_gripper_dummy/doc/plc_input_data_frame.png create mode 100644 schunk_egu_egk_gripper_dummy/doc/plc_output_data_frame.png create mode 100644 schunk_egu_egk_gripper_dummy/doc/plc_status_double_word.png create mode 100644 schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py diff --git a/schunk_egu_egk_gripper_dummy/doc/plc_control_double_word.png b/schunk_egu_egk_gripper_dummy/doc/plc_control_double_word.png new file mode 100644 index 0000000000000000000000000000000000000000..b25d51fa88390d5f62f60b51933ec6713cd0c72b GIT binary patch literal 32366 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 SxaJh_r>Uy*5Ov=wV-6xLLx< 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 literal 0 HcmV?d00001 diff --git a/schunk_egu_egk_gripper_dummy/doc/plc_status_double_word.png b/schunk_egu_egk_gripper_dummy/doc/plc_status_double_word.png new file mode 100644 index 0000000000000000000000000000000000000000..349bd255a26117e6b37119e330ac1fdfeb991804 GIT binary patch literal 33263 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 Wd2!@Fm4k`E-xEc3g~CV10sjXq?4nWt literal 0 HcmV?d00001 diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 42216d6..4e451cc 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -13,6 +13,8 @@ def __init__(self): self.enum = None self.metadata = None self.data = None + self.plc_input = "0x0040" + self.plc_output = "0x0048" enum_config = os.path.join( Path(__file__).resolve().parents[1], "config/enum.json" @@ -30,6 +32,9 @@ def __init__(self): with open(data_config, "r") as f: self.data = json.load(f) + self.plc_input_buffer = bytearray(bytes.fromhex(self.data[self.plc_input][0])) + self.plc_output_buffer = bytearray(bytes.fromhex(self.data[self.plc_output][0])) + def start(self) -> None: if self.running: return @@ -83,6 +88,16 @@ def get_data(self, query: dict[str, str]) -> list: return result if inst not in self.data: return result + if inst == self.plc_input: + return self.get_plc_input() + if inst == self.plc_output: + return self.get_plc_output() return self.data[inst] else: return [] + + def get_plc_input(self): + return [self.plc_input_buffer.hex().upper()] + + def get_plc_output(self): + return [self.plc_output_buffer.hex().upper()] diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py new file mode 100644 index 0000000..cfd9c73 --- /dev/null +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -0,0 +1,48 @@ +from src.dummy import Dummy +import pytest + +# [1]: https://stb.cloud.schunk.com/media/IM0046706.PDF + + +def test_dummy_initializes_plc_data_buffers(): + dummy = Dummy() + assert dummy.data[dummy.plc_input][0] == dummy.plc_input_buffer.hex().upper() + assert dummy.data[dummy.plc_output][0] == dummy.plc_output_buffer.hex().upper() + + +def test_dummy_returns_plc_data(): + dummy = Dummy() + assert dummy.data[dummy.plc_input] == dummy.get_plc_input() + assert dummy.data[dummy.plc_output] == dummy.get_plc_output() + + +# See p. 24 in +# Booting and establishing operational readiness [1] + + +@pytest.mark.skip() +def test_dummy_starts_in_error_state(): + dummy = Dummy() + query = {"inst": dummy.plc_input, "count": 1} + data = dummy.get_data(query)[0] + assert data[0:2] == "80" + assert data[30:] == "D9" # ERR_FAST_STOP + + +@pytest.mark.skip() +def test_dummy_is_ready_after_acknowledge(): + dummy = Dummy() + control_double_word = "04000000" + set_position = "00000000" + set_speed = "00000000" + gripping_force = "00000000" + command = { + "inst": dummy.plc_output, + "value": control_double_word + set_position + set_speed + gripping_force, + } + dummy.post(command) + + query = {"inst": dummy.plc_input, "count": 1} + data = dummy.get_data(query)[0] + assert data[0:2] == "11" + assert data[30:] == "00" # ERR_NONE From d2bb9589df068508886929ef958f0ae90b1779f0 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Tue, 30 Jul 2024 19:15:38 +0200 Subject: [PATCH 24/35] Implement reading and writing bits in the plc status dword --- schunk_egu_egk_gripper_dummy/src/dummy.py | 21 +++++++++++ .../tests/test_plc_communication.py | 36 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 4e451cc..1fde9fe 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -15,6 +15,7 @@ def __init__(self): self.data = None self.plc_input = "0x0040" self.plc_output = "0x0048" + self.reserved_status_bits = [10, 15] + list(range(18, 31)) enum_config = os.path.join( Path(__file__).resolve().parents[1], "config/enum.json" @@ -101,3 +102,23 @@ def get_plc_input(self): def get_plc_output(self): return [self.plc_output_buffer.hex().upper()] + + def set_status_bit(self, bit: int, value: bool) -> bool: + if bit < 0 or bit > 31: + return False + if bit in self.reserved_status_bits: + return False + byte_index, bit_index = divmod(bit, 8) + if value: + self.plc_input_buffer[byte_index] |= 1 << bit_index + else: + self.plc_input_buffer[byte_index] &= ~(1 << bit_index) + return True + + def get_status_bit(self, bit: int) -> int | bool: + if bit < 0 or bit > 31: + return False + if bit in self.reserved_status_bits: + return False + byte_index, bit_index = divmod(bit, 8) + return 1 if self.plc_input_buffer[byte_index] & (1 << bit_index) != 0 else 0 diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index cfd9c73..3a719a3 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -16,6 +16,42 @@ def test_dummy_returns_plc_data(): assert dummy.data[dummy.plc_output] == dummy.get_plc_output() +def test_dummy_rejects_writing_reserved_status_bits(): + dummy = Dummy() + invalid_bits = [-1, 999] + for bit in invalid_bits + dummy.reserved_status_bits: + assert not dummy.set_status_bit(bit, True) + + +def test_dummy_rejects_reading_reserved_status_bits(): + dummy = Dummy() + for bit in dummy.reserved_status_bits: + assert isinstance(dummy.get_status_bit(bit), bool) # call fails + assert not dummy.get_status_bit(bit) + + +def test_dummy_supports_reading_and_writing_bits_in_plc_status(): + dummy = Dummy() + valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] + for bit in valid_bits: + dummy.set_status_bit(bit=bit, value=True) + result = dummy.get_status_bit(bit=bit) + assert isinstance(result, int) # successful calls get the bit's value + assert result == 1 + + +def test_dummy_only_touches_specified_bits(): + dummy = Dummy() + before = dummy.get_plc_input() + valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] + for bit in valid_bits: + initial_value = dummy.get_status_bit(bit=bit) + dummy.set_status_bit(bit=bit, value=True) + dummy.set_status_bit(bit=bit, value=initial_value) + + assert dummy.get_plc_input() == before + + # See p. 24 in # Booting and establishing operational readiness [1] From 6f9f89f08de8e432d3b711e2aa576c517316ae21 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 31 Jul 2024 15:36:46 +0200 Subject: [PATCH 25/35] Add methods to read and write errors and diagnostics --- schunk_egu_egk_gripper_dummy/config/data.json | 2 +- .../doc/plc_diagnostics_double_word.png | Bin 0 -> 16267 bytes schunk_egu_egk_gripper_dummy/src/dummy.py | 24 +++++++++++ .../tests/test_plc_communication.py | 39 +++++++++++++++--- 4 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 schunk_egu_egk_gripper_dummy/doc/plc_diagnostics_double_word.png diff --git a/schunk_egu_egk_gripper_dummy/config/data.json b/schunk_egu_egk_gripper_dummy/config/data.json index 6f6eabe..371216e 100644 --- a/schunk_egu_egk_gripper_dummy/config/data.json +++ b/schunk_egu_egk_gripper_dummy/config/data.json @@ -1,6 +1,6 @@ { "0x0040": [ - "800000800000A25800000000000000EF" + "800000800000A25800000000D90000EF" ], "0x0048": [ "05000000000000000000000000000000" diff --git a/schunk_egu_egk_gripper_dummy/doc/plc_diagnostics_double_word.png b/schunk_egu_egk_gripper_dummy/doc/plc_diagnostics_double_word.png new file mode 100644 index 0000000000000000000000000000000000000000..f16354b76e34c0b461d840fae3ccfd6cb8116793 GIT binary patch literal 16267 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

DR# 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⩔lpwORh5)%1<%U>7$1{V;%E2PsYQ-+6D^z5vL-L zUr7SlTi{17>A+95&;m^^AM$!$!xTiaq~-^)C3RGWM+OtI7l!B8xH& z!)YZO2L}{0jao*M5KQ7<;ogPYfch2tjD6Tb*3uM7{zKGIKk@U-hXwYhFYe-6o+2-q(~{P!=_MJsUHf|l6%~8S+&xR zK0k_eEa=zlpR_YU{Dm~`N?ALUFOLqNZpVF9rx|P7r*~>}yTT~H%8*Kb{P-Mv^Q7f%r7for<}xA={tE&Xy!aD6*NXV# z>7&gW|J_wcE^SS-R?tZYLMCG_ySCGGBdP4`aBl!hKIJf)%Uu#4OVIJsU;M;TIg*@( zfsAT@EKx9Dd_6|DkU!p86? zJRC4dtbJx^`Kuv`{%N?4fgUSzDVliE)7@N1j*KIEBql*EwekrK2eS=K0I*LQ%0A?! zPByIF6BFzwS@j;8HecMRVm2KS$+#??jB5E<)a_}o%m@}&QMi0p%VPegoO;N4C4~;X zOKlgX+jKkw-?3h-pBHv1pS>tYSA*WCtl5txJ#F{q5W#`=aoB0iiQwQg%VoK|OH^5I z@XBCT+Z}kd$I7Ob<#K<67;xBTJ)7bES6E)((qNrVkG~_I46@cA>T%Q0tZJiSRqBtg zA6y`oflX(O3e> zj&A8MWT1Zy$)IzUYF1zR%NL2~RXZvx>6Q`eAB{4zx!Z-gy|$h&KStiM)E-l3q-F_? z*%Qmf-`Nc`G~Gqdh1==OT?ooPh_$+xabU&MQ*00Qg={o1ZA2SRd0XHdJWUWm)Tl(!w9R)XGX-J8MBy)LX>LfR& zc>inz;oR1tm9~*^-I&(iyIERsrT%0`SG%x@28Qm`HbmR7_fq%Y{&DkGdTESrT(;`tg-PL~*E8gO8;8blpIrU0(94o4yyg%8y(_ z2Y(LE1U0I1$gD!Z_O13!x;;BwTD;vuT!qEwE8Xs43W!R0ERlXHb}CQFB0ezG%RBK9 zsJV;zJswW%ZN^J&hZF&y<)EO(Z#kfn3TY)oioW`Ko2aX&60@m3@rwLQ_&jUfrrR zI_Q+@+GS&EbrTs$1$YpRA{4s%LDa=u=%DsyyDfEVkx3@OTy{L>*yVWp{p=-@C|A2663HXSgoi65zf$)SzR8O}bKC?@>%Y){R(}YF zU(iB|2T`SOx)ZDx*RBCCb(y4qW$0;0YLM1w7&1Q6+FOC`KgOaZNns*Ddi(cBwPDqq zpic57%&i_6A%wAmfxJ>$JIdhNpE>C2c@5&sxj;{=i?~-aoEPqL#&ssIj5p zaxS#Jc76Yc?4R0)@UTH2<)`JIMP;YG__cX++;4BzsZql|2-t##qE~LZ@&;x*6R)n) znm!|k{>));-0KvwVo)Mkw)9j6$b3G(ox{Mj8QZQHsL7hgIu_B$4tAxWx$xjn(jhzG zm)*1US$A@4K6e%XiUR$<62D<;2^YcjlQgwvfyqfsn7(u%aN z*}>msmD3IBp^*+vw`3e{bh~-#N`AiDb~%3fy>a^89CR)9U@;HwYO%aOJ~!=pAu@}U z^E5NpS$Jjd+QBhOHxHfOIz&1Z*;?b0fW38%V9DMzb~*OZCU_{&18D>NqwF^?GDA6` z*`s>>=ftt9o^wV7@CRbr%XxIpu@&dDf}Eatk@U;|8MRlQ}r;Eq#HTRHa0<={1kpQqu@x`_|{XWsaxEcr|an=R@EK<4Fb1Ic7zKz5V&waQsONEp~uL__5gdYy(rr!cyNXA_8e-Hl` zC}lF#41-C)flqV@RTKkzYE01$)&h|#iT$01nTQ=_H zH!@hJ+JXlCpiU}s+`-J&LPq85IDc>;v=^!ZcuA0HRb!tg>ClBa()CsjYVK^YRCF|h z_0o%5T^ttg9lT8sHvzzU)s int | bool: return False byte_index, bit_index = divmod(bit, 8) return 1 if self.plc_input_buffer[byte_index] & (1 << bit_index) != 0 else 0 + + def set_status_error(self, error: str) -> bool: + try: + self.plc_input_buffer[self.error_byte] = int(error, 16) + return True + except ValueError: + return False + + def get_status_error(self) -> str: + return hex(self.plc_input_buffer[self.error_byte]).replace("0x", "").upper() + + def set_status_diagnostics(self, diagnostics: str) -> bool: + try: + self.plc_input_buffer[self.diagnostics_byte] = int(diagnostics, 16) + return True + except ValueError: + return False + + def get_status_diagnostics(self) -> str: + return ( + hex(self.plc_input_buffer[self.diagnostics_byte]).replace("0x", "").upper() + ) diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index 3a719a3..1034227 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -52,17 +52,46 @@ def test_dummy_only_touches_specified_bits(): assert dummy.get_plc_input() == before +def test_dummy_supports_reading_and_writing_status_error(): + dummy = Dummy() + error_codes = ["AA", "bb", "0xcc"] + expected = ["AA", "BB", "CC"] + for error, expected in zip(error_codes, expected): + dummy.set_status_error(error) + assert dummy.get_status_error() == expected + + +def test_dummy_rejects_writing_invalid_status_error(): + dummy = Dummy() + invalid_codes = ["zz", "-1", "aaa"] + for error in invalid_codes: + assert not dummy.set_status_error(error) + + +def test_dummy_supports_reading_and_writing_status_diagnostics(): + dummy = Dummy() + diagnostics_code = "EF" + dummy.set_status_diagnostics(diagnostics_code) + assert dummy.get_status_diagnostics() == diagnostics_code + + +def test_dummy_rejects_writing_invalid_status_diagnostics(): + dummy = Dummy() + invalid_codes = ["zz", "-1", "aaa"] + for code in invalid_codes: + assert not dummy.set_status_diagnostics(code) + + # See p. 24 in # Booting and establishing operational readiness [1] -@pytest.mark.skip() def test_dummy_starts_in_error_state(): dummy = Dummy() - query = {"inst": dummy.plc_input, "count": 1} - data = dummy.get_data(query)[0] - assert data[0:2] == "80" - assert data[30:] == "D9" # ERR_FAST_STOP + assert dummy.get_status_bit(0) == 0 # not ready for operation + assert dummy.get_status_bit(7) == 1 # there's an error + assert dummy.get_status_error() == "D9" # ERR_FAST_STOP + assert dummy.get_status_diagnostics() == "EF" # ERR_COMM_LOST @pytest.mark.skip() From f531ef4e428237d81fd6285856472c05823385e7 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Wed, 31 Jul 2024 19:02:34 +0200 Subject: [PATCH 26/35] Store post requests' data for further processing. Also drop the pydantic message type. We currently don't make use of this in the ROS2 C++ driver. --- .../schunk_egu_egk_gripper_dummy/main.py | 10 +---- schunk_egu_egk_gripper_dummy/src/dummy.py | 12 ++++++ .../tests/test_requests.py | 37 ++++++++++++++++--- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py index 857964f..dbd2a49 100644 --- a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py @@ -3,7 +3,6 @@ from fastapi import FastAPI, Request, Form from fastapi.middleware.cors import CORSMiddleware from typing import Optional -from pydantic import BaseModel # Components dummy = Dummy() @@ -20,13 +19,6 @@ ) -class Update(BaseModel): - inst: str - value: str - elem: Optional[int] = None - callback: Optional[str] = None - - @server.post("/adi/update.json") async def post( inst: str = Form(...), @@ -34,7 +26,7 @@ async def post( elem: Optional[int] = Form(None), callback: Optional[str] = Form(None), ): - msg = Update(inst=inst, value=value, elem=elem, callback=callback) + msg = {"inst": inst, "value": value, "elem": elem, "callback": callback} return dummy.post(msg) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 15b09be..9bad432 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -3,6 +3,7 @@ import os from pathlib import Path import json +import string class Dummy(object): @@ -55,6 +56,17 @@ def _run(self) -> None: print("Done") def post(self, msg: dict) -> dict: + if "inst" not in msg and "value" not in msg: + return {"result": 1} + if msg["inst"] not in self.data: + return {"result": 1} + if not all(digit in string.hexdigits for digit in msg["value"]): + return {"result": 1} + + if msg["inst"] == self.plc_output: + self.plc_output_buffer = bytearray(bytes.fromhex(msg["value"])) + else: + self.data[msg["inst"]] = [msg["value"]] return {"result": 0} def get_info(self, query: dict[str, str]) -> dict: diff --git a/schunk_egu_egk_gripper_dummy/tests/test_requests.py b/schunk_egu_egk_gripper_dummy/tests/test_requests.py index 55bbcc1..b194959 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_requests.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_requests.py @@ -62,9 +62,36 @@ def test_dummy_survives_invalid_data_requests(): assert dummy.get_data(query) == expected -def test_dummy_responds_correctly_to_post_requests(): +def test_dummy_stores_post_requests(): dummy = Dummy() - inst = "0x0048" - data = {"inst": inst, "value": "01"} - expected = {"result": 0} - assert dummy.post(data) == expected + + # Using the plc command variable + msg = "00112233445566778899AABBCCDDEEFF" + data = {"inst": dummy.plc_output, "value": msg} + dummy.post(data) + assert dummy.get_plc_output() == [msg] + + # Using general variables + msg = "AABBCCDD" + inst = "0x0238" + data = {"inst": inst, "value": msg} + dummy.post(data) + assert dummy.data[inst] == [msg] + + +def test_dummy_rejects_invalid_post_requests(): + dummy = Dummy() + valid_data = "AABBCCDD" + valid_inst = "0x0238" + data = {"inst": valid_inst, "value": valid_data} + assert dummy.post(data) == {"result": 0} + + invalid_data = "hello:)" + valid_inst = "0x0238" + data = {"inst": valid_inst, "value": invalid_data} + assert dummy.post(data) == {"result": 1} + + valid_data = "AABBCCDD" + invalid_inst = "0x9999" + data = {"inst": invalid_inst, "value": valid_data} + assert dummy.post(data) == {"result": 1} From 80192ba8d800569268517be5c1da0e78c5aee1db Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Thu, 1 Aug 2024 07:45:00 +0200 Subject: [PATCH 27/35] Add function to read bits from plc control double word --- schunk_egu_egk_gripper_dummy/src/dummy.py | 9 +++++++++ .../tests/test_plc_communication.py | 19 ++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 9bad432..085296c 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -19,6 +19,7 @@ def __init__(self): self.error_byte = 12 self.diagnostics_byte = 15 self.reserved_status_bits = [10, 15] + list(range(18, 31)) + self.reserved_control_bits = [10, 15] + list(range(17, 30)) enum_config = os.path.join( Path(__file__).resolve().parents[1], "config/enum.json" @@ -158,3 +159,11 @@ def get_status_diagnostics(self) -> str: return ( hex(self.plc_input_buffer[self.diagnostics_byte]).replace("0x", "").upper() ) + + def get_control_bit(self, bit: int) -> int | bool: + if bit < 0 or bit > 31: + return False + if bit in self.reserved_control_bits: + return False + byte_index, bit_index = divmod(bit, 8) + return 1 if self.plc_output_buffer[byte_index] & (1 << bit_index) != 0 else 0 diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index 1034227..0b358e0 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -40,7 +40,7 @@ def test_dummy_supports_reading_and_writing_bits_in_plc_status(): assert result == 1 -def test_dummy_only_touches_specified_bits(): +def test_dummy_only_touches_specified_status_bits(): dummy = Dummy() before = dummy.get_plc_input() valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] @@ -82,11 +82,24 @@ def test_dummy_rejects_writing_invalid_status_diagnostics(): assert not dummy.set_status_diagnostics(code) -# See p. 24 in -# Booting and establishing operational readiness [1] +def test_dummy_supports_reading_bits_in_plc_control(): + dummy = Dummy() + valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 30, 31] + for bit in valid_bits: + result = dummy.get_control_bit(bit=bit) + assert isinstance(result, int) # successful calls get the bit's value + + +def test_dummy_rejects_reading_reserved_control_bits(): + dummy = Dummy() + for bit in dummy.reserved_control_bits: + assert isinstance(dummy.get_control_bit(bit), bool) # call fails + assert not dummy.get_control_bit(bit) def test_dummy_starts_in_error_state(): + # See p. 24 in + # Booting and establishing operational readiness [1] dummy = Dummy() assert dummy.get_status_bit(0) == 0 # not ready for operation assert dummy.get_status_bit(7) == 1 # there's an error From 05817c35bd08c78c7c64301a1b17ba47f660ae7e Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Thu, 1 Aug 2024 10:49:19 +0200 Subject: [PATCH 28/35] Implement the acknowledge mechanism in the dummy Also - Move behavior-related tests into `test_dummy.py`. They make more sense there - Adapt the ROS2 test for checking whether the driver is ready after startup. The driver should be operational after start by acknowledging internal errors. --- schunk_egu_egk_gripper_dummy/src/dummy.py | 12 ++++++ .../tests/test_dummy.py | 29 +++++++++++++ .../tests/test_plc_communication.py | 30 -------------- .../test/test_functionality.py | 41 ++++++++++++++++++- 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 085296c..140fa5b 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -68,6 +68,9 @@ def post(self, msg: dict) -> dict: self.plc_output_buffer = bytearray(bytes.fromhex(msg["value"])) else: self.data[msg["inst"]] = [msg["value"]] + + # Behavior + self.process_control_bits() return {"result": 0} def get_info(self, query: dict[str, str]) -> dict: @@ -167,3 +170,12 @@ def get_control_bit(self, bit: int) -> int | bool: return False byte_index, bit_index = divmod(bit, 8) return 1 if self.plc_output_buffer[byte_index] & (1 << bit_index) != 0 else 0 + + def process_control_bits(self) -> None: + + # Acknowledge + if self.get_control_bit(2) == 1: + self.set_status_bit(bit=0, value=True) + self.set_status_bit(bit=7, value=False) + self.set_status_error("00") + self.set_status_diagnostics("00") diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index 8ac92e2..ee6da02 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -1,5 +1,7 @@ from src.dummy import Dummy +# [1]: https://stb.cloud.schunk.com/media/IM0046706.PDF + def test_dummy_starts_a_background_thread(): dummy = Dummy() @@ -26,3 +28,30 @@ def test_dummy_reads_configuration_on_startup(): assert dummy.enum is not None assert dummy.data is not None assert dummy.metadata is not None + + +def test_dummy_starts_in_error_state(): + # See p. 24 in + # Booting and establishing operational readiness [1] + dummy = Dummy() + assert dummy.get_status_bit(0) == 0 # not ready for operation + assert dummy.get_status_bit(7) == 1 # there's an error + assert dummy.get_status_error() == "D9" # ERR_FAST_STOP + assert dummy.get_status_diagnostics() == "EF" # ERR_COMM_LOST + + +def test_dummy_is_ready_after_acknowledge(): + dummy = Dummy() + control_double_word = "04000000" + set_position = "00000000" + set_speed = "00000000" + gripping_force = "00000000" + command = { + "inst": dummy.plc_output, + "value": control_double_word + set_position + set_speed + gripping_force, + } + dummy.post(command) + assert dummy.get_status_bit(0) == 1 # ready + assert dummy.get_status_bit(7) == 0 # no error + assert dummy.get_status_error() == "0" + assert dummy.get_status_diagnostics() == "0" diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index 0b358e0..e058fae 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -1,5 +1,4 @@ from src.dummy import Dummy -import pytest # [1]: https://stb.cloud.schunk.com/media/IM0046706.PDF @@ -95,32 +94,3 @@ def test_dummy_rejects_reading_reserved_control_bits(): for bit in dummy.reserved_control_bits: assert isinstance(dummy.get_control_bit(bit), bool) # call fails assert not dummy.get_control_bit(bit) - - -def test_dummy_starts_in_error_state(): - # See p. 24 in - # Booting and establishing operational readiness [1] - dummy = Dummy() - assert dummy.get_status_bit(0) == 0 # not ready for operation - assert dummy.get_status_bit(7) == 1 # there's an error - assert dummy.get_status_error() == "D9" # ERR_FAST_STOP - assert dummy.get_status_diagnostics() == "EF" # ERR_COMM_LOST - - -@pytest.mark.skip() -def test_dummy_is_ready_after_acknowledge(): - dummy = Dummy() - control_double_word = "04000000" - set_position = "00000000" - set_speed = "00000000" - gripping_force = "00000000" - command = { - "inst": dummy.plc_output, - "value": control_double_word + set_position + set_speed + gripping_force, - } - dummy.post(command) - - query = {"inst": dummy.plc_input, "count": 1} - data = dummy.get_data(query)[0] - assert data[0:2] == "11" - assert data[30:] == "00" # ERR_NONE diff --git a/schunk_egu_egk_gripper_tests/test/test_functionality.py b/schunk_egu_egk_gripper_tests/test/test_functionality.py index 2d8fb7a..b34b90a 100644 --- a/schunk_egu_egk_gripper_tests/test/test_functionality.py +++ b/schunk_egu_egk_gripper_tests/test/test_functionality.py @@ -1,8 +1,45 @@ import pytest +import rclpy +from rclpy.node import Node from test.conftest import launch_description +from rclpy.action import ActionClient +from schunk_egu_egk_gripper_interfaces.action import ( # type: ignore[attr-defined] + MoveToAbsolutePosition, +) +from schunk_egu_egk_gripper_interfaces.srv import ( # type: ignore[attr-defined] + Acknowledge, +) from test.helpers import get_current_state @pytest.mark.launch(fixture=launch_description) -def test_driver_starts_in_not_ready_state(launch_context, isolated, gripper_dummy): - assert get_current_state(variable="ready_for_operation") is False +def test_driver_starts_in_ready_state(launch_context, isolated, gripper_dummy): + assert get_current_state(variable="ready_for_operation") is True + + +@pytest.mark.launch(fixture=launch_description) +def test_driver_is_ready_after_acknowledge(launch_context, isolated, gripper_dummy): + node = Node("test") + activate_srv = node.create_client(Acknowledge, "/acknowledge") + while not activate_srv.wait_for_service(1.0): + pass + future = activate_srv.call_async(Acknowledge.Request()) + rclpy.spin_until_future_complete(node, future) + assert future.result().success is True + + +@pytest.mark.launch(fixture=launch_description) +@pytest.mark.skip() +def test_driver_moves_to_absolute_position(launch_context, isolated, gripper_dummy): + node = Node("move_test") + # activate_srv = node.create_client(Acknowledge, "/acknowledge") + # future = activate_srv.call_async(Acknowledge.Request()) + # rclpy.spin_until_future_complete(node, future) + + client = ActionClient(node, MoveToAbsolutePosition, "/move_to_absolute_position") + client.wait_for_server() + goal = MoveToAbsolutePosition.Goal() + future = client.send_goal_async(goal) + rclpy.spin_until_future_complete(node, future) + print("done :)") + assert False From 2d50eda999d7f6d3bf5be9e75f7fa350d8c01a89 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 2 Aug 2024 14:48:37 +0200 Subject: [PATCH 29/35] Add functions to read target setpoints Also provide a mechanism to set actual position and velocity in the internal data. These data will be requested periodically by the driver. --- schunk_egu_egk_gripper_dummy/src/dummy.py | 17 +++++++++ .../tests/test_plc_communication.py | 36 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 140fa5b..e506613 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -4,6 +4,7 @@ from pathlib import Path import json import string +import struct class Dummy(object): @@ -20,6 +21,8 @@ def __init__(self): self.diagnostics_byte = 15 self.reserved_status_bits = [10, 15] + list(range(18, 31)) self.reserved_control_bits = [10, 15] + list(range(17, 30)) + self.actual_position = "0x0230" + self.actual_speed = "0x0238" enum_config = os.path.join( Path(__file__).resolve().parents[1], "config/enum.json" @@ -171,6 +174,20 @@ def get_control_bit(self, bit: int) -> int | bool: byte_index, bit_index = divmod(bit, 8) return 1 if self.plc_output_buffer[byte_index] & (1 << bit_index) != 0 else 0 + def get_target_position(self) -> float: + return struct.unpack("f", self.plc_output_buffer[4:8])[0] + + def get_target_speed(self) -> float: + return struct.unpack("f", self.plc_output_buffer[8:12])[0] + + def set_actual_position(self, position: float) -> None: + self.data[self.actual_position] = [ + bytes(struct.pack("f", position)).hex().upper() + ] + + def set_actual_speed(self, speed: float) -> None: + self.data[self.actual_speed] = [bytes(struct.pack("f", speed)).hex().upper()] + def process_control_bits(self) -> None: # Acknowledge diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index e058fae..5fd6e37 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -1,4 +1,6 @@ from src.dummy import Dummy +import struct +import pytest # [1]: https://stb.cloud.schunk.com/media/IM0046706.PDF @@ -94,3 +96,37 @@ def test_dummy_rejects_reading_reserved_control_bits(): for bit in dummy.reserved_control_bits: assert isinstance(dummy.get_control_bit(bit), bool) # call fails assert not dummy.get_control_bit(bit) + + +def test_dummy_supports_reading_target_position(): + dummy = Dummy() + target_pos = 0.0123 + dummy.plc_output_buffer[4:8] = bytes(struct.pack("f", target_pos)) + assert pytest.approx(dummy.get_target_position()) == target_pos + + +def test_dummy_supports_reading_target_speed(): + dummy = Dummy() + target_speed = 55.3 + dummy.plc_output_buffer[8:12] = bytes(struct.pack("f", target_speed)) + assert pytest.approx(dummy.get_target_speed()) == target_speed + + +def test_dummy_supports_writing_actual_position(): + dummy = Dummy() + inst = "0x0230" # + pos = 0.123 + dummy.set_actual_position(pos) + read_pos = dummy.data[inst][0] + read_pos = struct.unpack("f", bytes.fromhex(read_pos))[0] + assert pytest.approx(read_pos) == pos + + +def test_dummy_supports_writing_actual_speed(): + dummy = Dummy() + inst = "0x0238" # + speed = 66.5 + dummy.set_actual_speed(speed) + read_speed = dummy.data[inst][0] + read_speed = struct.unpack("f", bytes.fromhex(read_speed))[0] + assert pytest.approx(read_speed) == speed From b8f34e8cbda7db76b00d07175a5df3e83179c283 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 2 Aug 2024 15:18:04 +0200 Subject: [PATCH 30/35] Add function for toggling status bits We'll need this for toggling the `command received` bit for most plc commands. --- schunk_egu_egk_gripper_dummy/src/dummy.py | 11 ++++++++ .../tests/test_plc_communication.py | 26 ++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index e506613..771e656 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -19,6 +19,8 @@ def __init__(self): self.plc_output = "0x0048" self.error_byte = 12 self.diagnostics_byte = 15 + self.valid_status_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] + self.valid_control_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 30, 31] self.reserved_status_bits = [10, 15] + list(range(18, 31)) self.reserved_control_bits = [10, 15] + list(range(17, 30)) self.actual_position = "0x0230" @@ -144,6 +146,15 @@ def get_status_bit(self, bit: int) -> int | bool: byte_index, bit_index = divmod(bit, 8) return 1 if self.plc_input_buffer[byte_index] & (1 << bit_index) != 0 else 0 + def toggle_status_bit(self, bit: int) -> bool: + if bit < 0 or bit > 31: + return False + if bit in self.reserved_status_bits: + return False + byte_index, bit_index = divmod(bit, 8) + self.plc_input_buffer[byte_index] ^= 1 << bit_index + return True + def set_status_error(self, error: str) -> bool: try: self.plc_input_buffer[self.error_byte] = int(error, 16) diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index 5fd6e37..c24fd68 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -33,8 +33,7 @@ def test_dummy_rejects_reading_reserved_status_bits(): def test_dummy_supports_reading_and_writing_bits_in_plc_status(): dummy = Dummy() - valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] - for bit in valid_bits: + for bit in dummy.valid_status_bits: dummy.set_status_bit(bit=bit, value=True) result = dummy.get_status_bit(bit=bit) assert isinstance(result, int) # successful calls get the bit's value @@ -44,8 +43,7 @@ def test_dummy_supports_reading_and_writing_bits_in_plc_status(): def test_dummy_only_touches_specified_status_bits(): dummy = Dummy() before = dummy.get_plc_input() - valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] - for bit in valid_bits: + for bit in dummy.valid_status_bits: initial_value = dummy.get_status_bit(bit=bit) dummy.set_status_bit(bit=bit, value=True) dummy.set_status_bit(bit=bit, value=initial_value) @@ -85,8 +83,7 @@ def test_dummy_rejects_writing_invalid_status_diagnostics(): def test_dummy_supports_reading_bits_in_plc_control(): dummy = Dummy() - valid_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 30, 31] - for bit in valid_bits: + for bit in dummy.valid_control_bits: result = dummy.get_control_bit(bit=bit) assert isinstance(result, int) # successful calls get the bit's value @@ -98,6 +95,23 @@ def test_dummy_rejects_reading_reserved_control_bits(): assert not dummy.get_control_bit(bit) +def test_dummy_supports_toggling_status_bits(): + dummy = Dummy() + for bit in dummy.valid_status_bits: + before = dummy.get_status_bit(bit) + dummy.toggle_status_bit(bit=bit) + after = dummy.get_status_bit(bit) + assert after != before + dummy.toggle_status_bit(bit=bit) + assert dummy.get_status_bit(bit=bit) == before + + +def test_dummy_rejects_toggling_reserved_status_bits(): + dummy = Dummy() + for bit in dummy.reserved_status_bits: + assert not dummy.toggle_status_bit(bit) + + def test_dummy_supports_reading_target_position(): dummy = Dummy() target_pos = 0.0123 From c06385be97f627f5f0a3c6026aac7ec9559df445 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Fri, 2 Aug 2024 17:41:28 +0200 Subject: [PATCH 31/35] Use fastapi's background task mechanism for gripper motion This seems the cleanest approach for now for giving immediate results to post requests and having the motion run in the background. Also adapt the unit tests accordingly. --- .../schunk_egu_egk_gripper_dummy/main.py | 12 ++++++++++-- schunk_egu_egk_gripper_dummy/src/dummy.py | 12 ++---------- .../tests/test_plc_communication.py | 6 ++---- schunk_egu_egk_gripper_dummy/tests/test_requests.py | 11 +++++++---- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py index dbd2a49..46ec41f 100644 --- a/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py +++ b/schunk_egu_egk_gripper_dummy/schunk_egu_egk_gripper_dummy/main.py @@ -1,8 +1,9 @@ from src.dummy import Dummy -from fastapi import FastAPI, Request, Form +from fastapi import FastAPI, Request, Form, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from typing import Optional +import string # Components dummy = Dummy() @@ -25,9 +26,16 @@ async def post( value: str = Form(...), elem: Optional[int] = Form(None), callback: Optional[str] = Form(None), + background_tasks: BackgroundTasks = None, ): msg = {"inst": inst, "value": value, "elem": elem, "callback": callback} - return dummy.post(msg) + + if msg["inst"] not in dummy.data: + return {"result": 1} + if not all(digit in string.hexdigits for digit in str(msg["value"])): + return {"result": 1} + background_tasks.add_task(dummy.post, msg) + return {"result": 0} @server.get("/adi/{path}") diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index 771e656..c8401aa 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -3,7 +3,6 @@ import os from pathlib import Path import json -import string import struct @@ -17,14 +16,14 @@ def __init__(self): self.data = None self.plc_input = "0x0040" self.plc_output = "0x0048" + self.actual_position = "0x0230" + self.actual_speed = "0x0238" self.error_byte = 12 self.diagnostics_byte = 15 self.valid_status_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 17, 31] self.valid_control_bits = list(range(0, 10)) + [11, 12, 13, 14, 16, 30, 31] self.reserved_status_bits = [10, 15] + list(range(18, 31)) self.reserved_control_bits = [10, 15] + list(range(17, 30)) - self.actual_position = "0x0230" - self.actual_speed = "0x0238" enum_config = os.path.join( Path(__file__).resolve().parents[1], "config/enum.json" @@ -62,13 +61,6 @@ def _run(self) -> None: print("Done") def post(self, msg: dict) -> dict: - if "inst" not in msg and "value" not in msg: - return {"result": 1} - if msg["inst"] not in self.data: - return {"result": 1} - if not all(digit in string.hexdigits for digit in msg["value"]): - return {"result": 1} - if msg["inst"] == self.plc_output: self.plc_output_buffer = bytearray(bytes.fromhex(msg["value"])) else: diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index c24fd68..4a3bcdc 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -128,19 +128,17 @@ def test_dummy_supports_reading_target_speed(): def test_dummy_supports_writing_actual_position(): dummy = Dummy() - inst = "0x0230" # pos = 0.123 dummy.set_actual_position(pos) - read_pos = dummy.data[inst][0] + read_pos = dummy.data[dummy.actual_position][0] read_pos = struct.unpack("f", bytes.fromhex(read_pos))[0] assert pytest.approx(read_pos) == pos def test_dummy_supports_writing_actual_speed(): dummy = Dummy() - inst = "0x0238" # speed = 66.5 dummy.set_actual_speed(speed) - read_speed = dummy.data[inst][0] + read_speed = dummy.data[dummy.actual_speed][0] read_speed = struct.unpack("f", bytes.fromhex(read_speed))[0] assert pytest.approx(read_speed) == speed diff --git a/schunk_egu_egk_gripper_dummy/tests/test_requests.py b/schunk_egu_egk_gripper_dummy/tests/test_requests.py index b194959..48667c3 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_requests.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_requests.py @@ -1,4 +1,6 @@ from src.dummy import Dummy +from schunk_egu_egk_gripper_dummy.main import server +from fastapi.testclient import TestClient def test_dummy_responds_correctly_to_info_requests(): @@ -80,18 +82,19 @@ def test_dummy_stores_post_requests(): def test_dummy_rejects_invalid_post_requests(): - dummy = Dummy() + client = TestClient(server) + valid_data = "AABBCCDD" valid_inst = "0x0238" data = {"inst": valid_inst, "value": valid_data} - assert dummy.post(data) == {"result": 0} + assert client.post("/adi/update.json", data=data).json() == {"result": 0} invalid_data = "hello:)" valid_inst = "0x0238" data = {"inst": valid_inst, "value": invalid_data} - assert dummy.post(data) == {"result": 1} + assert client.post("/adi/update.json", data=data).json() == {"result": 1} valid_data = "AABBCCDD" invalid_inst = "0x9999" data = {"inst": invalid_inst, "value": valid_data} - assert dummy.post(data) == {"result": 1} + assert client.post("/adi/update.json", data=data).json() == {"result": 1} From b22fa1d62ccfa5ae68f6fe9d0f90cfcda36e61d0 Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 5 Aug 2024 14:11:07 +0200 Subject: [PATCH 32/35] Implement driving to absolute positions Using a linear motion profile should be sufficient for now. --- schunk_egu_egk_gripper_dummy/src/dummy.py | 64 +++++++++++++++++++ .../tests/test_dummy.py | 28 ++++++++ .../tests/test_plc_communication.py | 14 ++++ 3 files changed, 106 insertions(+) diff --git a/schunk_egu_egk_gripper_dummy/src/dummy.py b/schunk_egu_egk_gripper_dummy/src/dummy.py index c8401aa..2d7a045 100644 --- a/schunk_egu_egk_gripper_dummy/src/dummy.py +++ b/schunk_egu_egk_gripper_dummy/src/dummy.py @@ -4,6 +4,36 @@ from pathlib import Path import json import struct +from typing import Tuple + + +class LinearMotion(object): + def __init__( + self, + initial_pos: float, + initial_speed: float, + target_pos: float, + target_speed: float, + ): + self.min_speed = 0.001 + self.initial_pos = initial_pos + self.initial_speed = max(0.0, initial_speed) + self.target_pos = target_pos + self.target_speed = max(self.min_speed, target_speed) + self.time_finish = abs(self.target_pos / self.target_speed) + + def sample(self, t: float) -> Tuple[float, float]: + if t <= 0.0: + return (self.initial_pos, self.initial_speed) + if t >= self.time_finish: + return (self.target_pos, 0.0) + + v = self.target_speed + if self.target_pos < self.initial_pos: + v = -v + current_pos = v * t + self.initial_pos + current_speed = abs(v) + return (current_pos, current_speed) class Dummy(object): @@ -60,6 +90,22 @@ def _run(self) -> None: time.sleep(1) print("Done") + def move(self, target_pos: float, target_speed: float) -> None: + motion = LinearMotion( + initial_pos=self.get_actual_position(), + initial_speed=self.get_actual_speed(), + target_pos=target_pos, + target_speed=target_speed, + ) + start = time.time() + actual_pos, actual_speed = motion.sample(0) + while abs(actual_pos) < abs(target_pos): + t = time.time() - start + actual_pos, actual_speed = motion.sample(t) + self.set_actual_position(actual_pos) + self.set_actual_speed(actual_speed) + time.sleep(0.01) + def post(self, msg: dict) -> dict: if msg["inst"] == self.plc_output: self.plc_output_buffer = bytearray(bytes.fromhex(msg["value"])) @@ -191,6 +237,14 @@ def set_actual_position(self, position: float) -> None: def set_actual_speed(self, speed: float) -> None: self.data[self.actual_speed] = [bytes(struct.pack("f", speed)).hex().upper()] + def get_actual_position(self) -> float: + read_pos = self.data[self.actual_position][0] + return struct.unpack("f", bytes.fromhex(read_pos))[0] + + def get_actual_speed(self) -> float: + read_speed = self.data[self.actual_speed][0] + return struct.unpack("f", bytes.fromhex(read_speed))[0] + def process_control_bits(self) -> None: # Acknowledge @@ -199,3 +253,13 @@ def process_control_bits(self) -> None: self.set_status_bit(bit=7, value=False) self.set_status_error("00") self.set_status_diagnostics("00") + + # Move to absolute position + if self.get_control_bit(13) == 1: + self.toggle_status_bit(5) + self.move( + target_pos=self.get_target_position(), + target_speed=self.get_target_speed(), + ) + self.set_status_bit(bit=13, value=True) + self.set_status_bit(bit=4, value=True) diff --git a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py index ee6da02..b8a610a 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_dummy.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_dummy.py @@ -1,4 +1,6 @@ from src.dummy import Dummy +import pytest +import struct # [1]: https://stb.cloud.schunk.com/media/IM0046706.PDF @@ -55,3 +57,29 @@ def test_dummy_is_ready_after_acknowledge(): assert dummy.get_status_bit(7) == 0 # no error assert dummy.get_status_error() == "0" assert dummy.get_status_diagnostics() == "0" + + +def test_dummy_moves_to_absolute_position(): + dummy = Dummy() + target_pos = 12.345 + target_speed = 50.3 + + control_double_word = "00200000" + set_position = bytes(struct.pack("f", target_pos)).hex().upper() + set_speed = bytes(struct.pack("f", target_speed)).hex().upper() + gripping_force = "00000000" + command = { + "inst": dummy.plc_output, + "value": control_double_word + set_position + set_speed + gripping_force, + } + before = dummy.get_status_bit(bit=5) # command received toggle + + # Motion + dummy.post(command) + + # Done + assert pytest.approx(dummy.get_actual_position()) == target_pos + after = dummy.get_status_bit(bit=5) + assert after != before + assert dummy.get_status_bit(bit=13) == 1 # position reached + assert dummy.get_status_bit(bit=4) == 1 # command successfully processed diff --git a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py index 4a3bcdc..310692f 100644 --- a/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py +++ b/schunk_egu_egk_gripper_dummy/tests/test_plc_communication.py @@ -142,3 +142,17 @@ def test_dummy_supports_writing_actual_speed(): read_speed = dummy.data[dummy.actual_speed][0] read_speed = struct.unpack("f", bytes.fromhex(read_speed))[0] assert pytest.approx(read_speed) == speed + + +def test_dummy_supports_reading_actual_position(): + dummy = Dummy() + pos = 0.123 + dummy.set_actual_position(pos) + assert pytest.approx(dummy.get_actual_position()) == pos + + +def test_dummy_supports_reading_actual_speed(): + dummy = Dummy() + speed = 66.5 + dummy.set_actual_speed(speed) + assert pytest.approx(dummy.get_actual_speed()) == speed From 90dce50ad92e82ba04c96f048c99b1643fb2d6fb Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 5 Aug 2024 14:13:48 +0200 Subject: [PATCH 33/35] Add missing CI dependency --- .github/script/install_dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/script/install_dependencies.sh b/.github/script/install_dependencies.sh index 4e83ff2..1c7ac32 100755 --- a/.github/script/install_dependencies.sh +++ b/.github/script/install_dependencies.sh @@ -3,7 +3,7 @@ cd $HOME apt-get install -y curl libcurl4-openssl-dev # Python dependencies -python_deps="fastapi uvicorn httpx requests coverage" +python_deps="fastapi uvicorn httpx requests coverage python-multipart" os_name=$(lsb_release -cs) case $os_name in From 8640debdcaac27367d4e35073b4a29ff7de0764c Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 5 Aug 2024 17:17:11 +0200 Subject: [PATCH 34/35] Activate the absolute position test in the driver --- .../test/test_functionality.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/schunk_egu_egk_gripper_tests/test/test_functionality.py b/schunk_egu_egk_gripper_tests/test/test_functionality.py index b34b90a..e0b9d5f 100644 --- a/schunk_egu_egk_gripper_tests/test/test_functionality.py +++ b/schunk_egu_egk_gripper_tests/test/test_functionality.py @@ -29,17 +29,18 @@ def test_driver_is_ready_after_acknowledge(launch_context, isolated, gripper_dum @pytest.mark.launch(fixture=launch_description) -@pytest.mark.skip() def test_driver_moves_to_absolute_position(launch_context, isolated, gripper_dummy): node = Node("move_test") - # activate_srv = node.create_client(Acknowledge, "/acknowledge") - # future = activate_srv.call_async(Acknowledge.Request()) - # rclpy.spin_until_future_complete(node, future) client = ActionClient(node, MoveToAbsolutePosition, "/move_to_absolute_position") client.wait_for_server() goal = MoveToAbsolutePosition.Goal() - future = client.send_goal_async(goal) - rclpy.spin_until_future_complete(node, future) - print("done :)") - assert False + + goal_future = client.send_goal_async(goal) + rclpy.spin_until_future_complete(node, goal_future) + goal_handle = goal_future.result() + + result_future = goal_handle.get_result_async() + rclpy.spin_until_future_complete(node, result_future) + result = result_future.result().result + assert result.position_reached From 4b8313b5a511285fa706bff5e276ca18ce31839c Mon Sep 17 00:00:00 2001 From: Stefan Scherzinger Date: Mon, 5 Aug 2024 17:20:38 +0200 Subject: [PATCH 35/35] Drop the _testing_ branch for `Rolling` in the CI This is currently not very helpful. Let's get that back once we are interested in latest updates on `Rolling`. --- .github/workflows/industrial_ci_rolling_action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/industrial_ci_rolling_action.yml b/.github/workflows/industrial_ci_rolling_action.yml index 9bc91ab..78a21f9 100644 --- a/.github/workflows/industrial_ci_rolling_action.yml +++ b/.github/workflows/industrial_ci_rolling_action.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: env: - - {ROS_DISTRO: rolling, ROS_REPO: testing} + #- {ROS_DISTRO: rolling, ROS_REPO: testing} - {ROS_DISTRO: rolling, ROS_REPO: main} runs-on: ubuntu-latest steps: