From 219ffc531cc857728c67f4827935a46f7d6f8bbe Mon Sep 17 00:00:00 2001 From: ohbotno Date: Sun, 27 Apr 2025 02:03:42 +0100 Subject: [PATCH] Add files via upload --- LICENSE | 235 +++++++ MANIFEST.in | 3 + README.md | 178 +++++ RSI_EthernetConfig.xml | 42 ++ examples/README.md | 18 + examples/example_01_start_stop.py | 11 + examples/example_02_send_cartesian.py | 9 + examples/example_03_send_joint.py | 9 + examples/example_04_external_axes.py | 9 + examples/example_05_digital_io.py | 9 + examples/example_06_logging_csv.py | 11 + examples/example_07_graphing_live.py | 11 + examples/example_08_safety_limits.py | 14 + examples/example_09_trajectory_cartesian.py | 20 + examples/example_10_shutdown_safe.py | 14 + pyproject.toml | 32 + setup.py | 28 + src/RSIPI.egg-info/PKG-INFO | 435 +++++++++++++ src/RSIPI.egg-info/SOURCES.txt | 30 + src/RSIPI.egg-info/dependency_links.txt | 1 + src/RSIPI.egg-info/requires.txt | 5 + src/RSIPI.egg-info/top_level.txt | 1 + src/RSIPI/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 272 bytes .../__pycache__/config_parser.cpython-311.pyc | Bin 0 -> 10721 bytes .../echo_server_gui.cpython-311.pyc | Bin 0 -> 14455 bytes .../inject_rsi_to_krl.cpython-311.pyc | Bin 0 -> 3333 bytes .../krl_to_csv_parser.cpython-311.pyc | Bin 0 -> 6165 bytes .../kuka_visualiser.cpython-311.pyc | Bin 0 -> 9074 bytes .../__pycache__/live_plotter.cpython-311.pyc | Bin 0 -> 12095 bytes .../network_handler.cpython-311.pyc | Bin 0 -> 6875 bytes src/RSIPI/__pycache__/rsi_api.cpython-311.pyc | Bin 0 -> 36580 bytes .../__pycache__/rsi_client.cpython-311.pyc | Bin 0 -> 6213 bytes .../__pycache__/rsi_config.cpython-311.pyc | Bin 0 -> 9606 bytes .../__pycache__/rsi_config.cpython-313.pyc | Bin 0 -> 7109 bytes .../rsi_echo_server.cpython-311.pyc | Bin 0 -> 10721 bytes .../__pycache__/rsi_graphing.cpython-311.pyc | Bin 0 -> 14522 bytes .../rsi_limit_parser.cpython-311.pyc | Bin 0 -> 3607 bytes .../safety_manager.cpython-311.pyc | Bin 0 -> 5360 bytes .../static_plotter.cpython-311.pyc | Bin 0 -> 16410 bytes .../trajectory_planner.cpython-311.pyc | Bin 0 -> 3237 bytes .../__pycache__/xml_handler.cpython-311.pyc | Bin 0 -> 3622 bytes src/RSIPI/config_parser.py | 196 ++++++ src/RSIPI/echo_server_gui.py | 201 ++++++ src/RSIPI/inject_rsi_to_krl.py | 79 +++ src/RSIPI/krl_to_csv_parser.py | 100 +++ src/RSIPI/kuka_visualiser.py | 164 +++++ src/RSIPI/live_plotter.py | 130 ++++ src/RSIPI/network_handler.py | 86 +++ src/RSIPI/rsi_api.py | 609 ++++++++++++++++++ src/RSIPI/rsi_cli.py | 196 ++++++ src/RSIPI/rsi_client.py | 103 +++ src/RSIPI/rsi_config.py | 174 +++++ src/RSIPI/rsi_echo_server.py | 173 +++++ src/RSIPI/rsi_graphing.py | 193 ++++++ src/RSIPI/rsi_limit_parser.py | 74 +++ src/RSIPI/safety_manager.py | 110 ++++ src/RSIPI/static_plotter.py | 158 +++++ src/RSIPI/trajectory_planner.py | 65 ++ src/RSIPI/xml_handler.py | 58 ++ src/__init__.py | 0 src/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 266 bytes src/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 254 bytes 63 files changed, 3994 insertions(+) create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 RSI_EthernetConfig.xml create mode 100644 examples/README.md create mode 100644 examples/example_01_start_stop.py create mode 100644 examples/example_02_send_cartesian.py create mode 100644 examples/example_03_send_joint.py create mode 100644 examples/example_04_external_axes.py create mode 100644 examples/example_05_digital_io.py create mode 100644 examples/example_06_logging_csv.py create mode 100644 examples/example_07_graphing_live.py create mode 100644 examples/example_08_safety_limits.py create mode 100644 examples/example_09_trajectory_cartesian.py create mode 100644 examples/example_10_shutdown_safe.py create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 src/RSIPI.egg-info/PKG-INFO create mode 100644 src/RSIPI.egg-info/SOURCES.txt create mode 100644 src/RSIPI.egg-info/dependency_links.txt create mode 100644 src/RSIPI.egg-info/requires.txt create mode 100644 src/RSIPI.egg-info/top_level.txt create mode 100644 src/RSIPI/__init__.py create mode 100644 src/RSIPI/__pycache__/__init__.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/config_parser.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/echo_server_gui.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/inject_rsi_to_krl.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/krl_to_csv_parser.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/kuka_visualiser.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/live_plotter.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/network_handler.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/rsi_api.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/rsi_client.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/rsi_config.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/rsi_config.cpython-313.pyc create mode 100644 src/RSIPI/__pycache__/rsi_echo_server.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/rsi_graphing.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/rsi_limit_parser.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/safety_manager.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/static_plotter.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/trajectory_planner.cpython-311.pyc create mode 100644 src/RSIPI/__pycache__/xml_handler.cpython-311.pyc create mode 100644 src/RSIPI/config_parser.py create mode 100644 src/RSIPI/echo_server_gui.py create mode 100644 src/RSIPI/inject_rsi_to_krl.py create mode 100644 src/RSIPI/krl_to_csv_parser.py create mode 100644 src/RSIPI/kuka_visualiser.py create mode 100644 src/RSIPI/live_plotter.py create mode 100644 src/RSIPI/network_handler.py create mode 100644 src/RSIPI/rsi_api.py create mode 100644 src/RSIPI/rsi_cli.py create mode 100644 src/RSIPI/rsi_client.py create mode 100644 src/RSIPI/rsi_config.py create mode 100644 src/RSIPI/rsi_echo_server.py create mode 100644 src/RSIPI/rsi_graphing.py create mode 100644 src/RSIPI/rsi_limit_parser.py create mode 100644 src/RSIPI/safety_manager.py create mode 100644 src/RSIPI/static_plotter.py create mode 100644 src/RSIPI/trajectory_planner.py create mode 100644 src/RSIPI/xml_handler.py create mode 100644 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-311.pyc create mode 100644 src/__pycache__/__init__.cpython-313.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d83bb2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + RSI-PI + Copyright (C) 2025 adam + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..dad8dbc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +recursive-include src/RSIPI *.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bebca3 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# RSIPI: Robot Sensor Interface - Python Integration + +RSIPI is a high-performance, Python-based communication and control system designed for real-time interfacing with KUKA robots using the Robot Sensor Interface (RSI) protocol. It provides both a robust **API** for developers and a powerful **Command Line Interface (CLI)** for researchers and engineers who need to monitor, control, and analyse robotic movements in real time. + +--- + +🛡️ Safety Notice +RSIPI is a powerful tool that directly interfaces with industrial robotic systems. Improper use can lead to dangerous movements, property damage, or personal injury. + +⚠️ Safety Guidelines +- **Test in Simulation First:** Always verify your RSI communication and trajectories using simulation tools before deploying to a live robot. +- **Enable Emergency Stops:** Ensure all safety hardware (E-Stop, fencing, light curtains) is active and functioning correctly. +- **Supervised Operation Only:** Run RSIPI only in supervised environments with trained personnel present. +- **Limit Movement Ranges:** Use KUKA Workspaces or software limits to constrain movement, especially when testing new code. +- **Use Logging for Debugging:** Avoid debugging while RSI is active; instead, enable CSV logging and review logs post-run. +- **Secure Network Configuration:** Ensure your RSI network is on a closed, isolated interface to avoid external interference or spoofing. +- **Never Rely on RSIPI for Safety:** RSIPI is not a safety-rated system. Do not use it in applications where failure could result in harm. + +--- + +## 📄 Description + +RSIPI allows users to: +- Communicate with KUKA robots using the RSI XML-based protocol. +- Dynamically update control variables (TCP position, joint angles, I/O, external axes, etc.). +- Log and visualise robot movements with live graphs and static plots. +- Analyse motion data and compare planned vs actual trajectories. +- Parse and inject RSI into KRL programs. +- Simulate robot behaviour using a realistic Echo Server. +- Enforce safety limits and manage emergency stops. + +### Target Audience +- **Researchers** working on advanced robotic applications, control algorithms, and feedback systems. +- **Engineers** developing robotic workflows or automated processes. +- **Educators** using real robots in coursework or lab environments. +- **Students** learning about robot control systems and data-driven motion planning. + +--- + +## 📊 Features +- Real-time network communication with KUKA RSI over UDP. +- Structured logging to CSV with British date formatting. +- Background execution and live variable updates. +- Fully-featured Python API for scripting or external integration. +- CLI for interactive control, trajectory planning, and live monitoring. +- Real-time and post-analysis graphing (live TCP, joints, force, acceleration). +- Safety management: emergency stop, limit enforcement, safety override. +- KUKA KRL `.src/.dat` parsing and RSI injection tools. +- Echo Server and GUI for offline simulation and testing. +- Deviation and force spike alerts during live operation. + +--- + +## 📊 API Overview (`rsi_api.py`) + +### Initialization +```python +from src.RSIPI import rsi_api +api = rsi_api.RSIAPI(config_file='examples/RSI_EthernetConfig.xml') +``` + +### Selected Methods +| Method | CLI | API | Description | +|--------|-----|-----|-------------| +| `start_rsi()` | ✅ | ✅ | Starts RSI communication (non-blocking). | +| `stop_rsi()` | ✅ | ✅ | Stops RSI communication. | +| `update_variable(path, value)` | ✅ | ✅ | Dynamically updates a send variable (e.g. `RKorr.X`). | +| `get_variable(path)` | ✅ | ✅ | Retrieves the latest value of any variable. | +| `plan_linear_cartesian(start, end, steps)` | ✅ | ✅ | Create Cartesian paths. | +| `plan_linear_joint(start, end, steps)` | ✅ | ✅ | Create Joint-space paths. | +| `execute_trajectory(traj, rate)` | ✅ | ✅ | Execute planned trajectory live. | +| `enable_alerts(True/False)` | ✅ | ✅ | Enable or disable deviation/force alerts. | +| `start_live_plot(mode)` | ✅ | ✅ | Live graph position, velocity, force, etc. | +| `generate_plot(csv, type)` | ✅ | ✅ | Static graphing from CSV files. | +| `export_movement_data(filename)` | ✅ | ✅ | Export recorded motion as CSV. | +| `parse_krl_to_csv(src, dat, output)` | ✅ | ✅ | Extract TCP points from KRL programs. | +| `inject_rsi(input, output, config)` | ✅ | ✅ | Add RSI startup code to a KRL file. | + +_(Full API details available in `rsi_api.py`.)_ + +--- + +## 🔧 CLI Overview (`rsi_cli.py`) + +Start the CLI: +```bash +python main.py --cli +``` + +### Selected Commands +| Command | Description | +|---------|-------------| +| `start` / `stop` | Start or stop RSI client. | +| `set ` | Update send variable. | +| `get ` | Get latest receive variable. | +| `move_cartesian`, `move_joint` | Move robot using planned trajectories. | +| `queue_cartesian`, `queue_joint` | Queue trajectory steps. | +| `execute_queue` | Run queued trajectories. | +| `alerts on/off` | Enable or disable alerts. | +| `graph show/compare` | Plot or compare test runs. | +| `log start/stop/status` | Manage CSV logging. | +| `plot ` | Static plotting (position, velocity, deviation, etc.). | +| `safety-stop`, `safety-reset`, `safety-status` | Emergency stop and limit management. | +| `krlparse ` | Parse KRL to CSV. | +| `inject_rsi [out] [config]` | Inject RSI code into KRL file. | + +--- + +## 📃 Example Usage + +### Update TCP position live +```python +api.start_rsi() +api.update_variable('RKorr.X', 100.0) +api.update_variable('RKorr.Y', 50.0) +``` + +### Plan and execute a Cartesian move +```python +start_pose = {'X': 0, 'Y': 0, 'Z': 500} +end_pose = {'X': 200, 'Y': 0, 'Z': 500} +traj = api.plan_linear_cartesian(start_pose, end_pose, steps=100) +api.execute_trajectory(traj, rate=0.012) +``` + +### CLI session sample +```bash +> start +> set RKorr.X 150 +> move_cartesian X=0,Y=0,Z=500 X=200,Y=0,Z=500 steps=100 rate=0.012 +> graph show my_log.csv +> log start +> stop +``` + +--- + +## 📅 Output and Logs +- CSV logs saved to `logs/` folder. +- Each log includes British timestamp, sent/received variables. +- Static plots exportable as PNG/PDF. +- Live plots include alert messages and deviation tracking. + +--- + +## 🚀 Getting Started +1. Connect robot and PC via Ethernet. +2. Deploy KUKA RSI program with matching config. +3. Install Python dependencies: +```bash +pip install -r requirements.txt +``` +4. Run `main.py` or import `RSIAPI` in your Python scripts. + +--- + +## 🔖 Citation +If you use RSIPI in your research, please cite: +```bibtex +@software{rsipi2025, + author = {RSIPI Development Team}, + title = {RSIPI: Robot Sensor Interface - Python Integration}, + year = {2025}, + url = {https://github.com/your-org/rsipi}, + note = {Accessed: [insert date]} +} +``` + +--- + +## ⚖️ License +RSIPI is licensed under the MIT License. + +--- + +## 🚧 Disclaimer +RSIPI is intended for research and experimental purposes only. Always ensure safe operation with appropriate safety measures in place. + diff --git a/RSI_EthernetConfig.xml b/RSI_EthernetConfig.xml new file mode 100644 index 0000000..4b8466b --- /dev/null +++ b/RSI_EthernetConfig.xml @@ -0,0 +1,42 @@ + + + 10.10.10.10 + 64000 + ImFree + FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..cf252f3 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,18 @@ +# RSIPI Example Scripts + +This folder contains example scripts demonstrating key features of the RSIPI library. + +| Example | Description | +|:--------|:------------| +| `example_01_start_stop.py` | Start and stop RSI communication | +| `example_02_send_cartesian.py` | Move the robot TCP | +| `example_03_send_joint.py` | Move robot joints | +| `example_04_external_axes.py` | Move external axes | +| `example_05_digital_io.py` | Write digital outputs | +| `example_06_logging_csv.py` | Record robot data to CSV | +| `example_07_graphing_live.py` | Live plot robot movements | +| `example_08_safety_limits.py` | Apply and enforce motion limits | +| `example_09_trajectory_cartesian.py` | Execute simple Cartesian path | +| `example_10_shutdown_safe.py` | Safe shutdown with emergency handling | + +--- diff --git a/examples/example_01_start_stop.py b/examples/example_01_start_stop.py new file mode 100644 index 0000000..e8e1b44 --- /dev/null +++ b/examples/example_01_start_stop.py @@ -0,0 +1,11 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() + +rsi.start_rsi() + +print("RSI connection started. Press Enter to stop.") +input() + +rsi.stop_rsi() +print("RSI connection stopped.") diff --git a/examples/example_02_send_cartesian.py b/examples/example_02_send_cartesian.py new file mode 100644 index 0000000..ae120fd --- /dev/null +++ b/examples/example_02_send_cartesian.py @@ -0,0 +1,9 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() +rsi.start_rsi() + +# Move TCP 50mm along X-axis +rsi.update_cartesian(x=50, y=0, z=0) + +rsi.stop_rsi() diff --git a/examples/example_03_send_joint.py b/examples/example_03_send_joint.py new file mode 100644 index 0000000..d9f7712 --- /dev/null +++ b/examples/example_03_send_joint.py @@ -0,0 +1,9 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() +rsi.start_rsi() + +# Move Joint A1 by 10 degrees +rsi.update_joints(a1=10) + +rsi.stop_rsi() diff --git a/examples/example_04_external_axes.py b/examples/example_04_external_axes.py new file mode 100644 index 0000000..f1e0b19 --- /dev/null +++ b/examples/example_04_external_axes.py @@ -0,0 +1,9 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() +rsi.start_rsi() + +# Move external axis E1 by 100mm +rsi.update_external(e1=100) + +rsi.stop_rsi() diff --git a/examples/example_05_digital_io.py b/examples/example_05_digital_io.py new file mode 100644 index 0000000..4fb323a --- /dev/null +++ b/examples/example_05_digital_io.py @@ -0,0 +1,9 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() +rsi.start_rsi() + +# Set digital output (e.g., to open gripper) +rsi.update_digital_io(125) # Example binary pattern + +rsi.stop_rsi() diff --git a/examples/example_06_logging_csv.py b/examples/example_06_logging_csv.py new file mode 100644 index 0000000..9dd35b6 --- /dev/null +++ b/examples/example_06_logging_csv.py @@ -0,0 +1,11 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() +rsi.enable_csv_logging() + +rsi.start_rsi() + +print("Logging robot data to CSV. Press Enter to stop.") +input() + +rsi.stop_rsi() diff --git a/examples/example_07_graphing_live.py b/examples/example_07_graphing_live.py new file mode 100644 index 0000000..72d2d6f --- /dev/null +++ b/examples/example_07_graphing_live.py @@ -0,0 +1,11 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() +rsi.enable_graphing() + +rsi.start_rsi() + +print("Live graphing started. Press Enter to stop.") +input() + +rsi.stop_rsi() diff --git a/examples/example_08_safety_limits.py b/examples/example_08_safety_limits.py new file mode 100644 index 0000000..8498fd7 --- /dev/null +++ b/examples/example_08_safety_limits.py @@ -0,0 +1,14 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() + +# Set X axis soft limits +rsi.set_safety_limit(axis="X", min_value=-500, max_value=500) + +rsi.start_rsi() + +try: + while True: + pass +except KeyboardInterrupt: + rsi.stop_rsi() \ No newline at end of file diff --git a/examples/example_09_trajectory_cartesian.py b/examples/example_09_trajectory_cartesian.py new file mode 100644 index 0000000..41c3a22 --- /dev/null +++ b/examples/example_09_trajectory_cartesian.py @@ -0,0 +1,20 @@ +from RSIPI import rsi_api +import time + +rsi = rsi_api.RSIAPI() +rsi.start_rsi() + +# Plan simple trajectory +points = [ + {"x": 0, "y": 0, "z": 0}, + {"x": 50, "y": 0, "z": 0}, + {"x": 50, "y": 50, "z": 0}, + {"x": 0, "y": 50, "z": 0}, + {"x": 0, "y": 0, "z": 0} +] + +for point in points: + rsi.update_cartesian(**point) + time.sleep(0.5) + +rsi.stop_rsi() diff --git a/examples/example_10_shutdown_safe.py b/examples/example_10_shutdown_safe.py new file mode 100644 index 0000000..3d413b2 --- /dev/null +++ b/examples/example_10_shutdown_safe.py @@ -0,0 +1,14 @@ +from RSIPI import rsi_api + +rsi = rsi_api.RSIAPI() + +try: + rsi.start_rsi() + print("Press Ctrl+C to stop RSI safely.") + while True: + pass + +except KeyboardInterrupt: + print("\nEmergency stop triggered.") + rsi.safety_stop() + rsi.stop_rsi() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ce9b06 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "RSIPI" +version = "0.1.1" +description = "Robot Sensor Interface Python Integration (RSIPI) for KUKA RSI control" +readme = "README.md" +requires-python = ">=3.8" +license = { file = "LICENSE" } +authors = [ + { name="Adam Morgan", email="yadam.j.morgan@swansea.ac.uk" } +] +dependencies = [ + "pandas>=2.0", + "numpy>=1.22", + "matplotlib>=3.5", + "lxml>=4.9", + "scipy>=1.8", +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8287ce9 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +from setuptools import setup, find_packages + +setup( + name="RSIPI", + version="0.1.1", + description="Robot Sensor Interface Python Integration (RSIPI) for KUKA RSI control", + long_description=open("README.md", encoding="utf-8").read(), + long_description_content_type="text/markdown", + author="YAdam Morgan", + author_email="adam.j.morgan@swansea.ac.uk", + license="MIT", + python_requires=">=3.8", + packages=find_packages(where="src"), + package_dir={"": "src"}, + install_requires=[ + "pandas>=2.0", + "numpy>=1.22", + "matplotlib>=3.5", + "lxml>=4.9", + "scipy>=1.8", + ], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + include_package_data=True, +) diff --git a/src/RSIPI.egg-info/PKG-INFO b/src/RSIPI.egg-info/PKG-INFO new file mode 100644 index 0000000..a6d5435 --- /dev/null +++ b/src/RSIPI.egg-info/PKG-INFO @@ -0,0 +1,435 @@ +Metadata-Version: 2.4 +Name: RSIPI +Version: 0.1.1 +Summary: Robot Sensor Interface Python Integration (RSIPI) for KUKA RSI control +Author: YAdam Morgan +Author-email: Adam Morgan +License: GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + + Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + + When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + + A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + + The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + + An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + + The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based on the Program. + + To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + + A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those + subprograms and other parts of the work. + + The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + + When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + + A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + + If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + + The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + + All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + + However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + + Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + + If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent + license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + + A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may + not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + + Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + + Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + RSI-PI + Copyright (C) 2025 adam + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + + Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + + You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . + +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: pandas>=2.0 +Requires-Dist: numpy>=1.22 +Requires-Dist: matplotlib>=3.5 +Requires-Dist: lxml>=4.9 +Requires-Dist: scipy>=1.8 +Dynamic: author +Dynamic: license-file +Dynamic: requires-python + +# RSIPI: Robot Sensor Interface - Python Integration + +RSIPI is a high-performance, Python-based communication and control system designed for real-time interfacing with KUKA robots using the Robot Sensor Interface (RSI) protocol. It provides both a robust **API** for developers and a powerful **Command Line Interface (CLI)** for researchers and engineers who need to monitor, control, and analyse robotic movements in real time. + +--- + +🛡️ Safety Notice +RSIPI is a powerful tool that directly interfaces with industrial robotic systems. Improper use can lead to dangerous movements, property damage, or personal injury. + +⚠️ Safety Guidelines +- **Test in Simulation First:** Always verify your RSI communication and trajectories using simulation tools before deploying to a live robot. +- **Enable Emergency Stops:** Ensure all safety hardware (E-Stop, fencing, light curtains) is active and functioning correctly. +- **Supervised Operation Only:** Run RSIPI only in supervised environments with trained personnel present. +- **Limit Movement Ranges:** Use KUKA Workspaces or software limits to constrain movement, especially when testing new code. +- **Use Logging for Debugging:** Avoid debugging while RSI is active; instead, enable CSV logging and review logs post-run. +- **Secure Network Configuration:** Ensure your RSI network is on a closed, isolated interface to avoid external interference or spoofing. +- **Never Rely on RSIPI for Safety:** RSIPI is not a safety-rated system. Do not use it in applications where failure could result in harm. + +--- + +## 📄 Description + +RSIPI allows users to: +- Communicate with KUKA robots using the RSI XML-based protocol. +- Dynamically update control variables (TCP position, joint angles, I/O, external axes, etc.). +- Log and visualise robot movements with live graphs and static plots. +- Analyse motion data and compare planned vs actual trajectories. +- Parse and inject RSI into KRL programs. +- Simulate robot behaviour using a realistic Echo Server. +- Enforce safety limits and manage emergency stops. + +### Target Audience +- **Researchers** working on advanced robotic applications, control algorithms, and feedback systems. +- **Engineers** developing robotic workflows or automated processes. +- **Educators** using real robots in coursework or lab environments. +- **Students** learning about robot control systems and data-driven motion planning. + +--- + +## 📊 Features +- Real-time network communication with KUKA RSI over UDP. +- Structured logging to CSV with British date formatting. +- Background execution and live variable updates. +- Fully-featured Python API for scripting or external integration. +- CLI for interactive control, trajectory planning, and live monitoring. +- Real-time and post-analysis graphing (live TCP, joints, force, acceleration). +- Safety management: emergency stop, limit enforcement, safety override. +- KUKA KRL `.src/.dat` parsing and RSI injection tools. +- Echo Server and GUI for offline simulation and testing. +- Deviation and force spike alerts during live operation. + +--- + +## 📊 API Overview (`rsi_api.py`) + +### Initialization +```python +from src.RSIPI import rsi_api +api = rsi_api.RSIAPI(config_file='examples/RSI_EthernetConfig.xml') +``` + +### Selected Methods +| Method | CLI | API | Description | +|--------|-----|-----|-------------| +| `start_rsi()` | ✅ | ✅ | Starts RSI communication (non-blocking). | +| `stop_rsi()` | ✅ | ✅ | Stops RSI communication. | +| `update_variable(path, value)` | ✅ | ✅ | Dynamically updates a send variable (e.g. `RKorr.X`). | +| `get_variable(path)` | ✅ | ✅ | Retrieves the latest value of any variable. | +| `plan_linear_cartesian(start, end, steps)` | ✅ | ✅ | Create Cartesian paths. | +| `plan_linear_joint(start, end, steps)` | ✅ | ✅ | Create Joint-space paths. | +| `execute_trajectory(traj, rate)` | ✅ | ✅ | Execute planned trajectory live. | +| `enable_alerts(True/False)` | ✅ | ✅ | Enable or disable deviation/force alerts. | +| `start_live_plot(mode)` | ✅ | ✅ | Live graph position, velocity, force, etc. | +| `generate_plot(csv, type)` | ✅ | ✅ | Static graphing from CSV files. | +| `export_movement_data(filename)` | ✅ | ✅ | Export recorded motion as CSV. | +| `parse_krl_to_csv(src, dat, output)` | ✅ | ✅ | Extract TCP points from KRL programs. | +| `inject_rsi(input, output, config)` | ✅ | ✅ | Add RSI startup code to a KRL file. | + +_(Full API details available in `rsi_api.py`.)_ + +--- + +## 🔧 CLI Overview (`rsi_cli.py`) + +Start the CLI: +```bash +python main.py --cli +``` + +### Selected Commands +| Command | Description | +|---------|-------------| +| `start` / `stop` | Start or stop RSI client. | +| `set ` | Update send variable. | +| `get ` | Get latest receive variable. | +| `move_cartesian`, `move_joint` | Move robot using planned trajectories. | +| `queue_cartesian`, `queue_joint` | Queue trajectory steps. | +| `execute_queue` | Run queued trajectories. | +| `alerts on/off` | Enable or disable alerts. | +| `graph show/compare` | Plot or compare test runs. | +| `log start/stop/status` | Manage CSV logging. | +| `plot ` | Static plotting (position, velocity, deviation, etc.). | +| `safety-stop`, `safety-reset`, `safety-status` | Emergency stop and limit management. | +| `krlparse ` | Parse KRL to CSV. | +| `inject_rsi [out] [config]` | Inject RSI code into KRL file. | + +--- + +## 📃 Example Usage + +### Update TCP position live +```python +api.start_rsi() +api.update_variable('RKorr.X', 100.0) +api.update_variable('RKorr.Y', 50.0) +``` + +### Plan and execute a Cartesian move +```python +start_pose = {'X': 0, 'Y': 0, 'Z': 500} +end_pose = {'X': 200, 'Y': 0, 'Z': 500} +traj = api.plan_linear_cartesian(start_pose, end_pose, steps=100) +api.execute_trajectory(traj, rate=0.012) +``` + +### CLI session sample +```bash +> start +> set RKorr.X 150 +> move_cartesian X=0,Y=0,Z=500 X=200,Y=0,Z=500 steps=100 rate=0.012 +> graph show my_log.csv +> log start +> stop +``` + +--- + +## 📅 Output and Logs +- CSV logs saved to `logs/` folder. +- Each log includes British timestamp, sent/received variables. +- Static plots exportable as PNG/PDF. +- Live plots include alert messages and deviation tracking. + +--- + +## 🚀 Getting Started +1. Connect robot and PC via Ethernet. +2. Deploy KUKA RSI program with matching config. +3. Install Python dependencies: +```bash +pip install -r requirements.txt +``` +4. Run `main.py` or import `RSIAPI` in your Python scripts. + +--- + +## 🔖 Citation +If you use RSIPI in your research, please cite: +```bibtex +@software{rsipi2025, + author = {RSIPI Development Team}, + title = {RSIPI: Robot Sensor Interface - Python Integration}, + year = {2025}, + url = {https://github.com/your-org/rsipi}, + note = {Accessed: [insert date]} +} +``` + +--- + +## ⚖️ License +RSIPI is licensed under the MIT License. + +--- + +## 🚧 Disclaimer +RSIPI is intended for research and experimental purposes only. Always ensure safe operation with appropriate safety measures in place. + diff --git a/src/RSIPI.egg-info/SOURCES.txt b/src/RSIPI.egg-info/SOURCES.txt new file mode 100644 index 0000000..1ddb924 --- /dev/null +++ b/src/RSIPI.egg-info/SOURCES.txt @@ -0,0 +1,30 @@ +LICENSE +MANIFEST.in +README.md +pyproject.toml +setup.py +src/RSIPI/__init__.py +src/RSIPI/config_parser.py +src/RSIPI/echo_server_gui.py +src/RSIPI/inject_rsi_to_krl.py +src/RSIPI/krl_to_csv_parser.py +src/RSIPI/kuka_visualiser.py +src/RSIPI/live_plotter.py +src/RSIPI/network_handler.py +src/RSIPI/rsi_api.py +src/RSIPI/rsi_cli.py +src/RSIPI/rsi_client.py +src/RSIPI/rsi_config.py +src/RSIPI/rsi_echo_server.py +src/RSIPI/rsi_graphing.py +src/RSIPI/rsi_limit_parser.py +src/RSIPI/safety_manager.py +src/RSIPI/static_plotter.py +src/RSIPI/trajectory_planner.py +src/RSIPI/xml_handler.py +src/RSIPI.egg-info/PKG-INFO +src/RSIPI.egg-info/SOURCES.txt +src/RSIPI.egg-info/dependency_links.txt +src/RSIPI.egg-info/requires.txt +src/RSIPI.egg-info/top_level.txt +tests/test_rsipi.py \ No newline at end of file diff --git a/src/RSIPI.egg-info/dependency_links.txt b/src/RSIPI.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/RSIPI.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/RSIPI.egg-info/requires.txt b/src/RSIPI.egg-info/requires.txt new file mode 100644 index 0000000..1279b93 --- /dev/null +++ b/src/RSIPI.egg-info/requires.txt @@ -0,0 +1,5 @@ +pandas>=2.0 +numpy>=1.22 +matplotlib>=3.5 +lxml>=4.9 +scipy>=1.8 diff --git a/src/RSIPI.egg-info/top_level.txt b/src/RSIPI.egg-info/top_level.txt new file mode 100644 index 0000000..df52f88 --- /dev/null +++ b/src/RSIPI.egg-info/top_level.txt @@ -0,0 +1 @@ +RSIPI diff --git a/src/RSIPI/__init__.py b/src/RSIPI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/RSIPI/__pycache__/__init__.cpython-311.pyc b/src/RSIPI/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..706a046b6158a27e5c4475c0e1d326908d2ccbc2 GIT binary patch literal 272 zcmXw!!D<3Q42EYaEu|oRg&gZ)`v6h|t(Qfxi;K5`Wpv{@U^9}8F8d^XfF4TwTI~Y} z-aK`-bpOMj?@w|_KKlJT74?NbRa?JX>!knTd=ueKl`2;QJ^xgE74x|rUf!oe^Flaf zcSJ}auPq5$SW?U^E;J^y`$*;?CCJ!Uj3NU_DPYAmII3W_oQ+}OS3U^TE^Hxd!qyXv zVUG3RCU5K-VdEKcB=MX~tY8Vxo&x)w5nh``o+eq^VWgsPz>% literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/config_parser.cpython-311.pyc b/src/RSIPI/__pycache__/config_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae7401255d89746f1efb7a90c8635d2f539c68f5 GIT binary patch literal 10721 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 SY2bJF{MV%YzxL=QPxOCF?G8l% literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/echo_server_gui.cpython-311.pyc b/src/RSIPI/__pycache__/echo_server_gui.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41df210b8508485dbeb9e45ca8b143a0f639f95f GIT binary patch literal 14455 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 Ju;@de^4~!iIFkSX literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/krl_to_csv_parser.cpython-311.pyc b/src/RSIPI/__pycache__/krl_to_csv_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d40df5780df1a31a2360db2f5a0f5c3e78352be8 GIT binary patch literal 6165 zcmc&&U2GFq7QSQ8_}_#;oY>&Ngrp@Vkc6Z(z(S<_(bAM8C6t9Utz|ru#Nc?+JL80S z#}d-^AuDB9(F!flQq`b}rh`HcIDb{F_D(!Zqz8OI!mfe;1oEh7R zL$b;~_0G(h`*ZG{d(YhSopbIdZnuL#N_V&?CMyW}8y1R*Eo2`512Wf$Km;aAMvONz z!sunwhzZ`NsCkkdVHsi~uMolf9ue3(CPMDQW26yIvQ=GnS;2OPff3;`5(6t6)}Mg=2do+hFq(6a3L+JS z-bh#p7Z~+dvD@Q)P`pM&f}#L1Fe7Hc^d1>uQ2-;HzygIu1GNg4VG^)v_Wska4~3{K z(!{KThol!|zW>esE*{$WW|@ZhP)y*Pg^s`X;Rg1wNg5}3&^|h#s zN`j?k)dZ#DYKg*?8c4aIfvYs;IDt}ez^t*d7#*WlV5PV>Wb6czdv>q)=;&Zf?4^-ukw3r>&xB&K z7~FP_rdhS3k#Q zZM)L?{TP@@xb$nFV1Y;fk-vl5YeZSqcCnNf7zH~P(#4W?mg`}XQBd54q>_s(ztbd` z@3D8_J9qJ0dOAdsCe_qS-i{~DDwA5ZNs1hVsOEBgOR_3UM^!e3vwkiKTse1Y)ofOn z%eUZWa3jg79IU2<9McTlvAUdE7c0~?vth{mw4DHY!Bs9dKoL;$dUBA+?2PHM`8=6n z7;>I4a2WuQ5OocN?OW7*&{;$2Q8iYp`lN(9!H1>ENzidw2L)h`P~K1yzENx*Z{E)j zof+cK4YeO^4H(*uV<+6B7odJrZfYEzX$oizea6L=u>*%cWi*=@3rhm*{vU8x3pi%@<+gkC#9Vni=db&y|NZ_& z=ElIyfn|T|lD{?MZ_E1Imi>p9{D(9Cqgnsag}z*kKh>LUIGV1(8-~w%o0q+Zmb`~P z6EogpS?{s5_t?|C6TfMODd_Xi$s?T66C^8j8|I!qeY#E zy8w>APTEx4rhu#jPw>Y|sT}N*cEwX_BC5UoYl5ZVW}Q~;qwu$rj-=C0REJ=jCX`od zR(dMFmAvXKAEmscT2;p}V2*A3Uww*P^Ox(1vSVfJ1~A*9}e= zxLlgWC}^Ar4$iD*za-8Er*UA-3|WoS^D-i}j@boL2Hha%=b;;g51r)S7ooexclBbr z?zO*k|M^s=dL&ytGUr?$x~uH%1?b-HZhu68AavI+d-pAQ_btAZ@wR5Ytp(_=tZd&b zbnn^wNz?77#n7$hkDC|zzpLH7a3WW?f4Q!0sjls__Do$zwhruu!_%sh>s+oO@Jave z{#&O$KDFG?vDDC!X*iN?IC9R(*km#%*a?uTuMz7+5GeIETHnrZ9Fw)Lb=EVke3PxXJ_)^VTveE%2wjf$sFAJ!kp z6Q&lvT_m~YmXswW-*Tql%A27YtwIIyx8$qHf%b*os{^S+nX14)^Ls%;c{4~YPm1Ib z=AM*_D-G-AbZb-WxgLZ3GW^J|dQ5$F=5LO^+z~Og7rZo12l{~zL-!sbc^&1YOj%T2)}MKNz-b_Rb>QlI0g0|cL!Y1yCpXiTu!r6 zazF`Lab#7qYPz?^EkkEBwTinq0kS5F0fe%Gi@3_&1>F8u(gHOlr*?gZRb6Wxtg4m% z5x%;v;|flwMYXO!l3-gK6mQ6B-!!LV)11ytbGkOo>E1NwmQ8b3tmPD#n~rje#SoPf zwkF(Q|G4?TE4bSVX(a#oN)z4`Jtq%qTg#Z|X&60%zC^sVIjiP+;;P;mXlP^W4rZCJ6 z#b$XyjEagVv`j{18S9KAgcZXUaA>w+@!dE?2cl*flH}9kn8uvbnDZJlqA^_>)2%T* zbT7z|HUiO@8O<!dzPlb2sLElpjdc;*o}N>H%zWJRFA5ek>l1&SC_Wuo%I7!d6^m zz^NNm%?vTQ=D_nDUC>l>&Y-|2#1I_x=wXa*IyLjSsAx95jY>0`H7bpdgU~eVOo+xX z!$x9bQovTQuu@!^iWiPFx?w?@4P$>jCJk2=T78Y+m>CsmR!3sE%TH91_c&ju&J!Syh|^Q^1PK&@c!{SW^=_uBaba*_@cXKrR|nG# zr@j_}z@4cY$W{%c^*dj$%fO8yGz4kTN4ij-=yY}cMtR+;t{uc+{;`eNx6ZrLp3rA+ z0fGBZ6~-HA#&#iVyO1v4dZc%~x3$k>e&AvIeAWkk2IxS*h6hZjaKWJF2nHu5As)rN zD;Rt?9*P!Ptihllg@Zu~s7Po%l0!%afqY6R`r!Hwq-gJSg5kN02ZC2hSKegiY!D^` zaj;xNo|K5o#X&qa z43T!ybI^c+oxB7DJTZpJ5yut%%8@Ns^eac~SM)1K>eJV;#$E)e>8kVXL!jb(*c-pvJSrdeYVV VGHg?pZAusKyq&N+U}SyG{{^2qiTnTn literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/kuka_visualiser.cpython-311.pyc b/src/RSIPI/__pycache__/kuka_visualiser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7109eeafc701219020cd77e95c848d8557d6a0f GIT binary patch literal 9074 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 literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/live_plotter.cpython-311.pyc b/src/RSIPI/__pycache__/live_plotter.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56b19c13ef34f82678be4c089671e3ecbaac65d7 GIT binary patch literal 12095 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 literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/rsi_client.cpython-311.pyc b/src/RSIPI/__pycache__/rsi_client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe1444605d36f53706361705a4225923c027a3bb GIT binary patch literal 6213 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 K`vY%j7ybum*dOr# literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/rsi_config.cpython-311.pyc b/src/RSIPI/__pycache__/rsi_config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92ebcccfc14da8750f243e088d64944824d69e2e GIT binary patch literal 9606 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 Khh3hMCH`;lzg!^z literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/rsi_config.cpython-313.pyc b/src/RSIPI/__pycache__/rsi_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db26a03847e34bffcdb7516c7660ca49aed1cb33 GIT binary patch literal 7109 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 literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/rsi_echo_server.cpython-311.pyc b/src/RSIPI/__pycache__/rsi_echo_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d786f2f5c37e2ecf5776dba67ecea85950c57df2 GIT binary patch literal 10721 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 literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/rsi_graphing.cpython-311.pyc b/src/RSIPI/__pycache__/rsi_graphing.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..586ccc24767d0feb260aaf61bb98e2ea85e54c4f GIT binary patch literal 14522 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 literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/rsi_limit_parser.cpython-311.pyc b/src/RSIPI/__pycache__/rsi_limit_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71add9a4ee1e232cf6898a14fb1b4ad37f452b33 GIT binary patch literal 3607 zcmd57&-6`m!RzblGTSENP#PGp-FZHabbJ9cD6GHt4=?8Fo#JFr~|!dknM>5xlh zcWp_|5^&HoPyzvAg#aR&80E=T;fv5qbL_F`X(TO1c{`x~=<|6Iw9ykkEWX!-(7%jMt=X#1(;jd(5s!E(h0@f#jY4VL zO*@5n+sEi8%~R7TVt-b{B|f&aMP&r&K?sI6>7 zskcYqJ+Ak%wj|2hRa?PWVPE4ZYt%T(QM1pov`XKhYvclCwZHK;-u?*Ie_YQY>rm-} zb3YRyp0=)%b*jz>)cTvMgLl4f&X*pk&R1mUvd)d8+0U~s)z!$4cZceWX5EUv-a!_Y zr@^vD%z9Ke@2%$HR6VMlXNDoj`f0$MEV$3aV}LhXaKDMa3wVnKZ#D57fd9gRvnDP8 z-e$oACjJ57!7AR)AAlVPvR>80Ajqs;^?r%?&_lbyG;^&W9Tj9fxSm`8C+ON)o&U*y zp{w}6*R^2O_P)kQR^b;a;U1y$3IJ54qJEoBf5mG%$MkL4*@YA=|#T?46j z)|D9#j^7iSrLK4fJ}V=BWt`2OG8_MD{;)_v(+qx#I6w^?Ikn}3ww@$f~y zUv2)#qxwH`sm-c?!KOB;mG^m8ZGxLQa{upM3~iE=WI@Ipj!JSex{^-eg=9*={ZrEu ziCcGsc?B=>)? z$$4c6Pd8>&=r}22<+flhfj`83QWt4 z62wzam$)G9tE)=_LBtE2$)pvn)vO zB-7XLGu3S7AQj@dwzRYl=T;j08#Vq7D}OJ(mw&UyziH*~#evVf%dJdhl$f}f5+Zg& z#RzqQP#5nzE6EYsoDCMZ?%4H->K1Feh&2+AJ^44Z`>v{(gqkA`lSb^u2JJ=!rWRrB z`qjqP#u`D!YV*gQ`~cA+O~f{Kl{l7+A|kFuL6I^Ug}4@yBG08##K|d&l)OdkFhXbp zC60wuhEs@FRyawKmy^nELZ^~448Jd|%ET>*yop9yh#RVLZizD(iX;exHiLU5Zh5X! zKEw+(JC`xbl(^x0PB9MN3F&ZY*-VQ|6IwJ5*>TT2Uz%G<%ES(9k=8~L*W%H)IAb56 z#~jeX;Z_VsZh?2&DC52D`Q!M|OhOdmQu3aF2l4bWC&~hcuZhryyp+|MNe*g8p6QCV-MWGM!FGJYh< zzXAIIebr zR0|y4#UPO_VC==Pd0Ibr{_Akws~voED-Nde6x+|}L!&Q2we~a5%12Bsj-uvZzGKIC zQ1cz!r4W3?C}x{$=O>6womBK9zF(&%fCxJ`_^Zq(PHG;z2ggOzVOd8 z&+p4l6zbIIz&Cz2cWE=RktntuF0~yl`okrE7<6>JZJIT^&NLh4pLZ6S-V)PWMh?&0 zIvdQ%d0S5Y;_O;n58_R6L)@YZv&LUBc(N2cxiQjqP#m=&;uE zTH#Q!<;b&Lrc0x{p2E9`A`TLOTQ3S1sUSqhQbJk~m8-_(=P)6{?7^f%FvEP0|AeFq vhJPN+bN6sM!{1E_Bhol@jsKwJ5in&tMNvBH(_ZnE>2ZqcE~A%QGyZ=7&NH_Z literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/safety_manager.cpython-311.pyc b/src/RSIPI/__pycache__/safety_manager.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36a57196adf9687ae3bd7876d22004672613d54f GIT binary patch literal 5360 zcmc&&%}*T374Po(1O^5J#@Obg@Uk(Jh4IJQC}BS$Y=h-^jS0hMy_of=>8^n`Gu@Nw z9hq5mAV;lAXkxSUEUY&cV4k*Zm6y3n`;kELq8El?yB95cv@DUUknWc$MUk z^mM=ac=hqB-ml(!HNR_YtQX*^f7USn(hs+&86;v@PObe25QBbAZ zg6er73Bn`zoM&28#h4KBeu>3##jWLu-z~iOHDvAxRDia^w4{pD9<@#N+!m+3s+amy zAN8w#>QDI9z-{kzKn>m&rh{r7&^peq2Yv(Rhk#$NHUg(XZ2}rn!?ZCWsm)N|1Y=q5O+>(=;D&t5Ix5s-p4WZ8DDXLq}jF%0@$O$syriL|78T4kFJe#DZ z+jlafTUv@bqLNG+Ms|7QN=a+5DVX=b_hJml9U&+FNXm)2s0leC4mHlBN>CDuCC)32 z!Ff0We>e&}*}riqaO+0coVx>9!pGgbepOSf$S}E@#wE+in!VL@0_(yBr<@IRIy-c^ zG<;?4@k!i~=(Xd+q<`>i^yB~m5*gUPE#JN9$SK}<2pa==MdN5Rx@VA;QqA~S#B2MV zRoQ+TNA21TN!hgGpwbLA?VxdsGN!3CBH3P(CKC+Lgo#^7$jgTphG(wo^a9gvQ8Gwk zi?VJ~nY^z1NhcG`m}As5Bjl29(K!Zq(dWqJ_b-o-Nn_RkC@pH% zJegRs<^ho-WSlPYmMkGFd^bsq>ZZX4&&eiL$$3M!n30?@nKA=N9-O#T%wmp%Jr0=0 zqYzCm*}-^R(={s|&$d?`lW4IXtcYMnfvgHUZ9?zyJUP9Q*hoB)*1YQth4!O7Ioh@6 z-41pXIv`zdT!Y^W=poqNv~11WKE7$TpHVBrbfttWXwd+lgH^yHU{$z@{rJPA9GqXM zDWR0TVfl(&LAY5OM-_A8ufT?V?s))hAC=mRf>l>;uX=bZZ*8kjpf}7?8u^yMW-7}$ z@o;I78Bp+wXT_WI2ey|d>c=hAiaFs5HNw&we7i2bRC<_3S{?WnM)YSta}H2%2Nq?fV%Wgjk5@!|4BZ?VTUPh8VMhP=~gd*5bTa-vzDAT_CGMp&`8f&W<3}9e&>0vG(3`D87Zo8Sz=Db^U6- zGq$M$ao(-a)OKhpADVjB(w*;_&c{AL%HOS)>)S2Y^P%f7!p51;-pV(0!&~Ti?cVa{ zneCpFTcOUiiyI#l+K%4M&j5k<$?Hy}Eutk>zu}PI@CbC7C6I(>bBO*U-5pTr_ zD@kFZ6nLl{?bPAoxR=Zxs$N}*F$5h<><>EW>bpOe?~I~iHZE)h59j&&HTpRN{i=P! z{YqQ{*Lfa5QjP`zk79OUlKUE5s$!&<&@5pt#F#Z{Ww z8YPxF!ytm$4g}U2Ze3sgQ@H18xMwTeyB+S$2YdG}5*NaC*bJHKMe+pz0MS{3^Pi_Y z8pP148jWaG9rGQ11^hIVV=qnBTFJ|tcTsXYcdq35;1Mn@#63DjpZkZFjwwLLSz!wB zx*%nTMtMI(oT`~PL~*-|03`jhOKI73Vq|5WDhp;LidFzU0w{yq_6W-5#8VF1!o}`! zM|;_>YRuKJ&tZSp8gz6Q!;=485#Ov;go~Zxbd_3o1LoX=pElfUC+>3ls8r7$s}_F0 zHe!uso~A8-)J`~>DKMbzg}%S3}yNY2X)+=M3UByE@! zw-4i6xE#PuiZZx0FpPTLJ4HE4pIS<53UHn1VwimKhf`-yo+POhfv(p+oF0IhH-}Ds z@fgbn)8Lr%n^$>+h@|zs+}+jw450y_46&u=0Qb0f2n`9~q#x@P@* zP;K;DhXThl_aw}VwMg(Y2S87&rQePe@CD&) z0cmOUb?*q(q&w_8csI5qK(cv|Cy&qaq?R0$d`ICV7D-U@bz;#aP&{#0!HN@`qda-y zBs^b1py4qrx&(?fqxRfak>68GpkH& z@(#{M;?{u?Jy_gIHi0?xw|tLZ2H$oR{Vw|+)WH@DCR()}k0^=-p=p)>cKkO*5k}pg Ne#Ptm{t7Oi>tDlAVJrXu literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/static_plotter.cpython-311.pyc b/src/RSIPI/__pycache__/static_plotter.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5ee19a8b2fdb15c792056330578be651dfc9fad GIT binary patch literal 16410 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 literal 0 HcmV?d00001 diff --git a/src/RSIPI/__pycache__/trajectory_planner.cpython-311.pyc b/src/RSIPI/__pycache__/trajectory_planner.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbdf3941a44f294d44e57bf0c629c8d520067bbe GIT binary patch literal 3237 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 section for IP/port/etc. + config = root.find("CONFIG") + if config is None: + raise ValueError("Missing section in RSI_EthernetConfig.xml") + + self.network_settings = { + "ip": config.find("IP_NUMBER").text.strip() if config.find("IP_NUMBER") is not None else None, + "port": int(config.find("PORT").text.strip()) if config.find("PORT") is not None else None, + "sentype": config.find("SENTYPE").text.strip() if config.find("SENTYPE") is not None else None, + "onlysend": config.find("ONLYSEND").text.strip().upper() == "TRUE" if config.find("ONLYSEND") is not None else False, + } + + print(f"✅ Loaded network settings: {self.network_settings}") + + if None in self.network_settings.values(): + raise ValueError("Missing one or more required network settings (ip, port, sentype, onlysend)") + + # Parse SEND section + send_section = root.find("SEND/ELEMENTS") + if send_section is not None: + for element in send_section.findall("ELEMENT"): + tag = element.get("TAG").replace("DEF_", "") + var_type = element.get("TYPE", "") + self.process_variable_structure(send_vars, tag, var_type) + + # Parse RECEIVE section + receive_section = root.find("RECEIVE/ELEMENTS") + if receive_section is not None: + for element in receive_section.findall("ELEMENT"): + tag = element.get("TAG").replace("DEF_", "") + var_type = element.get("TYPE", "") + self.process_variable_structure(receive_vars, tag, var_type) + + return send_vars, receive_vars + + except Exception as e: + logging.error(f"Error processing config file: {e}") + return {}, {} + + def process_variable_structure(self, var_dict, tag, var_type, indx=""): + """ + Processes and assigns a variable to the dictionary based on its tag and type. + + Args: + var_dict (dict): Dictionary to add variable to. + tag (str): Variable tag (can be nested like Tech.T1). + var_type (str): Variable type (e.g. BOOL, DOUBLE, STRING). + indx (str): Optional index (unused). + """ + tag = tag.replace("DEF_", "") # Remove DEF_ prefix if present + + if tag in self.internal_structure: + # If pre-defined internally, copy structure + internal_value = self.internal_structure[tag] + var_dict[tag] = internal_value.copy() if isinstance(internal_value, dict) else internal_value + elif "." in tag: + # Handle nested dictionary e.g. Tech.T21 -> { 'Tech': { 'T21': 0.0 } } + parent, subkey = tag.split(".", 1) + if parent not in var_dict: + var_dict[parent] = {} + var_dict[parent][subkey] = self.get_default_value(var_type) + else: + # Standard single-value variable + var_dict[tag] = self.get_default_value(var_type) + + @staticmethod + def rename_tech_keys(var_dict): + """ + Combines all Tech.XX keys into a single 'Tech' dictionary. + + Args: + var_dict (dict): The variable dictionary to modify. + """ + tech_data = {} + for key in list(var_dict.keys()): + if key.startswith("Tech."): + tech_data.update(var_dict.pop(key)) + if tech_data: + var_dict["Tech"] = tech_data + + @staticmethod + def get_default_value(var_type): + """ + Returns a default Python value based on RSI TYPE. + + Args: + var_type (str): RSI type attribute. + + Returns: + Default Python value. + """ + if var_type == "BOOL": + return False + elif var_type == "STRING": + return "" + elif var_type == "LONG": + return 0 + elif var_type == "DOUBLE": + return 0.0 + return None + + def get_network_settings(self): + """ + Returns extracted IP, port, and message mode settings. + + Returns: + dict: Network settings extracted from the config file. + """ + return self.network_settings diff --git a/src/RSIPI/echo_server_gui.py b/src/RSIPI/echo_server_gui.py new file mode 100644 index 0000000..a977c75 --- /dev/null +++ b/src/RSIPI/echo_server_gui.py @@ -0,0 +1,201 @@ +import tkinter as tk +from tkinter import ttk, filedialog +import threading +import time +from src.RSIPI.rsi_echo_server import EchoServer +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from mpl_toolkits.mplot3d import Axes3D +import numpy as np +import os +import math + + +class EchoServerGUI: + """ + Graphical interface for running and visualising the RSI Echo Server. + Provides live feedback of robot TCP position and joint states, along with XML message logs. + """ + + def __init__(self, master): + """ + Initialises the GUI, default values, and layout. + + Args: + master (tk.Tk): Root tkinter window. + """ + self.master = master + self.master.title("RSI Echo Server GUI") + self.master.geometry("1300x800") + + # Configurable input variables + self.config_file = tk.StringVar(value="RSI_EthernetConfig.xml") + self.mode = tk.StringVar(value="relative") + self.delay = tk.IntVar(value=4) + self.show_robot = tk.BooleanVar(value=True) + + # Internal state + self.server = None + self.visual_thread = None + self.running = False + self.trace = [] + + self.create_widgets() + + def create_widgets(self): + """Create and layout all UI elements including buttons, entry fields, and plots.""" + frame = ttk.Frame(self.master) + frame.pack(pady=10) + + # Config file input + ttk.Label(frame, text="RSI Config File:").grid(row=0, column=0, padx=5) + ttk.Entry(frame, textvariable=self.config_file, width=50).grid(row=0, column=1, padx=5) + ttk.Button(frame, text="Browse", command=self.browse_file).grid(row=0, column=2) + + # Mode selection + ttk.Label(frame, text="Mode:").grid(row=1, column=0, padx=5) + ttk.Combobox(frame, textvariable=self.mode, values=["relative", "absolute"], width=10).grid(row=1, column=1, sticky='w') + + # Delay input + ttk.Label(frame, text="Delay (ms):").grid(row=2, column=0, padx=5) + ttk.Entry(frame, textvariable=self.delay, width=10).grid(row=2, column=1, sticky='w') + + # Show/hide robot checkbox + ttk.Checkbutton(frame, text="Show Robot Stick Figure", variable=self.show_robot).grid(row=3, column=0, sticky='w') + + # Start/Stop buttons + ttk.Button(frame, text="Start Server", command=self.start_server).grid(row=4, column=0, pady=10) + ttk.Button(frame, text="Stop Server", command=self.stop_server).grid(row=4, column=1, pady=10) + + # Status label + self.status_label = ttk.Label(frame, text="Status: Idle") + self.status_label.grid(row=5, column=0, columnspan=3) + + # 3D Plot setup + self.figure = plt.Figure(figsize=(6, 5)) + self.ax = self.figure.add_subplot(111, projection='3d') + self.canvas = FigureCanvasTkAgg(self.figure, master=self.master) + self.canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=1) + + # XML message display + right_frame = ttk.Frame(self.master) + right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1) + + ttk.Label(right_frame, text="📤 Sent Message").pack() + self.sent_box = tk.Text(right_frame, height=15, width=70) + self.sent_box.pack(pady=5) + + ttk.Label(right_frame, text="📩 Received Message").pack() + self.received_box = tk.Text(right_frame, height=15, width=70) + self.received_box.pack(pady=5) + + def browse_file(self): + """Open a file dialog to select a new RSI config file.""" + filename = filedialog.askopenfilename(filetypes=[("XML Files", "*.xml")]) + if filename: + self.config_file.set(filename) + + def start_server(self): + """ + Starts the Echo Server in a background thread and begins the visual update loop. + Validates the existence of the config file first. + """ + if not os.path.exists(self.config_file.get()): + self.status_label.config(text="❌ Config file not found.") + return + + self.server = EchoServer( + config_file=self.config_file.get(), + delay_ms=self.delay.get(), + mode=self.mode.get() + ) + self.server.start() + self.running = True + self.status_label.config(text=f"✅ Server running in {self.mode.get().upper()} mode.") + self.visual_thread = threading.Thread(target=self.update_visualisation, daemon=True) + self.visual_thread.start() + + def stop_server(self): + """Stops the Echo Server and ends the visual update thread.""" + if self.server: + self.server.stop() + self.status_label.config(text="😕 Server stopped.") + self.running = False + + def update_visualisation(self): + """ + Continuously updates the 3D plot and message windows with live robot TCP and joint data. + Also displays simplified robot kinematics as a stick figure if enabled. + """ + while self.running: + try: + pos = self.server.state.get("RIst", {}) + joints = self.server.state.get("AIPos", {}) + x = pos.get("X", 0) + y = pos.get("Y", 0) + z = pos.get("Z", 0) + + # Track TCP trace history (max 300 points) + self.trace.append((x, y, z)) + if len(self.trace) > 300: + self.trace.pop(0) + + self.ax.clear() + self.ax.set_title("3D Robot Movement Trace") + self.ax.set_xlabel("X") + self.ax.set_ylabel("Y") + self.ax.set_zlabel("Z") + + # Draw shaded base plane + floor_x, floor_y = np.meshgrid(np.linspace(-200, 200, 2), np.linspace(-200, 200, 2)) + floor_z = np.zeros_like(floor_x) + self.ax.plot_surface(floor_x, floor_y, floor_z, alpha=0.2, color='gray') + + # Draw TCP trajectory + xs = [pt[0] for pt in self.trace] + ys = [pt[1] for pt in self.trace] + zs = [pt[2] for pt in self.trace] + self.ax.plot(xs, ys, zs, label="TCP Path", color='blue') + + # Draw robot as stick figure if enabled + if self.show_robot.get(): + link_lengths = [100, 80, 60, 40, 20, 10] + angles = [math.radians(joints.get(f"A{i+1}", 0)) for i in range(6)] + + x0, y0, z0 = 0, 0, 0 + x_points = [x0] + y_points = [y0] + z_points = [z0] + + for i in range(6): + dx = link_lengths[i] * math.cos(angles[i]) + dy = link_lengths[i] * math.sin(angles[i]) + dz = 0 if i < 3 else link_lengths[i] * math.sin(angles[i]) + x0 += dx + y0 += dy + z0 += dz + x_points.append(x0) + y_points.append(y0) + z_points.append(z0) + + self.ax.plot(x_points, y_points, z_points, label="Robot Arm", color='red', marker='o') + + self.ax.legend() + self.canvas.draw() + + # Update message displays + self.received_box.delete("1.0", tk.END) + self.received_box.insert(tk.END, self.server.last_received.strip() if hasattr(self.server, 'last_received') else "") + + self.sent_box.delete("1.0", tk.END) + self.sent_box.insert(tk.END, self.server.generate_message().strip()) + + time.sleep(0.2) + except Exception as e: + print(f"[Visualisation Error] {e}") + + +if __name__ == "__main__": + root = tk.Tk() + app = EchoServerGUI(root) + root.mainloop() diff --git a/src/RSIPI/inject_rsi_to_krl.py b/src/RSIPI/inject_rsi_to_krl.py new file mode 100644 index 0000000..cf34f0a --- /dev/null +++ b/src/RSIPI/inject_rsi_to_krl.py @@ -0,0 +1,79 @@ +def inject_rsi_to_krl(input_file, output_file=None, rsi_config="RSIGatewayv1.rsi"): + """ + Injects RSI commands into a KUKA KRL (.src) program file by: + - Declaring RSI variables. + - Creating the RSI context with a given configuration. + - Starting and stopping RSI execution around the program body. + + Args: + input_file (str): Path to the original KRL file. + output_file (str, optional): Output file to save modified code. Defaults to overwriting input_file. + rsi_config (str): Name of the RSI configuration (usually ending in .rsi). + """ + if output_file is None: + output_file = input_file # Overwrite original file if no output specified + + # RSI declarations to insert at top + rsi_start = """ +; RSI Variable Declarations +DECL INT ret +DECL INT CONTID +""" + + # RSI context creation and startup block + rsi_middle = f""" +; Create RSI Context +ret = RSI_CREATE("{rsi_config}", CONTID, TRUE) +IF (ret <> RSIOK) THEN + HALT +ENDIF + +; Start RSI Execution +ret = RSI_ON(#RELATIVE) +IF (ret <> RSIOK) THEN + HALT +ENDIF +""" + + # RSI shutdown block to insert before END + rsi_end = """ +; Stop RSI Execution +ret = RSI_OFF() +IF (ret <> RSIOK) THEN + HALT +ENDIF +""" + + # Read original KRL file into memory + with open(input_file, "r") as file: + lines = file.readlines() + + # Identify key structural markers in the KRL program + header_end, ini_end, end_start = None, None, None + + for i, line in enumerate(lines): + if line.strip().startswith("DEF"): + header_end = i + elif line.strip().startswith(";ENDFOLD (INI)"): + ini_end = i + elif line.strip().startswith("END"): + end_start = i + + # Validate presence of required sections + if header_end is None or ini_end is None or end_start is None: + raise ValueError("Required markers (DEF, ;ENDFOLD (INI), END) not found in KRL file.") + + # Inject modified contents into new or overwritten file + with open(output_file, "w") as file: + file.writelines(lines[:header_end + 1]) # Preserve header + file.write(rsi_start) # Add RSI declarations + file.writelines(lines[header_end + 1:ini_end + 1]) # Preserve INI block + file.write(rsi_middle) # Insert RSI start commands + file.writelines(lines[ini_end + 1:end_start]) # Preserve main body + file.write(rsi_end) # Insert RSI stop commands + file.write(lines[end_start]) # Write final END line + + +# Example usage +if __name__ == "__main__": + inject_rsi_to_krl("my_program.src", "my_program_rsi.src") diff --git a/src/RSIPI/krl_to_csv_parser.py b/src/RSIPI/krl_to_csv_parser.py new file mode 100644 index 0000000..f33f66d --- /dev/null +++ b/src/RSIPI/krl_to_csv_parser.py @@ -0,0 +1,100 @@ +import csv +import logging +import re +from collections import OrderedDict + +class KRLParser: + """ + Parses KUKA KRL .src and .dat files to extract TCP setpoints + and exports them into a structured CSV format. + """ + + def __init__(self, src_file, dat_file): + self.src_file = src_file + self.dat_file = dat_file + self.positions = OrderedDict() # Maintain order of appearance + self.labels_to_extract = [] # Store labels found in .src (e.g., XP310, XP311) + + def parse_src(self): + """ + Parses the .src file to extract motion commands and their labels (e.g., PTP XP310). + """ + move_pattern = re.compile(r"\bPTP\s+(\w+)", re.IGNORECASE) + + with open(self.src_file, 'r', encoding='utf-8') as file: + for line in file: + match = move_pattern.search(line) + if match: + label = match.group(1).strip().upper() + if label not in self.labels_to_extract: + self.labels_to_extract.append(label) + + def parse_dat(self): + """ + Parses the .dat file and retrieves Cartesian coordinates for each label. + """ + pos_pattern = re.compile(r"DECL\s+E6POS\s+(\w+)\s*=\s*\{([^}]*)\}", re.IGNORECASE) + + with open(self.dat_file, 'r', encoding='utf-8') as file: + for line in file: + match = pos_pattern.search(line) + if match: + label = match.group(1).strip().upper() + coords_text = match.group(2) + + coords = {} + for entry in coords_text.split(','): + key_value = entry.strip().split() + if len(key_value) == 2: + key, value = key_value + try: + if key in ["S", "T"]: + coords[key] = int(float(value)) + else: + coords[key] = float(value) + except ValueError: + coords[key] = 0 # fallback + + self.positions[label] = coords + + def export_csv(self, output_file): + """ + Writes the extracted Cartesian positions into a structured CSV file, + skipping any deleted/missing points. + """ + fieldnames = ["Sequence", "PosRef", "X", "Y", "Z", "A", "B", "C", "S", "T"] + + with open(output_file, 'w', newline='', encoding='utf-8') as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + writer.writeheader() + + sequence_number = 0 # Only count real points + + for label in self.labels_to_extract: + coords = self.positions.get(label) + if coords: + writer.writerow({ + "Sequence": sequence_number, + "PosRef": label, + "X": coords.get("X", 0), + "Y": coords.get("Y", 0), + "Z": coords.get("Z", 0), + "A": coords.get("A", 0), + "B": coords.get("B", 0), + "C": coords.get("C", 0), + "S": coords.get("S", 0), + "T": coords.get("T", 0), + }) + sequence_number += 1 + else: + logging.warning(f"Skipped missing/deleted point: {label}") + + logging.info(f"CSV exported successfully to {output_file} with {sequence_number} points.") + + +# Optional CLI usage +if __name__ == "__main__": + parser = KRLParser("path/to/file.src", "path/to/file.dat") + parser.parse_src() + parser.parse_dat() + parser.export_csv("path/to/output.csv") diff --git a/src/RSIPI/kuka_visualiser.py b/src/RSIPI/kuka_visualiser.py new file mode 100644 index 0000000..0c3fb84 --- /dev/null +++ b/src/RSIPI/kuka_visualiser.py @@ -0,0 +1,164 @@ +import pandas as pd +import matplotlib.pyplot as plt +import argparse +import os + + +class KukaRSIVisualiser: + """ + Visualises robot motion and diagnostics from RSI-generated CSV logs. + + Supports: + - 3D trajectory plotting (actual vs planned) + - Joint position plotting with safety band overlays + - Force correction trend visualisation + - Optional graph export to PNG + """ + + def __init__(self, csv_file, safety_limits=None): + """ + Initialise the visualiser. + + Args: + csv_file (str): Path to the RSI CSV log. + safety_limits (dict): Optional dict of axis limits (e.g., {"AIPos.A1": [-170, 170]}). + """ + self.csv_file = csv_file + self.safety_limits = safety_limits or {} + + if not os.path.exists(csv_file): + raise FileNotFoundError(f"CSV file {csv_file} not found.") + + self.df = pd.read_csv(csv_file) + + def plot_trajectory(self, save_path=None): + """ + Plots the 3D robot trajectory from actual and planned data. + + Args: + save_path (str): Optional path to save the figure. + """ + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + def safe_col(name): + return name if name in self.df.columns else f"Receive.{name}" + + ax.plot(self.df[safe_col("RIst.X")], + self.df[safe_col("RIst.Y")], + self.df[safe_col("RIst.Z")], + label="Actual Trajectory", linestyle='-') + + if "RSol.X" in self.df.columns: + ax.plot(self.df["RSol.X"], self.df["RSol.Y"], self.df["RSol.Z"], + label="Planned Trajectory", linestyle='--') + + ax.set_xlabel("X Position") + ax.set_ylabel("Y Position") + ax.set_zlabel("Z Position") + ax.set_title("Robot Trajectory") + ax.legend() + + if save_path: + plt.savefig(save_path) + plt.show() + + def has_column(self, col): + """ + Checks if the given column exists in the dataset. + + Args: + col (str): Column name to check. + """ + return col in self.df.columns + + def plot_joint_positions(self, save_path=None): + """ + Plots joint angle positions over time, with optional safety zone overlays. + + Args: + save_path (str): Optional path to save the figure. + """ + plt.figure() + time_series = range(len(self.df)) + + for col in ["AIPos.A1", "AIPos.A2", "AIPos.A3", "AIPos.A4", "AIPos.A5", "AIPos.A6"]: + if col in self.df.columns: + plt.plot(time_series, self.df[col], label=col) + + if col in self.safety_limits: + low, high = self.safety_limits[col] + plt.axhspan(low, high, color='red', alpha=0.1, label=f"{col} Safe Zone") + + plt.xlabel("Time Steps") + plt.ylabel("Joint Position (Degrees)") + plt.title("Joint Positions Over Time") + plt.legend() + + if save_path: + plt.savefig(save_path) + plt.show() + + def plot_force_trends(self, save_path=None): + """ + Plots force correction trends (PosCorr.*) over time, if present. + + Args: + save_path (str): Optional path to save the figure. + """ + force_columns = ["PosCorr.X", "PosCorr.Y", "PosCorr.Z"] + plt.figure() + time_series = range(len(self.df)) + + for col in force_columns: + if col in self.df.columns: + plt.plot(time_series, self.df[col], label=col) + + if col in self.safety_limits: + low, high = self.safety_limits[col] + plt.axhspan(low, high, color='red', alpha=0.1, label=f"{col} Safe Zone") + + plt.xlabel("Time Steps") + plt.ylabel("Force Correction (N)") + plt.title("Force Trends Over Time") + plt.legend() + + if save_path: + plt.savefig(save_path) + plt.show() + + def export_graphs(self, export_dir="exports"): + """ + Saves all graphs (trajectory, joints, force) as PNG images. + + Args: + export_dir (str): Output directory. + """ + os.makedirs(export_dir, exist_ok=True) + self.plot_trajectory(save_path=os.path.join(export_dir, "trajectory.png")) + self.plot_joint_positions(save_path=os.path.join(export_dir, "joint_positions.png")) + self.plot_force_trends(save_path=os.path.join(export_dir, "force_trends.png")) + print(f"Graphs exported to {export_dir}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Visualise RSI data logs.") + parser.add_argument("csv_file", type=str, help="Path to the RSI CSV log file.") + parser.add_argument("--export", action="store_true", help="Export graphs as PNG/PDF.") + parser.add_argument("--limits", type=str, help="Optional .rsi.xml file to overlay safety bands") + + args = parser.parse_args() + + if args.limits: + from src.RSIPI.rsi_limit_parser import parse_rsi_limits + limits = parse_rsi_limits(args.limits) + visualiser = KukaRSIVisualiser(args.csv_file, safety_limits=limits) + else: + visualiser = KukaRSIVisualiser(args.csv_file) + + visualiser.plot_trajectory() + visualiser.plot_joint_positions() + visualiser.plot_force_trends() + + if args.export: + visualiser.export_graphs() diff --git a/src/RSIPI/live_plotter.py b/src/RSIPI/live_plotter.py new file mode 100644 index 0000000..3d8d163 --- /dev/null +++ b/src/RSIPI/live_plotter.py @@ -0,0 +1,130 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from collections import deque +from threading import Thread, Lock +import time + +class LivePlotter: + def __init__(self, client, mode="3d", interval=100): + self.client = client + self.mode = mode + self.interval = interval + self.running = False + + # Plot data buffers + self.time_data = deque(maxlen=500) + self.position_data = {k: deque(maxlen=500) for k in ["X", "Y", "Z"]} + self.velocity_data = {k: deque(maxlen=500) for k in ["X", "Y", "Z"]} + self.acceleration_data = {k: deque(maxlen=500) for k in ["X", "Y", "Z"]} + self.joint_data = {f"A{i}": deque(maxlen=500) for i in range(1, 7)} + self.force_data = {f"A{i}": deque(maxlen=500) for i in range(1, 7)} + + self.previous_positions = {"X": 0, "Y": 0, "Z": 0} + self.previous_velocities = {"X": 0, "Y": 0, "Z": 0} + self.previous_time = time.time() + + self.lock = Lock() + self.collector_thread = None + + self.fig = plt.figure() + self.ax = self.fig.add_subplot(111, projection="3d" if self.mode == "3d" else None) + + def start(self): + self.running = True + self.collector_thread = Thread(target=self.collect_data_loop, daemon=True) + self.collector_thread.start() + self.ani = animation.FuncAnimation(self.fig, self.update_plot, interval=self.interval) + try: + plt.show() + except RuntimeError: + print("⚠️ Matplotlib GUI interrupted during shutdown.") + self.running = False + + def stop(self, save_path: str = None): + self.running = False + if save_path: + try: + self.fig.savefig(save_path, bbox_inches="tight") + print(f"📸 Plot saved to '{save_path}'") + except Exception as e: + print(f"❌ Failed to save plot: {e}") + plt.close(self.fig) + + def collect_data_loop(self): + while self.running: + with self.lock: + current_time = time.time() + dt = current_time - self.previous_time + self.previous_time = current_time + self.time_data.append(current_time) + + position = self.client.receive_variables.get("RIst", {"X": 0, "Y": 0, "Z": 0}) + joints = self.client.receive_variables.get("AIPos", {f"A{i}": 0 for i in range(1, 7)}) + force = self.client.receive_variables.get("MaCur", {f"A{i}": 0 for i in range(1, 7)}) + + for axis in ["X", "Y", "Z"]: + vel = (position[axis] - self.previous_positions[axis]) / dt if dt > 0 else 0 + acc = (vel - self.previous_velocities[axis]) / dt if dt > 0 else 0 + self.previous_positions[axis] = position[axis] + self.previous_velocities[axis] = vel + self.position_data[axis].append(position[axis]) + self.velocity_data[axis].append(vel) + self.acceleration_data[axis].append(acc) + + for i in range(1, 7): + self.joint_data[f"A{i}"].append(joints.get(f"A{i}", 0)) + self.force_data[f"A{i}"].append(force.get(f"A{i}", 0)) + + time.sleep(self.interval / 1000.0) + + def update_plot(self, frame): + if not self.running: + return + + with self.lock: + self.ax.clear() + self.render_plot() + + def render_plot(self): + if self.mode == "3d": + self.ax.set_title("Live 3D TCP Trajectory") + self.ax.plot(self.position_data["X"], self.position_data["Y"], self.position_data["Z"], label="TCP Path") + self.ax.set_xlabel("X") + self.ax.set_ylabel("Y") + self.ax.set_zlabel("Z") + elif self.mode == "2d_xy": + self.ax.set_title("Live 2D Trajectory (X-Y)") + self.ax.plot(self.position_data["X"], self.position_data["Y"], label="XY Path") + self.ax.set_xlabel("X") + self.ax.set_ylabel("Y") + elif self.mode == "velocity": + self.ax.set_title("Live TCP Velocity") + self.ax.plot(self.time_data, self.velocity_data["X"], label="dX/dt") + self.ax.plot(self.time_data, self.velocity_data["Y"], label="dY/dt") + self.ax.plot(self.time_data, self.velocity_data["Z"], label="dZ/dt") + self.ax.set_ylabel("Velocity [mm/s]") + elif self.mode == "acceleration": + self.ax.set_title("Live TCP Acceleration") + self.ax.plot(self.time_data, self.acceleration_data["X"], label="d²X/dt²") + self.ax.plot(self.time_data, self.acceleration_data["Y"], label="d²Y/dt²") + self.ax.plot(self.time_data, self.acceleration_data["Z"], label="d²Z/dt²") + self.ax.set_ylabel("Acceleration [mm/s²]") + elif self.mode == "joints": + self.ax.set_title("Live Joint Angles") + for j, values in self.joint_data.items(): + self.ax.plot(self.time_data, values, label=j) + self.ax.set_ylabel("Angle [deg]") + elif self.mode == "force": + self.ax.set_title("Live Motor Currents") + for j, values in self.force_data.items(): + self.ax.plot(self.time_data, values, label=j) + self.ax.set_ylabel("Current [Nm]") + + self.ax.set_xlabel("Time") + self.ax.legend() + self.ax.grid(True) + self.fig.tight_layout() + + def change_mode(self, mode): + self.mode = mode + self.ax = self.fig.add_subplot(111, projection="3d" if mode == "3d" else None) diff --git a/src/RSIPI/network_handler.py b/src/RSIPI/network_handler.py new file mode 100644 index 0000000..f42e2b9 --- /dev/null +++ b/src/RSIPI/network_handler.py @@ -0,0 +1,86 @@ +import multiprocessing +import socket +import logging +import xml.etree.ElementTree as ET +from .xml_handler import XMLGenerator +from .safety_manager import SafetyManager + +class NetworkProcess(multiprocessing.Process): + """Handles UDP communication and optional CSV logging in a separate process.""" + + def __init__(self, ip, port, send_variables, receive_variables, stop_event, config_parser, start_event): + super().__init__() + self.send_variables = send_variables + self.receive_variables = receive_variables + self.stop_event = stop_event + self.start_event = start_event # ✅ NEW + self.config_parser = config_parser + self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.safety_manager = SafetyManager(config_parser.safety_limits) + + self.client_address = (ip, port) + self.logging_active = multiprocessing.Value('b', False) + self.log_filename = multiprocessing.Array('c', 256) + self.csv_process = None + + self.controller_ip_and_port = None + + def run(self): + """Start the network loop.""" + self.start_event.wait() # ✅ Wait until RSIClient sends start signal + + try: + if not self.is_valid_ip(self.client_address[0]): + logging.warning(f"Invalid IP address '{self.client_address[0]}'. Falling back to '0.0.0.0'.") + self.client_address = ('0.0.0.0', self.client_address[1]) + + self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.udp_socket.bind(self.client_address) + logging.info(f"✅ Network process bound on {self.client_address}") + + except OSError as e: + logging.error(f"❌ Failed to bind to {self.client_address}: {e}") + raise + + while not self.stop_event.is_set(): + try: + self.udp_socket.settimeout(5) + data_received, self.controller_ip_and_port = self.udp_socket.recvfrom(1024) + message = data_received.decode() + self.process_received_data(message) + send_xml = XMLGenerator.generate_send_xml(self.send_variables, self.config_parser.network_settings) + self.udp_socket.sendto(send_xml.encode(), self.controller_ip_and_port) + + if self.logging_active.value: + self.log_to_csv() + + except socket.timeout: + logging.error("[WARNING] No message received within timeout period.") + except Exception as e: + logging.error(f"[ERROR] Network process error: {e}") + + @staticmethod + def is_valid_ip(ip): + try: + socket.inet_aton(ip) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.bind((ip, 0)) + return True + except (socket.error, OSError): + return False + + def process_received_data(self, xml_string): + try: + root = ET.fromstring(xml_string) + for element in root: + if element.tag in self.receive_variables: + if len(element.attrib) > 0: + self.receive_variables[element.tag] = {k: float(v) for k, v in element.attrib.items()} + else: + self.receive_variables[element.tag] = element.text + if element.tag == "IPOC": + received_ipoc = int(element.text) + self.receive_variables["IPOC"] = received_ipoc + self.send_variables["IPOC"] = received_ipoc + 4 + except Exception as e: + logging.error(f"[ERROR] Error parsing received message: {e}") diff --git a/src/RSIPI/rsi_api.py b/src/RSIPI/rsi_api.py new file mode 100644 index 0000000..6893ca5 --- /dev/null +++ b/src/RSIPI/rsi_api.py @@ -0,0 +1,609 @@ +import logging +import pandas as pd +import numpy as np +import json +import matplotlib.pyplot as plt +from .kuka_visualiser import KukaRSIVisualiser +from .krl_to_csv_parser import KRLParser +from .inject_rsi_to_krl import inject_rsi_to_krl +import threading +from .trajectory_planner import generate_trajectory, execute_trajectory +import datetime +from src.RSIPI.static_plotter import StaticPlotter # Make sure this file exists as described +import os +from src.RSIPI.live_plotter import LivePlotter +from threading import Thread +import asyncio + +class RSIAPI: + """RSI API for programmatic control, including alerts, logging, graphing, and data retrieval.""" + + def __init__(self, config_file="RSI_EthernetConfig.xml"): + """Initialize RSIAPI with an RSI client instance.""" + self.thread = None + self.config_file = config_file + self.client = None + self.graph_process = None + self.graphing_instance = None + self.graph_thread = None# + self.trajectory_queue = [] + self.live_plotter = None + self.live_plot_thread = None + + self._ensure_client() + + def _ensure_client(self): + """Ensure RSIClient is initialised before use.""" + if self.client is None: + from .rsi_client import RSIClient + self.client = RSIClient(self.config_file) + + def start_rsi(self): + + self.thread = threading.Thread(target=self.client.start, daemon=True) + self.thread.start() + return "RSI started in background." + + def stop_rsi(self): + """Stop the RSI client.""" + self.client.stop() + return "RSI stopped." + + def generate_report(filename, format_type): + """ + Generate a statistical report from a CSV log file. + + Args: + filename (str): Path to the CSV file (or base name without .csv). + format_type (str): 'csv', 'json', or 'pdf' + """ + # Ensure filename ends with .csv + if not filename.endswith(".csv"): + filename += ".csv" + + if not os.path.exists(filename): + raise FileNotFoundError(f"❌ File not found: {filename}") + + df = pd.read_csv(filename) + + # Only keep relevant columns (e.g. actual positions) + position_cols = [col for col in df.columns if col.startswith("Receive.RIst.")] + if not position_cols: + raise ValueError("❌ No 'Receive.RIst' position columns found in CSV.") + + report_data = { + "Max Position": df[position_cols].max().to_dict(), + "Mean Position": df[position_cols].mean().to_dict(), + } + + report_base = filename.replace(".csv", "") + output_path = f"{report_base}_report.{format_type.lower()}" + + if format_type == "csv": + pd.DataFrame(report_data).T.to_csv(output_path) + elif format_type == "json": + with open(output_path, "w") as f: + json.dump(report_data, f, indent=4) + elif format_type == "pdf": + fig, ax = plt.subplots() + pd.DataFrame(report_data).T.plot(kind='bar', ax=ax) + ax.set_title("RSI Position Report") + plt.tight_layout() + plt.savefig(output_path) + else: + raise ValueError(f"Unsupported format: {format_type}") + + return f"Report saved as {output_path}" + + def update_variable(self, name, value): + if "." in name: + parent, child = name.split(".", 1) + full_path = f"{parent}.{child}" + if parent in self.client.send_variables: + current = dict(self.client.send_variables[parent]) + # 🛡️ Validate using SafetyManager + safe_value = self.client.safety_manager.validate(full_path, float(value)) + current[child] = safe_value + self.client.send_variables[parent] = current + return f"Updated {name} to {safe_value}" + else: + raise KeyError(f"Parent variable '{parent}' not found in send_variables") + else: + safe_value = self.client.safety_manager.validate(name, float(value)) + self.client.send_variables[name] = safe_value + return f"Updated {name} to {safe_value}" + + def show_variables(self): + """Print available variable names in send and receive variables.""" + def format_grouped(var_dict): + output = [] + for var, val in var_dict.items(): + if isinstance(val, dict): + sub_vars = ', '.join(val.keys()) + output.append(f"{var}: {sub_vars}") + else: + output.append(var) + return output + + send_vars = format_grouped(self.client.send_variables) + receive_vars = format_grouped(self.client.receive_variables) + + print("Send Variables:") + for item in send_vars: + print(f" - {item}") + + print("\nReceive Variables:") + for item in receive_vars: + print(f" - {item}") + + def get_live_data(self): + """Retrieve real-time RSI data for external processing.""" + return { + "position": self.client.receive_variables.get("RIst", {"X": 0, "Y": 0, "Z": 0}), + "velocity": self.client.receive_variables.get("Velocity", {"X": 0, "Y": 0, "Z": 0}), + "acceleration": self.client.receive_variables.get("Acceleration", {"X": 0, "Y": 0, "Z": 0}), + "force": self.client.receive_variables.get("MaCur", {"A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0, "A6": 0}), + "ipoc": self.client.receive_variables.get("IPOC", "N/A") + } + + def get_live_data_as_numpy(self): + data = self.get_live_data() + flat = [] + for section in ["position", "velocity", "acceleration", "force"]: + values = list(data[section].values()) + flat.append(values) + + max_len = max(len(row) for row in flat) + for row in flat: + row.extend([0] * (max_len - len(row))) # Pad missing values + + return np.array(flat) + + def get_live_data_as_dataframe(self): + """Retrieve live RSI data as a Pandas DataFrame.""" + data = self.get_live_data() + return pd.DataFrame([data]) + + def get_ipoc(self): + """Retrieve the latest IPOC value.""" + return self.client.receive_variables.get("IPOC", "N/A") + + def reconnect(self): + """Restart the network connection without stopping RSI.""" + self.client.reconnect() + return "Network connection restarted." + + def toggle_digital_io(self, io_group, io_name, state): + """ + Toggle a digital IO variable. + + Args: + io_group (str): Parent variable group (e.g., 'Digout', 'DiO', 'DiL') + io_name (str): IO name or number within the group (e.g., 'o1', '1') + state (bool | int): Desired state (True/False or 1/0) + + Returns: + str: Success or failure message. + """ + var_name = f"{io_group}.{io_name}" + state_value = int(bool(state)) # ensures it's either 1 or 0 + return self.update_variable(var_name, state_value) + + def move_external_axis(self, axis, value): + """Move an external axis.""" + return self.update_variable(f"ELPos.{axis}", value) + + def correct_position(self, correction_type, axis, value): + """Apply correction to RKorr or AKorr.""" + return self.update_variable(f"{correction_type}.{axis}", value) + + def adjust_speed(self, tech_param, value): + """Adjust speed settings (e.g., Tech.T21).""" + return self.update_variable(tech_param, value) + + def reset_variables(self): + """Reset send variables to default values.""" + self.client.reset_send_variables() + return "✅ Send variables reset to default values." + + def show_config_file(self): + """Retrieve key information from config file.""" + return { + "Network": self.client.config_parser.get_network_settings(), + "Send variables": dict(self.client.send_variables), + "Receive variables": dict(self.client.receive_variables) + } + + def start_logging(self, filename=None): + if not filename: + timestamp = datetime.datetime.now().strftime("%d-%m-%Y_%H-%M-%S") + filename = f"logs/{timestamp}.csv" + + self.client.start_logging(filename) + return filename + + def stop_logging(self): + """Stop logging RSI data.""" + self.client.stop_logging() + return "CSV Logging stopped." + + def is_logging_active(self): + """Return logging status.""" + return self.client.is_logging_active() + + @staticmethod + def generate_plot(csv_path: str, plot_type: str = "3d", overlay_path: str = None): + """ + Generate a static plot based on RSI CSV data. + + Args: + csv_path (str): Path to the CSV log file. + plot_type (str): Type of plot to generate. Options: + - "3d", "2d_xy", "2d_xz", "2d_yz" + - "position", "velocity", "acceleration" + - "joints", "force", "deviation" + overlay_path (str): Optional CSV file for planned trajectory (used in "deviation" plots). + + Returns: + str: Status message indicating plot success or failure. + """ + if not os.path.exists(csv_path): + return f"CSV file not found: {csv_path}" + + try: + plot_type = plot_type.lower() + + match plot_type: + case "3d": + StaticPlotter.plot_3d_trajectory(csv_path) + case "2d_xy": + StaticPlotter.plot_2d_projection(csv_path, plane="xy") + case "2d_xz": + StaticPlotter.plot_2d_projection(csv_path, plane="xz") + case "2d_yz": + StaticPlotter.plot_2d_projection(csv_path, plane="yz") + case "position": + StaticPlotter.plot_position_vs_time(csv_path) + case "velocity": + StaticPlotter.plot_velocity_vs_time(csv_path) + case "acceleration": + StaticPlotter.plot_acceleration_vs_time(csv_path) + case "joints": + StaticPlotter.plot_joint_angles(csv_path) + case "force": + StaticPlotter.plot_motor_currents(csv_path) + case "deviation": + if overlay_path is None or not os.path.exists(overlay_path): + return "Deviation plot requires a valid overlay CSV file." + StaticPlotter.plot_deviation(csv_path, overlay_path) + case _: + return f"Invalid plot type '{plot_type}'. Use one of: 3d, 2d_xy, 2d_xz, 2d_yz, position, velocity, acceleration, joints, force, deviation." + + return f"✅ Plot '{plot_type}' generated successfully." + except Exception as e: + return f"Failed to generate plot '{plot_type}': {str(e)}" + + + + def start_live_plot(self, mode="3d", interval=100): + if self.live_plotter and self.live_plotter.running: + return "Live plotting already active." + + def runner(): + self.live_plotter = LivePlotter(self.client, mode=mode, interval=interval) + self.live_plotter.start() + + self.live_plot_thread = Thread(target=runner, daemon=True) + self.live_plot_thread.start() + return f"Live plot started in '{mode}' mode at {interval}ms interval." + + def stop_live_plot(self): + if self.live_plotter and self.live_plotter.running: + self.live_plotter.stop() + return "Live plotting stopped." + return "No live plot is currently running." + + def change_live_plot_mode(self, mode): + if self.live_plotter and self.live_plotter.running: + self.live_plotter.change_mode(mode) + return f"Live plot mode changed to '{mode}'." + return "No live plot is active to change mode." + + + + # ✅ ALERT METHODS + def enable_alerts(self, enable): + """Enable or disable real-time alerts.""" + self.client.enable_alerts(enable) + return f"Alerts {'enabled' if enable else 'disabled'}." + + def override_safety(self, enabled: bool): + self.client.safety_manager.override_safety(enabled) + + def is_safety_overridden(self) -> bool: + return self.client.safety_manager.is_safety_overridden() + + def set_alert_threshold(self, alert_type, value): + """Set threshold for deviation or force alerts.""" + if alert_type in ["deviation", "force"]: + self.client.set_alert_threshold(alert_type, value) + return f"{alert_type.capitalize()} alert threshold set to {value}" + return "Invalid alert type. Use 'deviation' or 'force'." + + @staticmethod + def visualise_csv_log(csv_file, export=False): + """ + Visualize CSV log file directly via RSIAPI. + + Args: + csv_file (str): Path to CSV log file. + export (bool): Whether to export the plots. + """ + visualizer = KukaRSIVisualiser(csv_file) + visualizer.plot_trajectory() + visualizer.plot_joint_positions() + visualizer.plot_force_trends() + + if export: + visualizer.export_graphs() + + @staticmethod + def parse_krl_to_csv(src_file, dat_file, output_file): + """ + Parses KRL files (.src, .dat) and exports coordinates to CSV. + + Args: + src_file (str): Path to KRL .src file. + dat_file (str): Path to KRL .dat file. + output_file (str): Path for output CSV file. + """ + try: + parser = KRLParser(src_file, dat_file) + parser.parse_src() + parser.parse_dat() + parser.export_csv(output_file) + return f"KRL data successfully exported to {output_file}" + except Exception as e: + return f"Error parsing KRL files: {e}" + + @staticmethod + def inject_rsi(input_krl, output_krl=None, rsi_config="RSIGatewayv1.rsi"): + """ + Inject RSI commands into a KRL (.src) program file. + + Args: + input_krl (str): Path to the input KRL file. + output_krl (str, optional): Path to the output file (defaults to overwriting input). + rsi_config (str, optional): RSI configuration file name. + """ + try: + inject_rsi_to_krl(input_krl, output_krl, rsi_config) + output_path = output_krl if output_krl else input_krl + return f"RSI successfully injected into {output_path}" + except Exception as e: + return f"RSI injection failed: {e}" + + @staticmethod + def generate_trajectory(start, end, steps=100, space="cartesian", mode="absolute", include_resets=False): + """Generates a linear trajectory (Cartesian or Joint).""" + return generate_trajectory(start, end, steps, space, mode, include_resets) + + import asyncio + + def execute_trajectory(self, trajectory, space="cartesian", rate=0.012): + """ + Executes a trajectory intelligently: + - If already inside an asyncio loop -> schedules task in background + - If no loop -> creates one and runs blocking + """ + + async def runner(): + for idx, point in enumerate(trajectory): + if space == "cartesian": + self.update_cartesian(**point) + elif space == "joint": + self.update_joints(**point) + else: + raise ValueError("space must be 'cartesian' or 'joint'") + print(f"Step {idx + 1}/{len(trajectory)} sent") + await asyncio.sleep(rate) + + try: + loop = asyncio.get_running_loop() + # If inside event loop, schedule runner as background task + asyncio.create_task(runner()) + except RuntimeError: + # If no event loop is running, create and run one + asyncio.run(runner()) + + def queue_trajectory(self, trajectory, space="cartesian", rate=0.012): + """Adds a trajectory to the internal queue.""" + self.trajectory_queue.append({ + "trajectory": trajectory, + "space": space, + "rate": rate, + }) + + def clear_trajectory_queue(self): + """Clears all queued trajectories.""" + self.trajectory_queue.clear() + + def get_trajectory_queue(self): + """Returns current queued trajectories (metadata only).""" + return [ + {"space": item["space"], "steps": len(item["trajectory"]), "rate": item["rate"]} + for item in self.trajectory_queue + ] + + def execute_queued_trajectories(self): + """Executes all queued trajectories in order.""" + for item in self.trajectory_queue: + self.execute_trajectory(item["trajectory"], item["space"], item["rate"]) + self.clear_trajectory_queue() + + def export_movement_data(self, filename="movement_log.csv"): + """ + Exports recorded movement data (if available) to a CSV file. + Assumes self.client.logger has stored entries. + """ + if not hasattr(self.client, "logger") or self.client.logger is None: + raise RuntimeError("No logger attached to RSI client.") + + data = self.client.get_movement_data() + if not data: + raise RuntimeError("No data available to export.") + + df = pd.DataFrame(data) + df.to_csv(filename, index=False) + return f"Movement data exported to {filename}" + + @staticmethod + def compare_test_runs(file1, file2): + """ + Compares two test run CSV files. + Returns a summary of average and max deviation for each axis. + """ + import pandas as pd + + df1 = pd.read_csv(file1) + df2 = pd.read_csv(file2) + + shared_cols = [col for col in df1.columns if col in df2.columns and col.startswith("Receive.RIst")] + diffs = {} + + for col in shared_cols: + delta = abs(df1[col] - df2[col]) + diffs[col] = { + "mean_diff": delta.mean(), + "max_diff": delta.max(), + } + + return diffs + + def update_cartesian(self, **kwargs): + """ + Update Cartesian correction values (RKorr). + """ + self._ensure_client() + if "RKorr" not in self.client.send_variables: + logging.warning("Warning: RKorr not configured in send_variables. Skipping Cartesian update.") + return + + for axis, value in kwargs.items(): + self.update_variable(f"RKorr.{axis}", float(value)) + + def update_joints(self, **kwargs): + """ + Update joint correction values (AKorr). + """ + self._ensure_client() + if "AKorr" not in self.client.send_variables: + logging.warning("⚠️ Warning: AKorr not configured in send_variables. Skipping Joint update.") + return + + for axis, value in kwargs.items(): + self.update_variable(f"AKorr.{axis}", float(value)) + + def watch_network(self, duration: float = None, rate: float = 0.2): + """ + Continuously prints current receive variables (and IPOC). + If duration is None, runs until interrupted. + """ + import time + import datetime + + logging.info("Watching network... Press Ctrl+C to stop.\n") + start_time = time.time() + + try: + while True: + live_data = self.get_live_data() + ipoc = live_data.get("IPOC", "N/A") + rpos = live_data.get("RIst", {}) + print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] IPOC: {ipoc} | RIst: {rpos}") + time.sleep(rate) + + if duration and (time.time() - start_time) >= duration: + break + + except KeyboardInterrupt: + logging.info("\nStopped network watch.") + + def move_cartesian_trajectory(self, start_pose, end_pose, steps=50, rate=0.012): + """ + Generate and execute a Cartesian (TCP) movement between two poses. + Args: + start_pose (dict): e.g. {"X":0, "Y":0, "Z":500} + end_pose (dict): e.g. {"X":100, "Y":0, "Z":500} + steps (int): Number of interpolation points. + rate (float): Time between points in seconds. + """ + trajectory = self.generate_trajectory(start_pose, end_pose, steps=steps, space="cartesian") + self.execute_trajectory(trajectory, space="cartesian", rate=rate) + + def move_joint_trajectory(self, start_joints, end_joints, steps=50, rate=0.4): + """ + Generate and execute a Joint-space movement between two poses. + Args: + start_joints (dict): e.g. {"A1":0, "A2":0, "A3":0, ...} + end_joints (dict): e.g. {"A1":90, "A2":0, "A3":0, ...} + steps (int): Number of interpolation points. + rate (float): Time between points in seconds. + """ + trajectory = self.generate_trajectory(start_joints, end_joints, steps=steps, space="joint") + self.execute_trajectory(trajectory, space="joint", rate=rate) + + def queue_cartesian_trajectory(self, start_pose, end_pose, steps=50, rate=0.012): + """ + Generate and queue a Cartesian movement (no execution). + """ + if not isinstance(start_pose, dict) or not isinstance(end_pose, dict): + raise ValueError("start_pose and end_pose must be dictionaries (e.g., {'X': 0, 'Y': 0, 'Z': 500})") + if steps <= 0: + raise ValueError("Steps must be greater than zero.") + if rate <= 0: + raise ValueError("Rate must be greater than zero.") + + trajectory = self.generate_trajectory(start_pose, end_pose, steps=steps, space="cartesian") + self.queue_trajectory(trajectory, "cartesian", rate) + + def queue_joint_trajectory(self, start_joints, end_joints, steps=50, rate=0.4): + """ + Generate and queue a Joint-space movement (no execution). + """ + if not isinstance(start_joints, dict) or not isinstance(end_joints, dict): + raise ValueError("start_joints and end_joints must be dictionaries (e.g., {'A1': 0, 'A2': 0})") + if steps <= 0: + raise ValueError("Steps must be greater than zero.") + if rate <= 0: + raise ValueError("Rate must be greater than zero.") + + trajectory = self.generate_trajectory(start_joints, end_joints, steps=steps, space="joint") + self.queue_trajectory(trajectory, "joint", rate) + + # --- 🛡️ Safety Management --- + + def safety_stop(self): + """Trigger emergency stop.""" + self._ensure_client() + self.client.safety_manager.emergency_stop() + + def safety_reset(self): + """Reset emergency stop.""" + self._ensure_client() + self.client.safety_manager.reset_stop() + + def safety_status(self): + """Return detailed safety status.""" + self._ensure_client() + sm = self.client.safety_manager + return { + "emergency_stop": sm.is_stopped(), + "safety_override": self.is_safety_overridden(), + "limits": sm.get_limits(), + } + + def safety_set_limit(self, variable, lower, upper): + """Set new safety limit bounds for a specific variable.""" + self._ensure_client() + self.client.safety_manager.set_limit(variable, float(lower), float(upper)) diff --git a/src/RSIPI/rsi_cli.py b/src/RSIPI/rsi_cli.py new file mode 100644 index 0000000..361d2d3 --- /dev/null +++ b/src/RSIPI/rsi_cli.py @@ -0,0 +1,196 @@ +from RSIPI.rsi_api import RSIAPI + +class RSICommandLineInterface: + """Command-Line Interface for controlling RSI Client.""" + + def __init__(self, input_config_file): + self.client = RSIAPI(input_config_file) + self.running = True + + def run(self): + print("RSI Command-Line Interface Started. Type 'help' for commands.") + while self.running: + try: + command = input("RSI> ").strip() + self.process_command(command) + except KeyboardInterrupt: + self.exit() + + def process_command(self, command): + parts = command.split() + if not parts: + return + + cmd = parts[0].lower() + args = parts[1:] + + try: + match cmd: + case "start": + print(self.client.start_rsi()) + case "stop": + print(self.client.stop_rsi()) + case "exit": + self.exit() + case "set": + var, val = args[0], args[1] + print(self.client.update_variable(var, val)) + case "show": + print("📤 Send Variables:") + self.client.show_variables() + case "reset": + print(self.client.reset_variables()) + case "status": + print(self.client.show_config_file()) + case "ipoc": + print(f"🛰 IPOC: {self.client.get_ipoc()}") + case "watch": + duration = float(args[0]) if args else None + self.client.watch_network(duration) + case "reconnect": + print(self.client.reconnect()) + case "alerts": + state = args[0].lower() + self.client.enable_alerts(state == "on") + case "set_alert_threshold": + alert_type, value = args[0], float(args[1]) + self.client.set_alert_threshold(alert_type, value) + case "toggle": + group, name, value = args + print(self.client.toggle_digital_io(group, name, value)) + case "move_external": + axis, value = args + print(self.client.move_external_axis(axis, value)) + case "correct": + corr_type, axis, value = args + print(self.client.correct_position(corr_type, axis, value)) + case "speed": + tech_param, value = args + print(self.client.adjust_speed(tech_param, value)) + case "override": + state = args[0] + self.client.override_safety(state in ["on", "true", "1"]) + case "log": + subcmd = args[0] + if subcmd == "start": + print(f"✅ Logging to {self.client.start_logging()}") + elif subcmd == "stop": + print(self.client.stop_logging()) + elif subcmd == "status": + print("📋", "ACTIVE" if self.client.is_logging_active() else "INACTIVE") + case "graph": + sub = args[0] + if sub == "show": + self.client.visualise_csv_log(args[1]) + elif sub == "compare": + print(self.client.compare_test_runs(args[1], args[2])) + case "plot": + plot_type, csv_path = args[0], args[1] + overlay = args[2] if len(args) > 2 else None + print(self.client.generate_plot(csv_path, plot_type, overlay)) + case "move_cartesian": + start = self.parse_pose(args[0]) + end = self.parse_pose(args[1]) + steps = self.extract_value(args, "steps", 50, int) + rate = self.extract_value(args, "rate", 0.04, float) + self.client.move_cartesian_trajectory(start, end, steps, rate) + case "move_joint": + start = self.parse_pose(args[0]) + end = self.parse_pose(args[1]) + steps = self.extract_value(args, "steps", 50, int) + rate = self.extract_value(args, "rate", 0.04, float) + self.client.move_joint_trajectory(start, end, steps, rate) + case "queue_cartesian": + start = self.parse_pose(args[0]) + end = self.parse_pose(args[1]) + steps = self.extract_value(args, "steps", 50, int) + rate = self.extract_value(args, "rate", 0.04, float) + self.client.queue_cartesian_trajectory(start, end, steps, rate) + case "queue_joint": + start = self.parse_pose(args[0]) + end = self.parse_pose(args[1]) + steps = self.extract_value(args, "steps", 50, int) + rate = self.extract_value(args, "rate", 0.04, float) + self.client.queue_joint_trajectory(start, end, steps, rate) + case "execute_queue": + self.client.execute_queued_trajectories() + case "clear_queue": + self.client.clear_trajectory_queue() + case "show_queue": + print(self.client.get_trajectory_queue()) + case "export_movement_data": + print(self.client.export_movement_data(args[0])) + case "compare_test_runs": + print(self.client.compare_test_runs(args[0], args[1])) + case "generate_report": + print(self.client.generate_report(args[0], args[1])) + case "safety-stop": + self.client.safety_stop() + case "safety-reset": + self.client.safety_reset() + case "safety-status": + print(self.client.safety_status()) + case "safety-set-limit": + var, lo, hi = args + self.client.safety_set_limit(var, lo, hi) + case "krlparse": + self.client.parse_krl_to_csv(args[0], args[1], args[2]) + case "inject_rsi": + input_krl = args[0] + output_krl = args[1] if len(args) > 1 else None + rsi_cfg = args[2] if len(args) > 2 else "RSIGatewayv1.rsi" + self.client.inject_rsi(input_krl, output_krl, rsi_cfg) + case "visualize": + self.client.visualise_csv_log(args[0], export="export" in args) + case "help": + self.show_help() + case _: + print("❌ Unknown command. Type 'help'.") + except Exception as e: + print(f"❌ Error: {e}") + + def parse_pose(self, pose_string): + return dict(item.split("=") for item in pose_string.split(",")) + + def extract_value(self, args, key, default, cast_type): + for arg in args[2:]: + if arg.startswith(f"{key}="): + try: + return cast_type(arg.split("=")[1]) + except ValueError: + return default + return default + + def exit(self): + print("🛑 Exiting RSI CLI...") + self.client.stop_rsi() + self.running = False + + def show_help(self): + print(""" +Available Commands: + start, stop, exit + set + show, status, ipoc, watch, reset, reconnect + alerts on/off, set_alert_threshold + toggle + move_external , correct + speed + log start|stop|status + graph show | graph compare + plot [overlay] + move_cartesian, move_joint, queue_cartesian, queue_joint + execute_queue, clear_queue, show_queue + export_movement_data + compare_test_runs + generate_report + safety-stop, safety-reset, safety-status, safety-set-limit + krlparse + inject_rsi [output] [rsi_config] + visualize [export] + help + """) + +if __name__ == "__main__": + cli = RSICommandLineInterface("../../examples/RSI_EthernetConfig.xml") + cli.run() diff --git a/src/RSIPI/rsi_client.py b/src/RSIPI/rsi_client.py new file mode 100644 index 0000000..c4eafaf --- /dev/null +++ b/src/RSIPI/rsi_client.py @@ -0,0 +1,103 @@ +import logging +import multiprocessing +import time +from .config_parser import ConfigParser +from .network_handler import NetworkProcess +from .safety_manager import SafetyManager +import threading + +class RSIClient: + """Main RSI API class that integrates network, config handling, and message processing.""" + + def __init__(self, config_file, rsi_limits_file=None): + logging.info(f"Loading RSI configuration from {config_file}...") + + self.config_parser = ConfigParser(config_file, rsi_limits_file) + network_settings = self.config_parser.get_network_settings() + + self.manager = multiprocessing.Manager() + self.send_variables = self.manager.dict(self.config_parser.send_variables) + self.receive_variables = self.manager.dict(self.config_parser.receive_variables) + self.stop_event = multiprocessing.Event() + self.start_event = multiprocessing.Event() # ✅ NEW + + self.safety_manager = SafetyManager(self.config_parser.safety_limits) + + # ✅ Create NetworkProcess but don't start communication yet + self.network_process = NetworkProcess( + network_settings["ip"], + network_settings["port"], + self.send_variables, + self.receive_variables, + self.stop_event, + self.config_parser, + self.start_event + ) + self.network_process.start() + self.logger = None + + def start(self): + """Send start signal to NetworkProcess and run control loop.""" + logging.info("RSIClient sending start signal to NetworkProcess...") + self.start_event.set() + self.running = True + + logging.info("RSI Client Started") + + try: + while self.running and not self.stop_event.is_set(): + time.sleep(2) + except KeyboardInterrupt: + self.stop() + except Exception as e: + logging.error(f"RSI Client encountered an error: {e}") + + def stop(self): + """Stop the network process and the client thread safely.""" + logging.info("🛑 Stopping RSI Client...") + + self.running = False + self.stop_event.set() # ✅ Tell network process to exit nicely + + if self.network_process and self.network_process.is_alive(): + self.network_process.join(timeout=3) # ✅ Give it time to shutdown + if self.network_process.is_alive(): + logging.warning("⚠️ Forcing network process termination...") + self.network_process.terminate() + self.network_process.join() + + if hasattr(self, "thread") and self.thread and self.thread.is_alive(): + self.thread.join() + self.thread = None + + logging.info("✅ RSI Client Stopped") + + def reconnect(self): + """Reconnects the network process safely.""" + logging.info("Reconnecting RSI Client network...") + + if self.network_process and self.network_process.is_alive(): + self.stop_event.set() + self.network_process.terminate() + self.network_process.join() + + # Fresh new events + self.stop_event = multiprocessing.Event() + self.start_event = multiprocessing.Event() + + # Create new network process + network_settings = self.config_parser.get_network_settings() + self.network_process = NetworkProcess( + network_settings["ip"], + network_settings["port"], + self.send_variables, + self.receive_variables, + self.stop_event, + self.config_parser, + self.start_event + ) + self.network_process.start() + + # Fresh control thread + self.thread = threading.Thread(target=self.start, daemon=True) + self.thread.start() diff --git a/src/RSIPI/rsi_config.py b/src/RSIPI/rsi_config.py new file mode 100644 index 0000000..7de83aa --- /dev/null +++ b/src/RSIPI/rsi_config.py @@ -0,0 +1,174 @@ +import xml.etree.ElementTree as ET +import logging +from src.RSIPI.rsi_limit_parser import parse_rsi_limits + +# ✅ Configure Logging (toggleable) +LOGGING_ENABLED = False # Change too False to silence logging output + +if LOGGING_ENABLED: + logging.basicConfig( + filename="rsi_config.log", + level=logging.DEBUG, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + +class RSIConfig: + """ + Loads and parses the RSI EthernetConfig.xml file, extracting: + - Network communication settings + - Variables to send/receive (with correct structure) + - Optional safety limit data from .rsi.xml file + """ + + # Known internal RSI variables and their structure + internal = { + "ComStatus": "String", + "RIst": ["X", "Y", "Z", "A", "B", "C"], + "RSol": ["X", "Y", "Z", "A", "B", "C"], + "AIPos": ["A1", "A2", "A3", "A4", "A5", "A6"], + "ASPos": ["A1", "A2", "A3", "A4", "A5", "A6"], + "ELPos": ["E1", "E2", "E3", "E4", "E5", "E6"], + "ESPos": ["E1", "E2", "E3", "E4", "E5", "E6"], + "MaCur": ["A1", "A2", "A3", "A4", "A5", "A6"], + "MECur": ["E1", "E2", "E3", "E4", "E5", "E6"], + "IPOC": 0, + "BMode": "Status", + "IPOSTAT": "", + "Delay": ["D"], + "EStr": "EStr Test", + "Tech.C1": ["C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C110"], + "Tech.C2": ["C21", "C22", "C23", "C24", "C25", "C26", "C27", "C28", "C29", "C210"], + "Tech.T2": ["T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T210"], + } + + def __init__(self, config_file, rsi_limits_file=None): + """ + Initialise config loader. + + Args: + config_file (str): Path to the RSI EthernetConfig.xml file. + rsi_limits_file (str): Optional path to .rsi.xml safety limits. + """ + self.config_file = config_file + self.rsi_limits_file = rsi_limits_file + self.safety_limits = {} + + self.network_settings = {} + self.send_variables = {} + self.receive_variables = {} + + self.load_config() + self.load_safety_limits() # Optional safety overlay + + def load_safety_limits(self): + """Loads safety bands from an optional .rsi.xml file, if provided.""" + if self.rsi_limits_file: + try: + self.safety_limits = parse_rsi_limits(self.rsi_limits_file) + logging.info(f"Loaded safety limits from {self.rsi_limits_file}") + except Exception as e: + logging.warning(f"Failed to load RSI safety limits: {e}") + self.safety_limits = {} + + @staticmethod + def strip_def_prefix(tag): + """Removes DEF_ prefix from variable names.""" + return tag.replace("DEF_", "") + + def process_internal_variable(self, tag): + """Initialises structured internal variables based on known RSI types.""" + if tag in self.internal: + if isinstance(self.internal[tag], list): + return {key: 0.0 for key in self.internal[tag]} + return self.internal[tag] + return None + + def process_variable_structure(self, var_dict, tag, var_type): + """ + Parses and groups structured variables, e.g., Tech.T2 → {'Tech': {'T2': 0.0}}. + + Args: + var_dict (dict): Either send_variables or receive_variables. + tag (str): The variable tag from XML. + var_type (str): The TYPE attribute from XML. + """ + if tag in self.internal: + var_dict[tag] = self.process_internal_variable(tag) + elif "." in tag: + base, subkey = tag.split(".", 1) + if base not in var_dict: + var_dict[base] = {} + var_dict[base][subkey] = self.get_default_value(var_type) + else: + var_dict[tag] = self.get_default_value(var_type) + + @staticmethod + def get_default_value(var_type): + """Returns a suitable default value for a given variable type.""" + if var_type == "BOOL": + return False + elif var_type == "STRING": + return "" + elif var_type == "LONG": + return 0 + elif var_type == "DOUBLE": + return 0.0 + return None # Fallback for unknown types + + def load_config(self): + """ + Parses the RSI config.xml, extracting: + - IP/port and communication mode + - Structured send and receive variable templates + """ + try: + logging.info(f"Loading config file: {self.config_file}") + tree = ET.parse(self.config_file) + root = tree.getroot() + + # Extract network settings + config = root.find("CONFIG") + self.network_settings = { + "ip": config.find("IP_NUMBER").text.strip(), + "port": int(config.find("PORT").text.strip()), + "sentype": config.find("SENTYPE").text.strip(), + "onlysend": config.find("ONLYSEND").text.strip().upper() == "TRUE", + } + logging.info(f"Network settings loaded: {self.network_settings}") + + # Extract section + send_section = root.find("SEND/ELEMENTS") + for element in send_section.findall("ELEMENT"): + tag = self.strip_def_prefix(element.get("TAG")) + var_type = element.get("TYPE") + if tag != "FREE": # Ignore placeholder entries + self.process_variable_structure(self.send_variables, tag, var_type) + + # Extract section + receive_section = root.find("RECEIVE/ELEMENTS") + for element in receive_section.findall("ELEMENT"): + tag = self.strip_def_prefix(element.get("TAG")) + var_type = element.get("TYPE") + if tag != "FREE": + self.process_variable_structure(self.receive_variables, tag, var_type) + + logging.info("Configuration successfully loaded.") + logging.debug(f"Send Variables: {self.send_variables}") + logging.debug(f"Receive Variables: {self.receive_variables}") + + except Exception as e: + logging.error(f"Error loading {self.config_file}: {e}") + + def get_network_settings(self): + """Returns network configuration (IP, port, SENTYPE, ONLYSEND).""" + return self.network_settings + + def get_send_variables(self): + """Returns structured send variable dictionary.""" + return self.send_variables + + def get_receive_variables(self): + """Returns structured receive variable dictionary.""" + return self.receive_variables diff --git a/src/RSIPI/rsi_echo_server.py b/src/RSIPI/rsi_echo_server.py new file mode 100644 index 0000000..6d07478 --- /dev/null +++ b/src/RSIPI/rsi_echo_server.py @@ -0,0 +1,173 @@ +import socket +import time +import xml.etree.ElementTree as ET +import logging +import threading +from src.RSIPI.rsi_config import RSIConfig + +# ✅ Toggle logging for debugging purposes +LOGGING_ENABLED = True + +if LOGGING_ENABLED: + logging.basicConfig( + filename="echo_server.log", + level=logging.DEBUG, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + +class EchoServer: + """ + Simulates a KUKA RSI UDP server for testing. + + - Responds to incoming RSI correction commands. + - Updates internal position state (absolute/relative). + - Returns structured XML messages (like a real robot). + """ + + def __init__(self, config_file, delay_ms=4, mode="relative"): + """ + Initialise the echo server. + + Args: + config_file (str): Path to RSI EthernetConfig.xml. + delay_ms (int): Delay between messages in milliseconds. + mode (str): Correction mode ("relative" or "absolute"). + """ + self.config = RSIConfig(config_file) + network_settings = self.config.get_network_settings() + + self.server_address = ("0.0.0.0", 50000) # Local bind + self.client_address = ("127.0.0.1", network_settings["port"]) # Client to echo back to + self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.udp_socket.bind(self.server_address) + + self.last_received = None + self.ipoc_value = 123456 + self.delay_ms = delay_ms / 1000 # Convert to seconds + self.mode = mode.lower() + + # Internal state to simulate robot values + self.state = { + "RIst": {k: 0.0 for k in ["X", "Y", "Z", "A", "B", "C"]}, + "AIPos": {f"A{i}": 0.0 for i in range(1, 7)}, + "ELPos": {f"E{i}": 0.0 for i in range(1, 7)}, + "DiO": 0, + "DiL": 0 + } + + self.running = True + self.thread = threading.Thread(target=self.send_message, daemon=True) + + logging.info(f"Echo Server started on {self.server_address}") + print(f"Echo Server started in {self.mode.upper()} mode.") + + def receive_and_process(self): + """ + Handles one incoming UDP message and updates the internal state accordingly. + Supports RKorr, AKorr, DiO, DiL, and IPOC updates. + """ + try: + self.udp_socket.settimeout(self.delay_ms) + data, addr = self.udp_socket.recvfrom(1024) + xml_string = data.decode() + root = ET.fromstring(xml_string) + self.last_received = xml_string + + for elem in root: + tag = elem.tag + if tag in ["RKorr", "AKorr"]: + for axis, value in elem.attrib.items(): + value = float(value) + if tag == "RKorr" and axis in self.state["RIst"]: + # Apply Cartesian correction + if self.mode == "relative": + self.state["RIst"][axis] += value + else: + self.state["RIst"][axis] = value + elif tag == "AKorr" and axis in self.state["AIPos"]: + # Apply joint correction + if self.mode == "relative": + self.state["AIPos"][axis] += value + else: + self.state["AIPos"][axis] = value + elif tag in ["DiO", "DiL"]: + if tag in self.state: + self.state[tag] = int(elem.text.strip()) + elif tag == "IPOC": + self.ipoc_value = int(elem.text.strip()) + + logging.debug(f"Processed input: {ET.tostring(root).decode()}") + except socket.timeout: + pass # No data within delay window + except ConnectionResetError: + print("⚠️ Connection was reset by client. Waiting before retry...") + time.sleep(0.5) + except Exception as e: + print(f"[ERROR] Failed to process input: {e}") + + def generate_message(self): + """ + Creates a reply XML message based on current state. + Format matches KUKA RSI's expected response structure. + """ + root = ET.Element("Rob", Type="KUKA") + + for key in ["RIst", "AIPos", "ELPos"]: + element = ET.SubElement(root, key) + for sub_key, value in self.state[key].items(): + element.set(sub_key, f"{value:.2f}") + + for key in ["DiO", "DiL"]: + ET.SubElement(root, key).text = str(self.state[key]) + + ET.SubElement(root, "IPOC").text = str(self.ipoc_value) + return ET.tostring(root, encoding="utf-8").decode() + + def send_message(self): + """ + Main loop to receive input, update state, and send reply. + Runs in a background thread until stopped. + """ + while self.running: + try: + self.receive_and_process() + response = self.generate_message() + self.udp_socket.sendto(response.encode(), self.client_address) + self.ipoc_value += 4 + time.sleep(self.delay_ms) + except Exception as e: + print(f"[ERROR] EchoServer error: {e}") + time.sleep(1) + + def start(self): + """Starts the echo server loop in a background thread.""" + self.running = True + self.thread.start() + + def stop(self): + """Stops the echo server and cleans up the socket.""" + print("Stopping Echo Server...") + self.running = False + self.thread.join() + self.udp_socket.close() + print("✅ Echo Server Stopped.") + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="Run Echo Server for RSI Simulation") + parser.add_argument("--config", type=str, default="RSI_EthernetConfig.xml", help="Path to RSI config file") + parser.add_argument("--mode", type=str, choices=["relative", "absolute"], default="relative", help="Correction mode") + parser.add_argument("--delay", type=int, default=4, help="Delay between messages in ms") + + args = parser.parse_args() + server = EchoServer(config_file=args.config, delay_ms=args.delay, mode=args.mode) + + try: + server.start() + while True: + time.sleep(1) + except KeyboardInterrupt: + server.stop() diff --git a/src/RSIPI/rsi_graphing.py b/src/RSIPI/rsi_graphing.py new file mode 100644 index 0000000..539a7ad --- /dev/null +++ b/src/RSIPI/rsi_graphing.py @@ -0,0 +1,193 @@ +import time +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from collections import deque +from .rsi_client import RSIClient +import csv + +class RSIGraphing: + """ + Handles real-time plotting of RSI data with support for: + - Position/velocity/acceleration/force monitoring + - Live deviation alerts + - Optional overlay of planned vs actual trajectories + """ + + def __init__(self, client, mode="position", overlay=False, plan_file=None): + """ + Initialise live graphing interface. + + Args: + client (RSIClient): Live RSI client instance providing data. + mode (str): One of "position", "velocity", "acceleration", "force". + overlay (bool): Whether to show planned vs. actual overlays. + plan_file (str): Optional CSV file containing planned trajectory. + """ + self.client = client + self.mode = mode + self.overlay = overlay + self.alerts_enabled = True + self.deviation_threshold = 5.0 # mm + self.force_threshold = 10.0 # Nm + self.fig, self.ax = plt.subplots(figsize=(10, 6)) + + # Live data buffers + self.time_data = deque(maxlen=100) + self.position_data = {axis: deque(maxlen=100) for axis in ["X", "Y", "Z"]} + self.velocity_data = {axis: deque(maxlen=100) for axis in ["X", "Y", "Z"]} + self.acceleration_data = {axis: deque(maxlen=100) for axis in ["X", "Y", "Z"]} + self.force_data = {axis: deque(maxlen=100) for axis in ["A1", "A2", "A3", "A4", "A5", "A6"]} + + self.previous_positions = {"X": 0, "Y": 0, "Z": 0} + self.previous_velocities = {"X": 0, "Y": 0, "Z": 0} + self.previous_time = time.time() + + # Overlay comparison + self.planned_data = {axis: deque(maxlen=100) for axis in ["X", "Y", "Z"]} + self.deviation_data = {axis: deque(maxlen=100) for axis in ["X", "Y", "Z"]} + + if plan_file: + self.load_plan(plan_file) + + self.ani = animation.FuncAnimation(self.fig, self.update_graph, interval=100, cache_frame_data=False) + plt.show() + + def update_graph(self, frame): + """ + Called periodically by matplotlib to refresh live graph based on current mode. + Also checks for force spikes and deviation alerts. + """ + current_time = time.time() + dt = current_time - self.previous_time + self.previous_time = current_time + + position = self.client.receive_variables.get("RIst", {"X": 0, "Y": 0, "Z": 0}) + force = self.client.receive_variables.get("MaCur", {"A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0, "A6": 0}) + + # Compute motion derivatives + for axis in ["X", "Y", "Z"]: + velocity = (position[axis] - self.previous_positions[axis]) / dt if dt > 0 else 0 + acceleration = (velocity - self.previous_velocities[axis]) / dt if dt > 0 else 0 + self.previous_positions[axis] = position[axis] + self.previous_velocities[axis] = velocity + + self.position_data[axis].append(position[axis]) + self.velocity_data[axis].append(velocity) + self.acceleration_data[axis].append(acceleration) + + for axis in ["A1", "A2", "A3", "A4", "A5", "A6"]: + self.force_data[axis].append(force[axis]) + + self.time_data.append(time.strftime("%H:%M:%S")) + + # Compare to planned overlay + if self.overlay and self.planned_data: + for axis in ["X", "Y", "Z"]: + planned_value = self.planned_data[axis][-1] if len(self.planned_data[axis]) > 0 else position[axis] + self.planned_data[axis].append(planned_value) + deviation = abs(position[axis] - planned_value) + self.deviation_data[axis].append(deviation) + + if self.alerts_enabled and deviation > self.deviation_threshold: + print(f"⚠️ Deviation Alert! {axis} exceeds {self.deviation_threshold} mm (Deviation: {deviation:.2f} mm)") + + if self.alerts_enabled: + for axis in ["A1", "A2", "A3", "A4", "A5", "A6"]: + if self.force_data[axis][-1] > self.force_threshold: + print(f"⚠️ Force Spike Alert! {axis} exceeds {self.force_threshold} Nm (Force: {self.force_data[axis][-1]:.2f} Nm)") + + self.ax.clear() + + if self.mode == "position": + self.ax.plot(self.time_data, self.position_data["X"], label="X Position") + self.ax.plot(self.time_data, self.position_data["Y"], label="Y Position") + self.ax.plot(self.time_data, self.position_data["Z"], label="Z Position") + self.ax.set_title("Live Position Tracking with Alerts") + self.ax.set_ylabel("Position (mm)") + + if self.overlay: + self.ax.plot(self.time_data, self.planned_data["X"], label="Planned X", linestyle="dashed") + self.ax.plot(self.time_data, self.planned_data["Y"], label="Planned Y", linestyle="dashed") + self.ax.plot(self.time_data, self.planned_data["Z"], label="Planned Z", linestyle="dashed") + + self.ax.legend() + self.ax.set_xlabel("Time") + self.ax.tick_params(axis='x', rotation=45) + + def change_mode(self, mode): + """Switch graphing mode at runtime (position, velocity, acceleration, force).""" + if mode in ["position", "velocity", "acceleration", "force"]: + self.mode = mode + print(f"Graphing mode changed to: {mode}") + else: + print("Invalid mode. Available: position, velocity, acceleration, force") + + def set_alert_threshold(self, alert_type, threshold): + """Update threshold values for alerts.""" + if alert_type == "deviation": + self.deviation_threshold = threshold + elif alert_type == "force": + self.force_threshold = threshold + print(f"{alert_type.capitalize()} alert threshold set to {threshold}") + + def enable_alerts(self, enable): + """Enable or disable real-time alerts.""" + self.alerts_enabled = enable + print(f"Alerts {'enabled' if enable else 'disabled'}.") + + def stop(self): + """Gracefully stop plotting by closing the figure.""" + plt.close(self.fig) + + def load_plan(self, plan_file): + """Load planned XYZ trajectory from CSV for overlay comparison.""" + with open(plan_file, newline='') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + for axis in ["X", "Y", "Z"]: + key = f"Send.RKorr.{axis}" + value = float(row.get(key, 0.0)) + self.planned_data[axis].append(value) + + @staticmethod + def plot_csv_file(csv_path): + """Standalone method to plot XYZ position from a log file (no live client required).""" + timestamps = [] + x_data, y_data, z_data = [], [], [] + + with open(csv_path, newline='') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + timestamps.append(row["Timestamp"]) + x_data.append(float(row.get("Receive.RIst.X", 0.0))) + y_data.append(float(row.get("Receive.RIst.Y", 0.0))) + z_data.append(float(row.get("Receive.RIst.Z", 0.0))) + + plt.figure(figsize=(10, 6)) + plt.plot(timestamps, x_data, label="X") + plt.plot(timestamps, y_data, label="Y") + plt.plot(timestamps, z_data, label="Z") + plt.title("Position from CSV Log") + plt.xlabel("Time") + plt.ylabel("Position (mm)") + plt.xticks(rotation=45) + plt.legend() + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="RSI Graphing Utility") + parser.add_argument("--mode", choices=["position", "velocity", "acceleration", "force"], default="position", help="Graphing mode") + parser.add_argument("--overlay", action="store_true", help="Enable planned vs. actual overlay") + parser.add_argument("--plan", type=str, help="CSV file with planned trajectory") + parser.add_argument("--alerts", action="store_true", help="Enable real-time alerts") + args = parser.parse_args() + + client = RSIClient("../../examples/RSI_EthernetConfig.xml") + graphing = RSIGraphing(client, mode=args.mode, overlay=args.overlay, plan_file=args.plan) + + if not args.alerts: + graphing.enable_alerts(False) diff --git a/src/RSIPI/rsi_limit_parser.py b/src/RSIPI/rsi_limit_parser.py new file mode 100644 index 0000000..dcfc267 --- /dev/null +++ b/src/RSIPI/rsi_limit_parser.py @@ -0,0 +1,74 @@ +import xml.etree.ElementTree as ET + +def parse_rsi_limits(xml_path): + """ + Parses a .rsi.xml file (RSIObject format) and returns structured safety limits. + + Returns: + dict: Structured limits in the form { "RKorr.X": (min, max), "AKorr.A1": (min, max), ... } + """ + tree = ET.parse(xml_path) + root = tree.getroot() + raw_limits = {} + + for rsi_object in root.findall("RSIObject"): + obj_type = rsi_object.attrib.get("ObjType", "") + params = rsi_object.find("Parameters") + + if params is None: + continue # Skip malformed entries + + if obj_type == "POSCORR": + # Cartesian position correction limits + for param in params.findall("Parameter"): + name = param.attrib["Name"] + value = float(param.attrib["ParamValue"]) + if name == "LowerLimX": + raw_limits["RKorr.X_min"] = value + elif name == "UpperLimX": + raw_limits["RKorr.X_max"] = value + elif name == "LowerLimY": + raw_limits["RKorr.Y_min"] = value + elif name == "UpperLimY": + raw_limits["RKorr.Y_max"] = value + elif name == "LowerLimZ": + raw_limits["RKorr.Z_min"] = value + elif name == "UpperLimZ": + raw_limits["RKorr.Z_max"] = value + elif name == "MaxRotAngle": + # Apply symmetric bounds to A/B/C + for axis in ["A", "B", "C"]: + raw_limits[f"RKorr.{axis}_min"] = -value + raw_limits[f"RKorr.{axis}_max"] = value + + elif obj_type == "AXISCORR": + # Joint axis correction limits + for param in params.findall("Parameter"): + name = param.attrib["Name"] + value = float(param.attrib["ParamValue"]) + if name.startswith("LowerLimA") or name.startswith("UpperLimA"): + axis = name[-1] + key = f"AKorr.A{axis}_{'min' if 'Lower' in name else 'max'}" + raw_limits[key] = value + + elif obj_type == "AXISCORREXT": + # External axis correction limits + for param in params.findall("Parameter"): + name = param.attrib["Name"] + value = float(param.attrib["ParamValue"]) + if name.startswith("LowerLimE") or name.startswith("UpperLimE"): + axis = name[-1] + key = f"AKorr.E{axis}_{'min' if 'Lower' in name else 'max'}" + raw_limits[key] = value + + # Combine _min and _max entries into structured tuples + structured_limits = {} + for key in list(raw_limits.keys()): + if key.endswith("_min"): + base = key[:-4] + min_val = raw_limits.get(f"{base}_min") + max_val = raw_limits.get(f"{base}_max") + if min_val is not None and max_val is not None: + structured_limits[base] = (min_val, max_val) + + return structured_limits diff --git a/src/RSIPI/safety_manager.py b/src/RSIPI/safety_manager.py new file mode 100644 index 0000000..721a0d9 --- /dev/null +++ b/src/RSIPI/safety_manager.py @@ -0,0 +1,110 @@ +import logging + + +class SafetyManager: + """ + Enforces safety limits for RSI motion commands. + + Supports: + - Emergency stop logic (halts all validation) + - Limit enforcement for RKorr / AKorr / other variables + - Runtime limit updates + """ + + def __init__(self, limits=None): + """ + Args: + limits (dict): Optional safety limits in the form: + { + 'RKorr.X': (-5.0, 5.0), + 'AKorr.A1': (-6.0, 6.0), + ... + } + """ + self.limits = limits if limits is not None else {} + self.e_stop = False + self.last_values = {} # Reserved for future tracking or override detection + self.override = False # ➡️ Track if safety checks are overridden + + def validate(self, path: str, value: float) -> float: + if self.override: + # Bypass all safety checks when override is active + return value + + if self.e_stop: + logging.warning(f"SafetyManager: {path} update blocked (E-STOP active)") + raise RuntimeError(f"SafetyManager: E-STOP active. Motion blocked for {path}.") + + if path in self.limits: + min_val, max_val = self.limits[path] + if not (min_val <= value <= max_val): + logging.warning(f"SafetyManager: {path}={value} blocked (out of bounds {min_val} to {max_val})") + raise ValueError(f"SafetyManager: {path}={value} is out of bounds ({min_val} to {max_val})") + + return value + + def emergency_stop(self): + """Activates emergency stop: all motion validation will fail.""" + self.e_stop = True + + def reset_stop(self): + """Resets emergency stop, allowing motion again.""" + self.e_stop = False + + def set_limit(self, path: str, min_val: float, max_val: float): + """Sets or overrides a safety limit at runtime.""" + self.limits[path] = (min_val, max_val) + + def get_limits(self): + """Returns a copy of all current safety limits.""" + return self.limits.copy() + + def is_stopped(self): + """Returns whether the emergency stop is active.""" + return self.e_stop + + def override_safety(self, enable: bool): + """Enable or disable safety override (bypass all checks).""" + self.override = enable + + def is_safety_overridden(self) -> bool: + """Returns whether safety override is active.""" + return self.override + + @staticmethod + def check_cartesian_limits(pose): + """ + Check if a Cartesian pose is within general robot limits. + Typical bounds: ±1500 mm in XYZ, ±360° in orientation. + """ + limits = { + "X": (-1500, 1500), + "Y": (-1500, 1500), + "Z": (0, 2000), + "A": (-360, 360), + "B": (-360, 360), + "C": (-360, 360), + } + for key, (lo, hi) in limits.items(): + if key in pose and not (lo <= pose[key] <= hi): + return False + return True + + @staticmethod + def check_joint_limits(pose): + """ + Check if a joint-space pose is within KUKA limits. + Typical KUKA ranges: A1–A6 in defined degrees. + """ + limits = { + "A1": (-185, 185), + "A2": (-185, 185), + "A3": (-185, 185), + "A4": (-350, 350), + "A5": (-130, 130), + "A6": (-350, 350), + } + for key, (lo, hi) in limits.items(): + if key in pose and not (lo <= pose[key] <= hi): + return False + return True \ No newline at end of file diff --git a/src/RSIPI/static_plotter.py b/src/RSIPI/static_plotter.py new file mode 100644 index 0000000..c389c5c --- /dev/null +++ b/src/RSIPI/static_plotter.py @@ -0,0 +1,158 @@ +# Re-execute since code state was reset +static_plotter_path = "/mnt/data/static_plotter.py" + +import csv +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + +class StaticPlotter: + + @staticmethod + def _load_csv(csv_path): + data = { + "time": [], + "x": [], "y": [], "z": [], + "vx": [], "vy": [], "vz": [], + "ax": [], "ay": [], "az": [], + "joints": {f"A{i}": [] for i in range(1, 7)}, + "force": {f"A{i}": [] for i in range(1, 7)} + } + + with open(csv_path, newline='') as f: + reader = csv.DictReader(f) + for row in reader: + data["time"].append(row.get("Timestamp", "")) + data["x"].append(float(row.get("Receive.RIst.X", 0))) + data["y"].append(float(row.get("Receive.RIst.Y", 0))) + data["z"].append(float(row.get("Receive.RIst.Z", 0))) + for i in range(1, 7): + data["joints"][f"A{i}"].append(float(row.get(f"Receive.AIPos.A{i}", 0))) + data["force"][f"A{i}"].append(float(row.get(f"Receive.MaCur.A{i}", 0))) + return data + + @staticmethod + def plot_3d_trajectory(csv_path): + data = StaticPlotter._load_csv(csv_path) + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + ax.plot(data["x"], data["y"], data["z"], label="TCP Path") + ax.set_xlabel("X [mm]") + ax.set_ylabel("Y [mm]") + ax.set_zlabel("Z [mm]") + ax.set_title("3D TCP Trajectory") + ax.legend() + plt.tight_layout() + plt.show() + + @staticmethod + def plot_2d_projection(csv_path, plane="xy"): + data = StaticPlotter._load_csv(csv_path) + x, y = { + "xy": (data["x"], data["y"]), + "xz": (data["x"], data["z"]), + "yz": (data["y"], data["z"]), + }.get(plane, (data["x"], data["y"])) + plt.plot(x, y) + plt.title(f"2D Trajectory Projection ({plane.upper()})") + plt.xlabel(f"{plane[0].upper()} [mm]") + plt.ylabel(f"{plane[1].upper()} [mm]") + plt.grid(True) + plt.tight_layout() + plt.show() + + @staticmethod + def plot_position_vs_time(csv_path): + data = StaticPlotter._load_csv(csv_path) + plt.plot(data["time"], data["x"], label="X") + plt.plot(data["time"], data["y"], label="Y") + plt.plot(data["time"], data["z"], label="Z") + plt.title("TCP Position vs Time") + plt.xlabel("Time") + plt.ylabel("Position [mm]") + plt.legend() + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() + + @staticmethod + def plot_joint_angles(csv_path): + data = StaticPlotter._load_csv(csv_path) + for joint, values in data["joints"].items(): + plt.plot(data["time"], values, label=joint) + plt.title("Joint Angles vs Time") + plt.xlabel("Time") + plt.ylabel("Angle [deg]") + plt.legend() + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() + + @staticmethod + def plot_motor_currents(csv_path): + data = StaticPlotter._load_csv(csv_path) + for joint, values in data["force"].items(): + plt.plot(data["time"], values, label=joint) + plt.title("Motor Current (Torque Proxy) vs Time") + plt.xlabel("Time") + plt.ylabel("Current [Nm]") + plt.legend() + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() + + @staticmethod + def plot_velocity_vs_time(csv_path): + data = StaticPlotter._load_csv(csv_path) + vx = [0] + [(data["x"][i] - data["x"][i - 1]) for i in range(1, len(data["x"]))] + vy = [0] + [(data["y"][i] - data["y"][i - 1]) for i in range(1, len(data["y"]))] + vz = [0] + [(data["z"][i] - data["z"][i - 1]) for i in range(1, len(data["z"]))] + plt.plot(data["time"], vx, label="dX/dt") + plt.plot(data["time"], vy, label="dY/dt") + plt.plot(data["time"], vz, label="dZ/dt") + plt.title("Velocity vs Time") + plt.xlabel("Time") + plt.ylabel("Velocity [mm/s]") + plt.legend() + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() + + @staticmethod + def plot_acceleration_vs_time(csv_path): + data = StaticPlotter._load_csv(csv_path) + vx = [0] + [(data["x"][i] - data["x"][i - 1]) for i in range(1, len(data["x"]))] + vy = [0] + [(data["y"][i] - data["y"][i - 1]) for i in range(1, len(data["y"]))] + vz = [0] + [(data["z"][i] - data["z"][i - 1]) for i in range(1, len(data["z"]))] + ax = [0] + [(vx[i] - vx[i - 1]) for i in range(1, len(vx))] + ay = [0] + [(vy[i] - vy[i - 1]) for i in range(1, len(vy))] + az = [0] + [(vz[i] - vz[i - 1]) for i in range(1, len(vz))] + plt.plot(data["time"], ax, label="d²X/dt²") + plt.plot(data["time"], ay, label="d²Y/dt²") + plt.plot(data["time"], az, label="d²Z/dt²") + plt.title("Acceleration vs Time") + plt.xlabel("Time") + plt.ylabel("Acceleration [mm/s²]") + plt.legend() + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() + + @staticmethod + def plot_deviation(csv_actual, csv_planned): + actual = StaticPlotter._load_csv(csv_actual) + planned = StaticPlotter._load_csv(csv_planned) + deviation = { + "x": [abs(a - b) for a, b in zip(actual["x"], planned["x"])], + "y": [abs(a - b) for a, b in zip(actual["y"], planned["y"])], + "z": [abs(a - b) for a, b in zip(actual["z"], planned["z"])] + } + plt.plot(actual["time"], deviation["x"], label="X Deviation") + plt.plot(actual["time"], deviation["y"], label="Y Deviation") + plt.plot(actual["time"], deviation["z"], label="Z Deviation") + plt.title("Deviation (Actual - Planned) vs Time") + plt.xlabel("Time") + plt.ylabel("Deviation [mm]") + plt.legend() + plt.xticks(rotation=45) + plt.tight_layout() + plt.show() diff --git a/src/RSIPI/trajectory_planner.py b/src/RSIPI/trajectory_planner.py new file mode 100644 index 0000000..a735033 --- /dev/null +++ b/src/RSIPI/trajectory_planner.py @@ -0,0 +1,65 @@ +from RSIPI.safety_manager import SafetyManager +import time + +def generate_trajectory(start, end, steps=100, space="cartesian", mode="absolute", include_resets=False): + """ + Generates a trajectory from start to end across N steps. + + - Absolute mode (default): full poses, no resets + - Relative mode: incremental steps, optional resets after each step + """ + if mode not in ["relative", "absolute"]: + raise ValueError("mode must be 'relative' or 'absolute'") + if space not in ["cartesian", "joint"]: + raise ValueError("space must be 'cartesian' or 'joint'") + + if mode == "absolute": + include_resets = False # Smart safeguard + + axes = start.keys() + trajectory = [] + + # Optional safety check hook — assumes SafetyManager has static validation methods + safety_fn = SafetyManager.check_cartesian_limits if space == "cartesian" else SafetyManager.check_joint_limits + global enforce_safety + enforce_safety = hasattr(SafetyManager, "check_cartesian_limits") # Enable only if those methods exist + + + for i in range(1, steps + 1): + point = {} + for axis in axes: + delta = end[axis] - start[axis] + value = start[axis] + (delta * i / steps) + + point[axis] = delta / steps if mode == "relative" else value + + # Optional safety enforcement + if enforce_safety and not safety_fn(point): + raise ValueError(f"⚠️ Safety check failed at step {i}: {point}") + + trajectory.append(point) + + if mode == "relative" and include_resets: + # Insert a zero-correction step to prevent drift + trajectory.append({axis: 0.0 for axis in axes}) + + return trajectory + +def execute_trajectory(api, trajectory, space="cartesian", rate=0.004): + """ + Sends a list of corrections to the RSI API at fixed intervals. + + Args: + api: An RSI-compatible API object with update_cartesian / update_joints methods. + trajectory (list[dict]): Movement steps generated by generate_trajectory(). + space (str): "cartesian" or "joint". + rate (float): Time between steps in seconds (default = 4ms). + """ + for point in trajectory: + if space == "cartesian": + api.update_cartesian(**point) + elif space == "joint": + api.update_joints(**point) + else: + raise ValueError("space must be 'cartesian' or 'joint'") + time.sleep(rate) diff --git a/src/RSIPI/xml_handler.py b/src/RSIPI/xml_handler.py new file mode 100644 index 0000000..8f114bb --- /dev/null +++ b/src/RSIPI/xml_handler.py @@ -0,0 +1,58 @@ +import xml.etree.ElementTree as ET + +class XMLGenerator: + """ + Converts structured dictionaries of RSI send/receive variables into + valid XML strings for UDP transmission to/from the robot controller. + """ + + @staticmethod + def generate_send_xml(send_variables, network_settings): + """ + Build an outgoing XML message based on the current send variables. + + Args: + send_variables (dict): Structured dictionary of values to send. + network_settings (dict): Contains 'sentype' used for the root element. + + Returns: + str: XML-formatted string ready for UDP transmission. + """ + root = ET.Element("Sen", Type=network_settings["sentype"]) + + # Convert structured keys (e.g. RKorr, Tech) and flat elements (e.g. IPOC) + for key, value in send_variables.items(): + if key == "FREE": + continue # Skip unused placeholder fields + + if isinstance(value, dict): + element = ET.SubElement(root, key) + for sub_key, sub_value in value.items(): + element.set(sub_key, f"{float(sub_value):.2f}") + else: + ET.SubElement(root, key).text = str(value) + + return ET.tostring(root, encoding="utf-8").decode() + + @staticmethod + def generate_receive_xml(receive_variables): + """ + Build an incoming XML message for emulation/testing purposes. + + Args: + receive_variables (dict): Structured dictionary of values to simulate reception. + + Returns: + str: XML-formatted string mimicking a KUKA robot's reply. + """ + root = ET.Element("Rob", Type="KUKA") + + for key, value in receive_variables.items(): + if isinstance(value, dict) or hasattr(value, "items"): + element = ET.SubElement(root, key) + for sub_key, sub_value in value.items(): + element.set(sub_key, f"{float(sub_value):.2f}") + else: + ET.SubElement(root, key).text = str(value) + + return ET.tostring(root, encoding="utf-8").decode() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71703744635708e5f5af1ab2a38fb2bee9872bc3 GIT binary patch literal 266 zcmXw!!D<3Q42EYaEk%&NLXLXaK0qmg*2^N;#l_peGIirRpczS~E_>@+^we|d{1-9B!8`MSX& WU&m|Pf8;LOp`*1vsEctY_Wl65bxW%N literal 0 HcmV?d00001 diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25719b3b8461b30d225b5a4c1ce15626be79e633 GIT binary patch literal 254 zcmXw!L23d)5JjhB0zvQ&wTz4L03if4ZU(^&4(=L;j+SkMsgml}VRpGhmN`IWu+HYk(b2<>)MeEbv@d*r`v~DULSV&C^xZf O`o