diff --git a/LICENSE b/LICENSE index f288702..3edc958 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 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 General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is 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. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - 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. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - 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 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. Use with the GNU Affero General Public License. - - 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 Affero 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 special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 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 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 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. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - 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 GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2026 Ryan Malloy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index af9ada8..0aabe84 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,23 @@ # GR-MCP: GNU Radio MCP Server [![Python Version](https://img.shields.io/badge/python-3.14%2B-blue.svg)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -**GR-MCP** is a FastMCP server for [GNU Radio](https://www.gnuradio.org/) that enables programmatic, automated, and AI-driven creation and control of GNU Radio flowgraphs. It exposes 36 MCP tools for building, modifying, validating, running, and monitoring `.grc` files. +**GR-MCP** is a [FastMCP](https://gofastmcp.com) server for [GNU Radio](https://www.gnuradio.org/) that enables programmatic, automated, and AI-driven creation of GNU Radio flowgraphs. It exposes 80+ MCP tools for building, validating, running, and exporting `.grc` files — plus block development, protocol analysis, and OOT module management. -> **Why GR-MCP?** +> **What can you do with it?** > - Build and validate flowgraphs programmatically -> - Run flowgraphs in Docker containers with XML-RPC control -> - Adjust variables in real-time without restarting -> - Collect Python code coverage from containerized flowgraphs -> - Integrate with LLMs, automation frameworks, and custom tools - - -## Features - -### Flowgraph Building (15 tools) -Build, edit, and validate `.grc` files programmatically: -- `get_blocks` / `make_block` / `remove_block` - Block management -- `get_block_params` / `set_block_params` - Parameter control -- `get_block_sources` / `get_block_sinks` - Port inspection -- `get_connections` / `connect_blocks` / `disconnect_blocks` - Wiring -- `validate_block` / `validate_flowgraph` / `get_all_errors` - Validation -- `save_flowgraph` - Save to `.grc` file -- `get_all_available_blocks` - List available block types - -### Runtime Control (11 tools) -Run flowgraphs in Docker containers with headless QT rendering: -- `launch_flowgraph` - Start a flowgraph in a container (Xvfb + optional VNC) -- `list_containers` / `stop_flowgraph` / `remove_flowgraph` - Container lifecycle -- `connect` / `connect_to_container` / `disconnect` - XML-RPC connection -- `list_variables` / `get_variable` / `set_variable` - Real-time variable control -- `start` / `stop` / `lock` / `unlock` - Flowgraph execution control -- `capture_screenshot` / `get_container_logs` - Visual feedback -- `get_status` - Connection and container status - -### Coverage Collection (4 tools) -Collect Python code coverage from containerized flowgraphs: -- `collect_coverage` - Gather coverage data after flowgraph stops -- `generate_coverage_report` - Generate HTML/XML/JSON reports -- `combine_coverage` - Aggregate coverage across multiple runs -- `delete_coverage` - Clean up coverage data - - -## Requirements - -- Python >= 3.14 -- GNU Radio (tested with GRC v3.10.12.0) -- Docker (optional, for runtime control features) -- UV package manager +> - Generate custom GNU Radio blocks from natural language descriptions +> - Parse protocol specifications into decoder pipelines +> - Analyze IQ recordings to detect signal characteristics +> - Export blocks to distributable OOT modules +> - Run flowgraphs in Docker containers with real-time variable control +> - Install and manage OOT modules via Docker ## Quickstart -### 1. Clone and setup +### 1. Install ```bash git clone https://github.com/rsp2k/gr-mcp @@ -63,30 +28,99 @@ uv venv --system-site-packages --python 3.14 uv sync ``` -### 2. Configure your MCP client +### 2. Run -Add to Claude Desktop, Cursor, or other MCP client config: +```bash +uv run gnuradio-mcp +``` +### 3. Add to your MCP client + +**Claude Code:** +```bash +claude mcp add gnuradio-mcp -- uv run --directory /path/to/gr-mcp gnuradio-mcp +``` + +**Claude Desktop / Cursor / other MCP clients:** ```json { "mcpServers": { - "gr-mcp": { + "gnuradio-mcp": { "command": "uv", - "args": ["--directory", "/path/to/gr-mcp", "run", "main.py"] + "args": ["run", "--directory", "/path/to/gr-mcp", "gnuradio-mcp"] } } } ``` -### 3. (Optional) Build Docker images for runtime control +### Requirements -```bash -# Build the runtime image (Xvfb + VNC + ImageMagick) -docker build -f docker/Dockerfile.gnuradio-runtime -t gnuradio-runtime:latest docker/ +- Python >= 3.14 +- GNU Radio (tested with GRC v3.10.12.0) +- Docker (optional — for runtime control, block testing, OOT builds) +- [uv](https://docs.astral.sh/uv/) package manager -# Build the coverage image (adds python3-coverage) -docker build -f docker/Dockerfile.gnuradio-coverage -t gnuradio-coverage:latest docker/ -``` +> **Note:** GR-MCP is designed for single-session use. All connected MCP clients share the same flowgraph state. Run one server instance per concurrent session. + + +## Features + +### Flowgraph Building (30 tools) + +Build, edit, validate, and export `.grc` files: + +| Category | Tools | +|----------|-------| +| Blocks | `make_block`, `remove_block`, `get_blocks` | +| Parameters | `get_block_params`, `set_block_params` | +| Ports | `get_block_sources`, `get_block_sinks` | +| Connections | `connect_blocks`, `disconnect_blocks`, `get_connections` | +| Validation | `validate_block`, `validate_flowgraph`, `get_all_errors` | +| Persistence | `save_flowgraph`, `load_flowgraph` | +| Code Gen | `generate_code` | +| Discovery | `get_all_available_blocks`, `search_blocks`, `get_block_categories` | +| Options | `get_flowgraph_options`, `set_flowgraph_options` | +| Python | `create_embedded_python_block`, `evaluate_expression` | +| Bypass | `bypass_block`, `unbypass_block` | +| Import/Export | `export_flowgraph_data`, `import_flowgraph_data` | +| OOT Paths | `load_oot_blocks`, `add_block_path`, `get_block_paths` | + +### Block Development (18 tools, dynamically registered) + +Generate, validate, test, and export custom blocks. These tools are registered on-demand via `enable_block_dev_mode` to minimize context usage: + +| Category | Tools | +|----------|-------| +| Generation | `generate_sync_block`, `generate_basic_block`, `generate_interp_block`, `generate_decim_block` | +| Validation | `validate_block_code`, `parse_block_prompt` | +| Testing | `test_block_in_docker` | +| Integration | `inject_generated_block` | +| Protocol | `parse_protocol_spec`, `generate_decoder_chain`, `get_missing_oot_modules` | +| Signal | `analyze_iq_file` | +| OOT Export | `generate_oot_skeleton`, `export_block_to_oot`, `export_from_flowgraph` | +| Mode | `enable_block_dev_mode`, `disable_block_dev_mode`, `get_block_dev_mode` | + +### Runtime Control (36 tools) + +Run flowgraphs in Docker containers with real-time control: + +| Category | Tools | +|----------|-------| +| XML-RPC | `connect`, `disconnect`, `get_status`, `list_variables`, `get_variable`, `set_variable` | +| Execution | `start`, `stop`, `lock`, `unlock` | +| ControlPort | `connect_controlport`, `disconnect_controlport`, `get_knobs`, `set_knobs`, `get_knob_properties`, `get_performance_counters`, `post_message` | +| Docker | `launch_flowgraph`, `list_containers`, `stop_flowgraph`, `remove_flowgraph`, `connect_to_container`, `capture_screenshot`, `get_container_logs` | +| Coverage | `collect_coverage`, `generate_coverage_report`, `combine_coverage`, `delete_coverage` | +| OOT Mgmt | `detect_oot_modules`, `install_oot_module`, `list_oot_images`, `remove_oot_image`, `build_multi_oot_image`, `list_combo_images`, `remove_combo_image` | + +### MCP Resources + +| Resource URI | Description | +|-------------|-------------| +| `oot://directory` | Curated directory of 20 OOT modules (12 preinstalled) | +| `oot://directory/{module}` | Details for a specific OOT module | +| `prompts://block-generation/*` | Block generation patterns and templates | +| `prompts://protocol-analysis/*` | Decoder pipeline guidance | ## Usage Examples @@ -94,111 +128,158 @@ docker build -f docker/Dockerfile.gnuradio-coverage -t gnuradio-coverage:latest ### Building a flowgraph ```python -# Create a signal generator block +# Create blocks make_block(block_type="analog_sig_source_x", name="sig_source") +make_block(block_type="audio_sink", name="speaker") -# Set parameters +# Configure set_block_params(block_name="sig_source", params={ "freq": "1000", "amplitude": "0.5", "waveform": "analog.GR_COS_WAVE" }) -# Connect blocks +# Wire and save connect_blocks( source_block="sig_source", source_port="0", - sink_block="audio_sink", sink_port="0" + sink_block="speaker", sink_port="0" ) - -# Validate and save validate_flowgraph() save_flowgraph(path="/tmp/my_flowgraph.grc") ``` -### Running a flowgraph with runtime control +### Generating a custom block ```python -# Launch in Docker container +enable_block_dev_mode() + +generate_sync_block( + name="pm_demod", + description="Phase modulation demodulator", + inputs=[{"dtype": "complex", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + parameters=[{"name": "sensitivity", "dtype": "float", "default": 1.0}], + work_logic="Extract instantaneous phase from complex samples" +) +``` + +### Protocol analysis to decoder chain + +```python +enable_block_dev_mode() + +# Parse a protocol spec +protocol = parse_protocol_spec( + "GFSK at 250k baud, deviation: 25khz, preamble 0xAA, sync 0x2DD4" +) + +# Generate the decoder pipeline +chain = generate_decoder_chain(protocol=protocol, sample_rate=2000000.0) +# Returns: blocks, connections, variables, missing_oot_modules +``` + +### Exporting to an OOT module + +```python +enable_block_dev_mode() + +# Generate block +block = generate_sync_block(name="my_filter", ...) + +# Export to distributable OOT module +export_block_to_oot( + generated=block, + module_name="mymodule", + output_dir="/path/to/gr-mymodule", + author="Your Name" +) +# Creates: CMakeLists.txt, python/mymodule/my_filter.py, grc/mymodule_my_filter.block.yml +``` + +### Runtime control (Docker) + +```python +# Launch flowgraph in container launch_flowgraph( flowgraph_path="/path/to/flowgraph.py", name="my-sdr", xmlrpc_port=8080, - enable_vnc=True # Optional: VNC on port 5900 + enable_vnc=True ) -# Connect and control +# Tune in real-time connect_to_container(name="my-sdr") -list_variables() # See available variables -set_variable(name="freq", value=2.4e9) # Tune in real-time +set_variable(name="freq", value=2.4e9) -# Visual feedback -capture_screenshot(name="my-sdr") # Get QT GUI screenshot -get_container_logs(name="my-sdr") # Check for errors - -# Clean up +# Inspect and clean up +capture_screenshot(name="my-sdr") stop_flowgraph(name="my-sdr") -remove_flowgraph(name="my-sdr") -``` - -### Collecting code coverage - -```python -# Launch with coverage enabled -launch_flowgraph( - flowgraph_path="/path/to/flowgraph.py", - name="coverage-test", - enable_coverage=True -) - -# Run your test scenario... -# Then stop (graceful shutdown required for coverage data) -stop_flowgraph(name="coverage-test") - -# Collect and report -collect_coverage(name="coverage-test") -generate_coverage_report(name="coverage-test", format="html") -``` - - -## Development - -```bash -# Install dev dependencies -uv sync --all-extras - -# Run tests -pytest - -# Run with coverage -pytest --cov=gnuradio_mcp --cov-report=term-missing - -# Pre-commit hooks -pre-commit run --all-files ``` ## Architecture ``` -main.py # FastMCP app entry point src/gnuradio_mcp/ +├── server.py # FastMCP app entry point ├── models.py # Pydantic models for all tools +├── utils.py # Unique IDs, error formatting +├── oot_catalog.py # Curated OOT module directory ├── middlewares/ │ ├── platform.py # GNU Radio Platform wrapper -│ ├── flowgraph.py # Flowgraph block/connection management -│ ├── block.py # Block parameter/port access +│ ├── flowgraph.py # Block/connection management +│ ├── block.py # Parameter/port access +│ ├── ports.py # Port resolution utilities │ ├── docker.py # Docker container lifecycle -│ └── xmlrpc.py # XML-RPC variable control +│ ├── xmlrpc.py # XML-RPC variable control +│ ├── thrift.py # ControlPort/Thrift client +│ ├── oot.py # OOT module Docker builds +│ ├── block_generator.py # Code generation for custom blocks +│ ├── oot_exporter.py # Export blocks to OOT modules +│ └── protocol_analyzer.py # Protocol parsing, decoder chains, IQ analysis └── providers/ ├── base.py # PlatformProvider (flowgraph tools) ├── mcp.py # McpPlatformProvider (registers tools) - ├── runtime.py # RuntimeProvider (Docker/XML-RPC) - └── mcp_runtime.py # McpRuntimeProvider (registers tools) + ├── runtime.py # RuntimeProvider (Docker/XML-RPC/Thrift) + ├── mcp_runtime.py # McpRuntimeProvider (registers tools) + ├── block_dev.py # BlockDevProvider (generation/analysis) + └── mcp_block_dev.py # McpBlockDevProvider (dynamic registration) +``` + +**Data flow:** GNU Radio objects → Middlewares (validation/rewrite) → Pydantic Models (serialization) → MCP Tools + + +## Development + +```bash +# Install all dependencies +uv sync --all-extras + +# Run tests +pytest + +# Run specific test suite +pytest tests/unit/ +pytest tests/integration/ + +# Pre-commit hooks (black, flake8, isort, mypy) +pre-commit run --all-files ``` -## Project Status +## Docker Images (Optional) -**Active development.** Core flowgraph building is stable. Runtime control (Docker + XML-RPC) is Phase 1 complete. Coverage collection is functional. +For runtime control and block testing: -Contributions and feedback welcome! +```bash +# Runtime image (Xvfb + VNC + ImageMagick) +docker build -f docker/Dockerfile.gnuradio-runtime -t gnuradio-runtime:latest docker/ + +# Coverage image (adds python3-coverage) +docker build -f docker/Dockerfile.gnuradio-coverage -t gnuradio-coverage:latest docker/ +``` + + +## License + +[MIT](LICENSE) diff --git a/docs/block-dev-workflow.md b/docs/block-dev-workflow.md new file mode 100644 index 0000000..a40a72f --- /dev/null +++ b/docs/block-dev-workflow.md @@ -0,0 +1,524 @@ +# AI-Assisted Block Development Workflow + +This document describes the complete workflow for developing custom GNU Radio blocks using GR-MCP's block development tools. The workflow enables rapid iteration from concept to distributable OOT module. + +## Overview + +GR-MCP provides an AI-assisted development workflow that transforms signal processing requirements into working GNU Radio blocks: + +``` +Protocol Spec / IQ Recording + │ + ▼ +┌─────────────────────────┐ +│ Protocol Analysis │ parse_protocol_spec() +│ Signal Detection │ analyze_iq_file() +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Block Generation │ generate_sync_block() +│ Decoder Chains │ generate_decoder_chain() +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Validation & Test │ validate_block_code() +│ Docker Testing │ test_block_in_docker() +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ OOT Export │ export_block_to_oot() +│ Distribution │ generate_oot_skeleton() +└─────────────────────────┘ +``` + +## Enabling Block Dev Mode + +Block development tools are dynamically registered to minimize context usage. Enable them when needed: + +```python +# Check if enabled +get_block_dev_mode() + +# Enable block development tools +enable_block_dev_mode() + +# Disable when done +disable_block_dev_mode() +``` + +--- + +## Phase 1: Protocol Analysis + +Parse natural language protocol specifications into structured models. + +### parse_protocol_spec + +Extracts modulation, framing, and encoding parameters from protocol descriptions. + +```python +result = parse_protocol_spec( + spec_text=""" + GFSK signal at 250k baud, deviation: 25khz + Preamble: 0xAA (8 bytes) + Sync word: 0x2D, 0xD4 + CRC-16 at end of frame + """ +) + +# Returns ProtocolModel with: +# - modulation.scheme = "GFSK" +# - modulation.symbol_rate = 250000.0 +# - modulation.deviation = 25000.0 +# - framing.preamble = "0xAA" +# - framing.sync_word = "0x2D, 0xD4" +# - encoding.crc = "CRC-16" +``` + +**Supported Parameters:** + +| Category | Parameters | +|----------|------------| +| Modulation | scheme (FSK, GFSK, BPSK, QPSK, OFDM, CSS), symbol_rate, deviation, order | +| Framing | preamble, sync_word, header_format, frame_length | +| Encoding | fec_type, interleaving, whitening, crc | + +### generate_decoder_chain + +Creates a complete decoder pipeline from a parsed protocol specification. + +```python +# Parse protocol first +protocol = parse_protocol_spec("GFSK at 50k baud, deviation: 25khz") + +# Generate decoder chain +chain = generate_decoder_chain( + protocol=protocol, + sample_rate=2000000.0 +) + +# Returns DecoderPipelineModel with: +# - blocks: list of DecoderBlock with block_type, parameters +# - connections: list of (src_block, src_port, dst_block, dst_port) +# - variables: dict of flowgraph variables +# - missing_oot_modules: list of required OOT modules +``` + +**Generated Blocks by Modulation:** + +| Modulation | Blocks Generated | +|------------|------------------| +| FSK/GFSK | low_pass_filter → analog_quadrature_demod_cf → clock_recovery | +| BPSK | costas_loop_cc → constellation_decoder_cb | +| LoRa/CSS | freq_xlating_fir_filter → lora_demod (requires gr-lora_sdr) | + +### get_missing_oot_modules + +Check which OOT modules are required for a decoder chain. + +```python +# Parse a LoRa protocol +protocol = parse_protocol_spec("CSS/LoRa at SF7, 125kHz bandwidth") + +# Check missing modules +missing = get_missing_oot_modules(protocol) +# Returns: ["gr-lora_sdr"] +``` + +--- + +## Phase 2: Signal Analysis + +Analyze IQ recordings to identify signal characteristics. + +### analyze_iq_file + +Performs FFT-based spectral analysis and signal detection. + +```python +result = analyze_iq_file( + file_path="/path/to/recording.cf32", + sample_rate=2000000.0, + dtype="complex64" # or "complex128", "int16" +) + +# Returns IQAnalysisResult with: +# - sample_count: int +# - duration_seconds: float +# - power_stats: {min_db, max_db, mean_db, std_db} +# - spectral_features: {peak_frequency, bandwidth_3db, ...} +# - signals_detected: list of detected signal regions +``` + +**Supported Data Types:** + +| Format | dtype Parameter | +|--------|-----------------| +| Complex float32 (GNU Radio default) | `complex64` | +| Complex float64 | `complex128` | +| Interleaved int16 (RTL-SDR) | `int16` | + +--- + +## Phase 3: Block Generation + +Generate GNU Radio block code from specifications. + +### generate_sync_block + +Creates a `gr.sync_block` with 1:1 input/output sample relationship. + +```python +result = generate_sync_block( + name="pm_demod", + description="Phase modulation demodulator", + inputs=[{"dtype": "complex", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + parameters=[ + {"name": "sensitivity", "dtype": "float", "default": 1.0} + ], + work_logic="Extract instantaneous phase from complex samples" +) + +# Returns GeneratedBlockCode with: +# - source_code: complete Python block implementation +# - block_name: "pm_demod" +# - block_class: "sync_block" +# - is_valid: bool +# - validation_errors: list[str] +``` + +**Work Templates:** + +Pre-built templates for common operations: + +| Template | Description | +|----------|-------------| +| `gain` | Multiply samples by gain factor | +| `add` | Add constant to samples | +| `threshold` | Binary threshold comparison | + +```python +# Using a work template +result = generate_sync_block( + name="my_gain", + description="Variable gain", + inputs=[{"dtype": "float", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + parameters=[{"name": "gain", "dtype": "float", "default": 1.0}], + work_template="gain" +) +``` + +### generate_basic_block + +Creates a `gr.basic_block` with custom input/output ratios. + +```python +result = generate_basic_block( + name="frame_sync", + description="Frame synchronizer with variable output", + inputs=[{"dtype": "byte", "vlen": 1}], + outputs=[{"dtype": "byte", "vlen": 1}], + parameters=[ + {"name": "sync_word", "dtype": "int", "default": 0x2DD4} + ], + work_logic="Search for sync word and output aligned frames", + forecast_logic="noutput_items + len(self.buffer)" +) +``` + +### generate_interp_block / generate_decim_block + +Create blocks with fixed interpolation or decimation ratios. + +```python +# Interpolating block (2x output samples per input) +interp = generate_interp_block( + name="upsample_2x", + description="2x upsampler with zero-stuffing", + inputs=[{"dtype": "float", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + interpolation=2, + work_logic="Zero-stuff between samples" +) + +# Decimating block (4x fewer output samples) +decim = generate_decim_block( + name="downsample_4x", + description="4x downsampler", + inputs=[{"dtype": "float", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + decimation=4, + work_logic="Output every 4th sample" +) +``` + +### validate_block_code + +Static analysis without execution. + +```python +result = validate_block_code(source_code=my_block_code) + +# Returns ValidationResult with: +# - is_valid: bool +# - errors: list[str] (syntax errors, missing imports) +# - warnings: list[str] (style issues, potential bugs) +``` + +### test_block_in_docker + +Test generated blocks in an isolated container. + +```python +result = test_block_in_docker( + source_code=my_block_code, + test_input=[1.0, 2.0, 3.0, 4.0], + expected_output=[2.0, 4.0, 6.0, 8.0], # optional + timeout_seconds=30 +) + +# Returns BlockTestResult with: +# - passed: bool +# - actual_output: list[float] +# - error_message: str (if failed) +# - execution_time_ms: float +``` + +--- + +## Phase 4: OOT Export + +Export generated blocks to distributable OOT modules. + +### generate_oot_skeleton + +Create an empty gr_modtool-compatible module structure. + +```python +result = generate_oot_skeleton( + module_name="mymodule", + output_dir="/path/to/gr-mymodule", + author="Your Name", + description="My custom GNU Radio blocks" +) + +# Creates: +# gr-mymodule/ +# ├── CMakeLists.txt +# ├── python/ +# │ └── mymodule/ +# │ └── __init__.py +# └── grc/ +# └── (empty, for .block.yml files) +``` + +### export_block_to_oot + +Export a generated block to an existing or new OOT module. + +```python +# First generate a block +block = generate_sync_block( + name="pm_demod", + description="Phase modulation demodulator", + inputs=[{"dtype": "complex", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + parameters=[{"name": "sensitivity", "dtype": "float", "default": 1.0}] +) + +# Export to OOT module +result = export_block_to_oot( + generated=block, + module_name="apollo", + output_dir="/path/to/gr-apollo", + author="Ryan Malloy" +) + +# Creates: +# gr-apollo/ +# ├── CMakeLists.txt +# ├── python/apollo/ +# │ ├── __init__.py +# │ └── pm_demod.py ← Block implementation +# └── grc/ +# └── apollo_pm_demod.block.yml ← GRC block definition +``` + +### export_from_flowgraph + +Export an embedded Python block from the current flowgraph. + +```python +# After creating an embedded block with create_embedded_python_block() +result = export_from_flowgraph( + block_name="epy_block_0", + module_name="custom", + output_dir="/path/to/gr-custom", + author="Your Name" +) +``` + +--- + +## Complete Workflow Example + +### Example: Apollo USB PCM Decoder + +This example demonstrates the full workflow for creating a decoder for Apollo mission telemetry. + +```python +# 1. Enable block dev mode +enable_block_dev_mode() + +# 2. Parse the protocol specification +protocol = parse_protocol_spec(""" + Apollo Unified S-Band PCM telemetry: + - BPSK subcarrier at 1.024 MHz + - 51.2 kbps bit rate + - Manchester encoding + - Frame: 128 words × 8 bits @ 50 fps + - Frame sync pattern: 0xEB9000 +""") + +# 3. Generate decoder chain +chain = generate_decoder_chain( + protocol=protocol, + sample_rate=2048000.0 # 2x subcarrier for Nyquist +) + +# 4. Generate custom phase demodulator +pm_demod = generate_sync_block( + name="pm_demod", + description="Apollo PM demodulator for 0.133 rad deviation", + inputs=[{"dtype": "complex", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + parameters=[ + {"name": "deviation", "dtype": "float", "default": 0.133} + ], + work_logic=""" + # Extract instantaneous phase + phase = numpy.angle(input_items[0]) + # Differentiate to get PM signal + output_items[0][:] = numpy.diff(phase, prepend=phase[0]) * self.deviation + """ +) + +# 5. Validate the generated block +validation = validate_block_code(pm_demod.source_code) +if not validation.is_valid: + print(f"Errors: {validation.errors}") + +# 6. Test in Docker +test = test_block_in_docker( + source_code=pm_demod.source_code, + test_input=[1+0j, 0+1j, -1+0j, 0-1j], # 90° phase steps + timeout_seconds=30 +) + +# 7. Export to OOT module +export_block_to_oot( + generated=pm_demod, + module_name="apollo", + output_dir="/home/user/gr-apollo", + author="Ryan Malloy" +) + +# 8. Build and install the module +install_oot_module( + git_url="file:///home/user/gr-apollo", + branch="main" +) +``` + +--- + +## Three-Tier Development Model + +GR-MCP supports three levels of block persistence: + +| Tier | Mechanism | Persistence | Use Case | +|------|-----------|-------------|----------| +| 1 | `create_embedded_python_block()` | In .grc file | Rapid iteration | +| 2 | `validate_block_code()` + flowgraph | Memory only | Session testing | +| 3 | `export_block_to_oot()` | File system | Distribution | + +**Workflow Progression:** + +``` +Tier 1: Rapid Iteration + create_embedded_python_block() → modify → test → iterate + │ + ▼ (satisfied with block) +Tier 2: Validation + validate_block_code() → test_block_in_docker() + │ + ▼ (ready for distribution) +Tier 3: Export + export_block_to_oot() → install_oot_module() +``` + +--- + +## Resources + +Block dev mode provides prompt and template resources: + +```python +# Access via MCP resources +"prompts://block-generation/sync-block" # gr.sync_block patterns +"prompts://block-generation/basic-block" # gr.basic_block patterns +"prompts://protocol-analysis/decoder-chain" # Decoder pipeline guidance +"templates://block/sync-block" # Python code template +"templates://oot/cmake" # CMakeLists.txt template +"templates://oot/block-yaml" # .block.yml template +``` + +--- + +## Tool Reference + +### Protocol Analysis Tools + +| Tool | Description | +|------|-------------| +| `parse_protocol_spec` | Parse natural language protocol spec → ProtocolModel | +| `generate_decoder_chain` | ProtocolModel → complete decoder pipeline | +| `get_missing_oot_modules` | Check which OOT modules are required | + +### Signal Analysis Tools + +| Tool | Description | +|------|-------------| +| `analyze_iq_file` | FFT analysis of IQ recordings | + +### Block Generation Tools + +| Tool | Description | +|------|-------------| +| `generate_sync_block` | Create 1:1 sample processing block | +| `generate_basic_block` | Create variable-ratio block | +| `generate_interp_block` | Create interpolating block | +| `generate_decim_block` | Create decimating block | +| `validate_block_code` | Static code analysis | +| `test_block_in_docker` | Isolated container testing | +| `parse_block_prompt` | Parse natural language → generation params | + +### OOT Export Tools + +| Tool | Description | +|------|-------------| +| `generate_oot_skeleton` | Create empty module structure | +| `export_block_to_oot` | Export generated block to OOT | +| `export_from_flowgraph` | Export embedded block to OOT | + +--- + +## Related Documentation + +- [GRC Runtime Communication](grc-runtime-communication.md) - XML-RPC and ControlPort +- [OOT Catalog](../src/gnuradio_mcp/oot_catalog.py) - Curated OOT module directory diff --git a/docs/grc-runtime-communication.md b/docs/grc-runtime-communication.md new file mode 100644 index 0000000..235da51 --- /dev/null +++ b/docs/grc-runtime-communication.md @@ -0,0 +1,242 @@ +# GRC Runtime Communication with Flowgraph Processes + +This document explains how GNU Radio Companion (GRC) communicates with running flowgraph processes and the two mechanisms available for runtime control. + +## Key Insight: GRC is a Code Generator, Not a Runtime Controller + +GRC runs flowgraphs as **completely separate subprocesses** via `subprocess.Popen()`. It does not have built-in runtime control capabilities. + +``` ++--------------------+ subprocess.Popen() +---------------------+ +| GNU Radio | -----------------------------------> | Generated Python | +| Companion (GRC) | | Flowgraph Script | +| | <----------------------------------- | | +| (Qt/GTK GUI) | stdout/stderr pipe | (gr.top_block) | ++--------------------+ +---------------------+ +``` + +The generated Python script runs independently. To control parameters at runtime, you must use one of the two communication mechanisms described below. + +## GRC Execution Flow + +``` +.grc file (YAML) + | + v Platform.load_and_generate_flow_graph() +Generator (Mako templates) + | + v generator.write() +Python script (with set_*/get_* methods) + | + v ExecFlowGraphThread -> subprocess.Popen() +Running flowgraph process + | + v stdout/stderr piped back to GRC console +``` + +### Key GRC Execution Files + +| File | Purpose | +|------|---------| +| `grc/main.py` | Entry point | +| `grc/gui_qt/components/executor.py` | ExecFlowGraphThread subprocess launcher | +| `grc/core/platform.py` | Block registry, flowgraph loading | +| `grc/core/generator/Generator.py` | Generator factory | +| `grc/workflows/common.py` | Base generator classes | +| `grc/workflows/python_nogui/flow_graph_nogui.py.mako` | Mako template for Python | + +--- + +## Two Runtime Control Mechanisms + +### 1. XML-RPC Server (Simple, HTTP-based) + +A **block-based approach** - add the `xmlrpc_server` block to your flowgraph to expose GRC variables over HTTP. + +| Aspect | Details | +|--------|---------| +| Protocol | HTTP (XML-RPC) | +| Default Port | 8080 | +| Setup | Add `XMLRPC Server` block to flowgraph | +| Naming | `set_varname()` / `get_varname()` | +| Type Support | Basic Python types | + +#### How It Works + +1. Add `XMLRPC Server` block to flowgraph +2. GRC variables automatically become `set_X()` / `get_X()` methods +3. Connect with any XML-RPC client (Python, C++, curl, etc.) + +#### Client Example + +```python +import xmlrpc.client + +# Connect to running flowgraph +server = xmlrpc.client.ServerProxy('http://localhost:8080') + +# Read and write variables +print(server.get_freq()) # Read a variable +server.set_freq(145.5e6) # Set a variable + +# Flowgraph control +server.stop() # Stop flowgraph +server.start() # Start flowgraph +server.lock() # Lock flowgraph for modifications +server.unlock() # Unlock flowgraph +``` + +#### Key Files + +| File | Purpose | +|------|---------| +| `gr-blocks/grc/xmlrpc_server.block.yml` | Server block definition | +| `gr-blocks/grc/xmlrpc_client.block.yml` | Client block definition | +| `gr-blocks/examples/xmlrpc/` | Example flowgraphs | + +--- + +### 2. ControlPort/Thrift (Advanced, Binary) + +A **configuration-based approach** - blocks register their parameters via `setup_rpc()` in C++ code. See `docs/doxygen/other/ctrlport.dox` for detailed block implementation. + +| Aspect | Details | +|--------|---------| +| Protocol | Thrift Binary TCP | +| Default Port | 9090 | +| Setup | Enable in config, blocks call `setup_rpc()` | +| Naming | `block_alias::varname` | +| Type Support | Rich (complex, vectors, PMT types) | +| Metadata | Units, min/max, display hints | + +#### Architecture + +``` ++------------------------------------------------------------------+ +| Running Flowgraph Process | ++-----------------------------------------------------------------+ +| Block A Block B | +| +------------------+ +------------------+ | +| | setup_rpc() { | | setup_rpc() { | | +| | add_rpc_var( | | add_rpc_var( | | +| | "gain", | | "freq", | | +| | &get_gain, | | &get_freq, | | +| | &set_gain); | | &set_freq); | | +| | } | | } | | +| +--------+---------+ +--------+---------+ | +| | | | +| v v | +| +----------------------------------------------------------+ | +| | rpcserver_thrift (port 9090) | | +| | +-----------------+ +-----------------+ | | +| | | setcallbackmap | | getcallbackmap | | | +| | | "blockA::gain" | | "blockA::gain" | | | +| | | "blockB::freq" | | "blockB::freq" | | | +| | +-----------------+ +-----------------+ | | +| +----------------------------------------------------------+ | ++------------------------------------------------------------------+ + ^ + | Thrift Binary Protocol (TCP) + v ++------------------------------------------------------------------+ +| Python Client | +| from gnuradio.ctrlport import GNURadioControlPortClient | +| | +| client = GNURadioControlPortClient(host='localhost', port=9090) | +| knobs = client.getKnobs(['blockA::gain', 'blockB::freq']) | +| client.setKnobs({'blockA::gain': 2.5}) | ++------------------------------------------------------------------+ +``` + +#### Enabling ControlPort + +**~/.gnuradio/config.conf:** +```ini +[ControlPort] +on = True +edges_list = True + +[thrift] +port = 9090 +nthreads = 2 +``` + +#### Client Example + +```python +from gnuradio.ctrlport.GNURadioControlPortClient import GNURadioControlPortClient + +# Connect to running flowgraph +client = GNURadioControlPortClient(host='localhost', port=9090) + +# Get knobs (read values) +knobs = client.getKnobs(['analog_sig_source_0::frequency']) +print(knobs) + +# Set knobs (write values) +client.setKnobs({'analog_sig_source_0::frequency': 1500.0}) + +# Regex-based retrieval - get all frequency knobs +all_freq_knobs = client.getRe(['.*::frequency']) + +# Get metadata (units, min, max, description) +props = client.properties(['analog_sig_source_0::frequency']) +print(props['analog_sig_source_0::frequency'].units) +print(props['analog_sig_source_0::frequency'].min) +``` + +#### GUI Monitoring Tools + +- **gr-ctrlport-monitor** - Real-time variable inspection +- **gr-perf-monitorx** - Performance profiling visualization + +```bash +gr-ctrlport-monitor localhost 9090 +gr-perf-monitorx localhost 9090 +``` + +#### Key Files + +| File | Purpose | +|------|---------| +| `gnuradio-runtime/lib/controlport/thrift/gnuradio.thrift` | Thrift IDL definition | +| `gnuradio-runtime/include/gnuradio/rpcserver_thrift.h` | Server implementation | +| `gnuradio-runtime/include/gnuradio/rpcregisterhelpers.h` | Registration templates | +| `gnuradio-runtime/python/gnuradio/ctrlport/GNURadioControlPortClient.py` | Python client | +| `gnuradio-runtime/python/gnuradio/ctrlport/RPCConnectionThrift.py` | Thrift connection | + +--- + +## Comparison: XML-RPC vs ControlPort + +| Feature | XML-RPC | ControlPort/Thrift | +|---------|---------|-------------------| +| Setup | Add block to flowgraph | Enable in config.conf | +| Protocol | HTTP | Binary TCP | +| Performance | Slower (text-based) | Faster (binary) | +| Type support | Basic Python types | Complex, vectors, PMT | +| Metadata | None | Units, min/max, hints | +| Tooling | Any HTTP client | Specialized monitors | +| Use case | Simple control | Performance monitoring | + +### When to Use Each + +**Use XML-RPC when:** +- You need quick, simple remote control +- Integration with web applications +- Language-agnostic client access +- Minimal configuration + +**Use ControlPort when:** +- You need performance monitoring +- Working with complex data types +- Block-level control granularity +- Need metadata about parameters + +--- + +## Related Documentation + +- `docs/doxygen/other/ctrlport.dox` - Detailed ControlPort block implementation guide +- `gr-blocks/examples/xmlrpc/` - XML-RPC usage examples +- `docs/usage-manual/(exported from wiki) Performance Counters.txt` - Performance monitoring diff --git a/pyproject.toml b/pyproject.toml index 195753a..07aacb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,21 @@ build-backend = "setuptools.build_meta" [project] name = "gnuradio-mcp" version = "0.2.0" -description = "A FastMCP server for gnuradio." +description = "MCP server for GNU Radio — build, validate, run, and export flowgraphs programmatically." +readme = "README.md" +license = "MIT" requires-python = ">=3.14" +authors = [ + {name = "Ryan Malloy", email = "ryan@supported.systems"}, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering", + "Topic :: Communications :: Ham Radio", +] dependencies = [ "pydantic>=2.12", "fastmcp>=3.0.0b1", @@ -25,6 +38,14 @@ dev = [ "pre-commit>=4.5", ] +[project.urls] +Homepage = "https://github.com/rsp2k/gr-mcp" +Repository = "https://github.com/rsp2k/gr-mcp" +Issues = "https://github.com/rsp2k/gr-mcp/issues" + +[project.scripts] +gnuradio-mcp = "gnuradio_mcp.server:main" + [tool.pytest.ini_options] # Tell pytest where to find the package pythonpath = ["src", "."] diff --git a/src/gnuradio_mcp/middlewares/flowgraph.py b/src/gnuradio_mcp/middlewares/flowgraph.py index 5ccd1ba..4be8520 100644 --- a/src/gnuradio_mcp/middlewares/flowgraph.py +++ b/src/gnuradio_mcp/middlewares/flowgraph.py @@ -46,7 +46,8 @@ class FlowGraphMiddleware(ElementMiddleware): ) -> BlockModel: block_name = block_name or get_unique_id(self._flowgraph.blocks, block_type) block = self._flowgraph.new_block(block_type) - assert block is not None, f"Failed to create block: {block_type}" + if block is None: + raise ValueError(f"Failed to create block: {block_type!r} — unknown block type or internal GRC error") set_block_name(block, block_name) return BlockModel.from_block(block) @@ -213,7 +214,8 @@ class FlowGraphMiddleware(ElementMiddleware): """ block_name = block_name or get_unique_id(self._flowgraph.blocks, "epy_block") block = self._flowgraph.new_block("epy_block") - assert block is not None, "Failed to create epy_block" + if block is None: + raise ValueError("Failed to create epy_block — embedded Python block type not available") set_block_name(block, block_name) block.params["_source_code"].set_value(source_code) block.rewrite() @@ -223,12 +225,50 @@ class FlowGraphMiddleware(ElementMiddleware): # Gap 6: Expression Evaluation # ────────────────────────────────────────── + # Patterns that indicate code execution attempts rather than expressions + _BLOCKED_PATTERNS = ( + "__import__", + "exec(", + "eval(", + "compile(", + "open(", + "subprocess", + "os.system", + "os.popen", + "os.exec", + "os.spawn", + "os.remove", + "os.unlink", + "os.rmdir", + "shutil.", + "importlib", + "builtins", + "globals()", + "locals()", + "getattr(", + "setattr(", + "delattr(", + "breakpoint(", + ) + def evaluate_expression(self, expr: str) -> Any: """Evaluate a Python expression in the flowgraph's namespace. The namespace includes all imports, variables, parameters, and - modules defined in the flowgraph. + modules defined in the flowgraph. Intended for arithmetic, variable + lookups, and GRC expressions (e.g. "samp_rate / 2", "2 ** sf"). + + WARNING: This delegates to GRC's built-in evaluator which ultimately + calls Python eval(). A blocklist rejects obviously dangerous patterns, + but this is NOT a sandbox. Do not expose to untrusted inputs. """ + expr_lower = expr.lower().replace(" ", "") + for pattern in self._BLOCKED_PATTERNS: + if pattern.lower().replace(" ", "") in expr_lower: + raise ValueError( + f"Expression rejected: contains blocked pattern {pattern!r}. " + f"evaluate_expression is for arithmetic and variable lookups only." + ) fg = self._flowgraph fg.rewrite() return fg.evaluate(expr) diff --git a/src/gnuradio_mcp/middlewares/protocol_analyzer.py b/src/gnuradio_mcp/middlewares/protocol_analyzer.py index d6fd75a..9a4e118 100644 --- a/src/gnuradio_mcp/middlewares/protocol_analyzer.py +++ b/src/gnuradio_mcp/middlewares/protocol_analyzer.py @@ -92,7 +92,7 @@ class ProtocolAnalyzerMiddleware: def _refresh_available_blocks(self): """Update list of available blocks from platform.""" if self._platform_mw: - for block_type in self._platform_mw.block_types: + for block_type in self._platform_mw.blocks: self._available_blocks.add(block_type.key) # ────────────────────────────────────────── diff --git a/src/gnuradio_mcp/providers/base.py b/src/gnuradio_mcp/providers/base.py index caf0eee..dc4ab6a 100644 --- a/src/gnuradio_mcp/providers/base.py +++ b/src/gnuradio_mcp/providers/base.py @@ -213,7 +213,11 @@ class PlatformProvider: ############################################## def evaluate_expression(self, expr: str) -> Any: - """Evaluate a Python expression in the flowgraph's namespace.""" + """Evaluate a Python expression in the flowgraph's namespace. + + For arithmetic and variable lookups (e.g. "samp_rate / 2", "2 ** sf"). + Dangerous patterns (import, exec, open, os, subprocess) are blocked. + """ return self._flowgraph_mw.evaluate_expression(expr) ############################################## diff --git a/src/gnuradio_mcp/providers/block_dev.py b/src/gnuradio_mcp/providers/block_dev.py index ce61a26..8f156e3 100644 --- a/src/gnuradio_mcp/providers/block_dev.py +++ b/src/gnuradio_mcp/providers/block_dev.py @@ -3,16 +3,21 @@ Orchestrates block generation, validation, testing, and export operations. """ -from __future__ import annotations - import logging from typing import TYPE_CHECKING, Any from gnuradio_mcp.middlewares.block_generator import BlockGeneratorMiddleware +from gnuradio_mcp.middlewares.oot_exporter import OOTExporterMiddleware +from gnuradio_mcp.middlewares.protocol_analyzer import ProtocolAnalyzerMiddleware from gnuradio_mcp.models import ( BlockParameter, BlockTestResult, + DecoderPipelineModel, GeneratedBlockCode, + IQAnalysisResult, + OOTExportResult, + OOTSkeletonResult, + ProtocolModel, SignatureItem, ValidationResult, ) @@ -33,18 +38,23 @@ class BlockDevProvider: def __init__( self, - flowgraph_mw: FlowGraphMiddleware | None = None, - docker_mw: DockerMiddleware | None = None, + flowgraph_mw: "FlowGraphMiddleware | None" = None, + docker_mw: "DockerMiddleware | None" = None, + platform_mw=None, ): """Initialize the block development provider. Args: flowgraph_mw: Flowgraph middleware for block injection docker_mw: Docker middleware for isolated testing + platform_mw: Platform middleware for block availability """ self._flowgraph_mw = flowgraph_mw self._docker_mw = docker_mw + self._platform_mw = platform_mw self._generator = BlockGeneratorMiddleware(flowgraph_mw) + self._protocol_analyzer = ProtocolAnalyzerMiddleware(platform_mw) + self._oot_exporter = OOTExporterMiddleware(flowgraph_mw) # ────────────────────────────────────────── # Block Generation @@ -302,23 +312,26 @@ class BlockDevProvider: source_code: str, test_input: list[float], ) -> str: - """Generate a test flowgraph that exercises the block.""" - # Escape the source code for embedding - escaped_code = source_code.replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + """Generate a test flowgraph that exercises the block. + + Uses base64 encoding to safely embed user-supplied source code, + avoiding string interpolation injection risks. + """ + import base64 + + encoded = base64.b64encode(source_code.encode("utf-8")).decode("ascii") return f'''#!/usr/bin/env python3 """Auto-generated test flowgraph for block testing.""" +import base64 +import json import numpy as np from gnuradio import gr, blocks -import json -import sys -# Embedded block source -BLOCK_CODE = """{escaped_code}""" - -# Execute the block code to define the class -exec(BLOCK_CODE) +# Decode embedded block source (base64-encoded to prevent injection) +_block_code = base64.b64decode("{encoded}").decode("utf-8") +exec(_block_code) class test_flowgraph(gr.top_block): def __init__(self): @@ -357,10 +370,13 @@ if __name__ == "__main__": ) -> BlockTestResult: """Execute the test flowgraph in a Docker container.""" import json + import os import tempfile import time start_time = time.time() + script_path = None + container = None try: # Write flowgraph to temp file @@ -371,12 +387,13 @@ if __name__ == "__main__": script_path = f.name # Run in Docker (use local gnuradio-runtime image) + # remove=False so we can capture logs even on failure container = self._docker_mw._client.containers.run( image="gnuradio-runtime:latest", - command=f"python3 /test/script.py", + command="python3 /test/script.py", volumes={script_path: {"bind": "/test/script.py", "mode": "ro"}}, detach=True, - remove=True, + remove=False, ) # Wait for completion @@ -418,6 +435,19 @@ if __name__ == "__main__": error=str(e), execution_time_ms=(time.time() - start_time) * 1000, ) + finally: + # Clean up temp file + if script_path: + try: + os.unlink(script_path) + except OSError: + pass + # Clean up container + if container: + try: + container.remove(force=True) + except Exception: + pass def _compare_outputs( self, @@ -492,3 +522,271 @@ if __name__ == "__main__": def has_flowgraph(self) -> bool: """Check if flowgraph injection is available.""" return self._flowgraph_mw is not None + + # ────────────────────────────────────────── + # Protocol Analysis + # ────────────────────────────────────────── + + def parse_protocol_spec(self, spec_text: str) -> ProtocolModel: + """Parse a natural language protocol specification. + + Extracts modulation, framing, and encoding parameters from + a text description of a wireless protocol. + + Args: + spec_text: Natural language protocol description, e.g.: + "GFSK modulation at 250 kbaud, ±160 kHz deviation, + with 32-bit preamble 0xAAAAAAAA and sync word 0x7E, + CRC-16 for error detection" + + Returns: + ProtocolModel with extracted parameters including: + - modulation: scheme, symbol_rate, deviation, bandwidth + - framing: preamble, sync_word, crc_type + - encoding: fec_type, fec_rate, whitening + + Example: + protocol = parse_protocol_spec(''' + Apollo USB PCM Telemetry: + - BPSK on 1.024 MHz subcarrier + - 51.2 kbps bit rate + - 32-bit frame sync word + - 128-word frames at 50 fps + ''') + """ + return self._protocol_analyzer.parse_protocol_spec(spec_text) + + def generate_decoder_chain( + self, + protocol: ProtocolModel | dict[str, Any], + sample_rate: float | None = None, + ) -> DecoderPipelineModel: + """Generate a decoder pipeline from a protocol specification. + + Creates a chain of GNU Radio blocks appropriate for decoding + the specified protocol, including filtering, demodulation, + symbol recovery, and packet processing. + + Args: + protocol: Protocol spec from parse_protocol_spec() or dict + sample_rate: Sample rate override (uses protocol spec if None) + + Returns: + DecoderPipelineModel with: + - blocks: List of DecoderBlock with block_type, params + - connections: List of (src, port, dst, port) tuples + - variables: Flowgraph variables to set + - is_complete: True if all blocks are available + - missing_blocks: List of unavailable block types + + Example: + protocol = parse_protocol_spec("GFSK at 250 kbaud...") + pipeline = generate_decoder_chain(protocol, sample_rate=2e6) + # Returns blocks: [tuner, demod, timing, slicer, correlator, ...] + """ + # Convert dict to ProtocolModel if needed + if isinstance(protocol, dict): + protocol = ProtocolModel(**protocol) + + return self._protocol_analyzer.generate_decoder_chain( + protocol=protocol, + sample_rate=sample_rate, + ) + + def analyze_iq_file( + self, + file_path: str, + sample_rate: float | None = None, + fft_size: int = 1024, + threshold_db: float = -40, + ) -> IQAnalysisResult: + """Analyze an IQ capture file for signals and modulation. + + Performs spectral analysis to detect signals and attempts + automatic modulation classification using statistical features. + + Args: + file_path: Path to IQ file (complex64 raw or stereo WAV) + sample_rate: Sample rate if not in file metadata + fft_size: FFT size for spectral analysis (default 1024) + threshold_db: Power threshold for signal detection + + Returns: + IQAnalysisResult with: + - signals_detected: List of SignalDetection (center_freq, bandwidth, power) + - modulation_results: ModulationDetectionResult for each signal + - noise_floor_db: Estimated noise floor + - peak_power_db: Maximum signal power + + Example: + result = analyze_iq_file( + "/tmp/capture.raw", + sample_rate=2e6, + threshold_db=-30 + ) + for signal in result.signals_detected: + print(f"Signal at {signal.center_frequency/1e3:.1f} kHz") + """ + return self._protocol_analyzer.analyze_iq_file( + file_path=file_path, + sample_rate=sample_rate, + fft_size=fft_size, + threshold_db=threshold_db, + ) + + def get_missing_oot_modules( + self, + pipeline: DecoderPipelineModel | dict[str, Any], + ) -> list[str]: + """Identify OOT modules needed for a pipeline. + + Maps missing blocks in a decoder pipeline to the OOT modules + that provide them. Useful for determining what to install. + + Args: + pipeline: DecoderPipelineModel from generate_decoder_chain() + + Returns: + List of OOT module names (e.g., ["gr-lora_sdr", "gr-satellites"]) + """ + if isinstance(pipeline, dict): + pipeline = DecoderPipelineModel(**pipeline) + + return self._protocol_analyzer.get_missing_oot_modules(pipeline) + + # ────────────────────────────────────────── + # OOT Module Export + # ────────────────────────────────────────── + + def generate_oot_skeleton( + self, + module_name: str, + output_dir: str, + author: str = "gr-mcp", + description: str = "", + ) -> OOTSkeletonResult: + """Generate an empty OOT module structure. + + Creates the directory structure and CMake files for a new + GNU Radio OOT module. Blocks can be added later with + export_block_to_oot(). + + Args: + module_name: Module name (e.g., "custom" for gr-custom) + output_dir: Base directory for the module + author: Author name for copyright headers + description: Module description + + Returns: + OOTSkeletonResult with: + - success: True if skeleton was created + - module_name: Sanitized module name + - output_dir: Absolute path to module + - structure: Dict of created directories and files + - next_steps: Instructions for building + + Example: + result = generate_oot_skeleton( + module_name="apollo", + output_dir="/tmp/gr-apollo", + author="Ryan Malloy", + description="Apollo USB signal decoders" + ) + """ + return self._oot_exporter.generate_oot_skeleton( + module_name=module_name, + output_dir=output_dir, + author=author, + description=description, + ) + + def export_block_to_oot( + self, + generated: GeneratedBlockCode | dict[str, Any], + module_name: str, + output_dir: str, + author: str = "gr-mcp", + ) -> OOTExportResult: + """Export a generated block to an OOT module. + + Creates or updates an OOT module with the given block. + If the module doesn't exist, creates the skeleton first. + + Args: + generated: GeneratedBlockCode from generate_*() or dict + module_name: Module name (e.g., "custom") + output_dir: Base directory for the module + author: Author name for copyright headers + + Returns: + OOTExportResult with: + - success: True if export succeeded + - module_name: Final module name + - block_name: Final block name + - files_created: List of created file paths + - build_ready: True if module can be built + + Example: + # Generate a block + block = generate_sync_block( + name="pm_demod", + description="Phase demodulator", + inputs=[{"dtype": "complex", "vlen": 1}], + outputs=[{"dtype": "float", "vlen": 1}], + parameters=[{"name": "sensitivity", "dtype": "float", "default": 1.0}] + ) + + # Export to OOT module + result = export_block_to_oot( + generated=block, + module_name="apollo", + output_dir="/tmp/gr-apollo" + ) + # Creates: python/apollo/pm_demod.py, grc/apollo_pm_demod.block.yml + """ + # Convert dict to GeneratedBlockCode if needed + if isinstance(generated, dict): + generated = GeneratedBlockCode(**generated) + + return self._oot_exporter.export_block_to_oot( + generated=generated, + module_name=module_name, + output_dir=output_dir, + author=author, + ) + + def export_from_flowgraph( + self, + block_name: str, + module_name: str, + output_dir: str, + author: str = "gr-mcp", + ) -> OOTExportResult: + """Export an embedded block from the current flowgraph. + + Extracts the source code from an epy_block in the flowgraph + and exports it to a full OOT module. + + Args: + block_name: Name of the epy_block in the flowgraph + module_name: Target module name + output_dir: Base directory for the module + author: Author name + + Returns: + OOTExportResult with status and file paths. + + Example: + # After creating an epy_block via create_embedded_python_block() + result = export_from_flowgraph( + block_name="my_gain_0", + module_name="custom", + output_dir="/tmp/gr-custom" + ) + """ + return self._oot_exporter.export_from_flowgraph( + block_name=block_name, + module_name=module_name, + output_dir=output_dir, + author=author, + ) diff --git a/src/gnuradio_mcp/providers/mcp_block_dev.py b/src/gnuradio_mcp/providers/mcp_block_dev.py index bb796d7..4a7a2a3 100644 --- a/src/gnuradio_mcp/providers/mcp_block_dev.py +++ b/src/gnuradio_mcp/providers/mcp_block_dev.py @@ -4,8 +4,6 @@ Follows the dynamic registration pattern from McpRuntimeProvider to minimize context usage when block development features aren't needed. """ -from __future__ import annotations - import logging import os from typing import Any, Callable @@ -106,11 +104,20 @@ class McpBlockDevProvider: - validate_block_code: Static code analysis - test_block_in_docker: Isolated testing (if Docker available) - inject_block: Add generated block to flowgraph + - parse_protocol_spec: Extract protocol params from description + - generate_decoder_chain: Generate block pipeline from protocol + - analyze_iq_file: Detect signals and modulation in IQ captures + - get_missing_oot_modules: Identify OOT modules for pipeline + - generate_oot_skeleton: Create empty OOT module structure + - export_block_to_oot: Export generated block to OOT module + - export_from_flowgraph: Export epy_block to OOT module Use this when you need to: - Generate custom signal processing blocks - Create protocol-specific decoders - Build and test new DSP algorithms + - Analyze captured signals and auto-generate decoders + - Export blocks to distributable OOT modules """ if self._block_dev_enabled: return BlockDevModeStatus( @@ -190,6 +197,20 @@ class McpBlockDevProvider: if p.has_flowgraph: self._add_tool("inject_generated_block", p.inject_block) + # Protocol analysis tools (Phase 3) + self._add_tool("parse_protocol_spec", p.parse_protocol_spec) + self._add_tool("generate_decoder_chain", p.generate_decoder_chain) + self._add_tool("get_missing_oot_modules", p.get_missing_oot_modules) + + # Signal analysis tools (Phase 4) + self._add_tool("analyze_iq_file", p.analyze_iq_file) + + # OOT export tools (Phase 5) + self._add_tool("generate_oot_skeleton", p.generate_oot_skeleton) + self._add_tool("export_block_to_oot", p.export_block_to_oot) + if p.has_flowgraph: + self._add_tool("export_from_flowgraph", p.export_from_flowgraph) + def _unregister_block_dev_tools(self): """Remove all dynamically registered block dev tools.""" for name in list(self._block_dev_tools.keys()): @@ -503,6 +524,7 @@ class McpBlockDevProvider: cls, mcp_instance: FastMCP, flowgraph_mw=None, + platform_mw=None, auto_enable: bool = False, ) -> McpBlockDevProvider: """Factory: create provider with optional Docker support. @@ -510,6 +532,7 @@ class McpBlockDevProvider: Args: mcp_instance: FastMCP app instance flowgraph_mw: Optional FlowGraphMiddleware for block injection + platform_mw: Optional PlatformMiddleware for block availability auto_enable: Register block dev tools at startup Returns: @@ -519,5 +542,6 @@ class McpBlockDevProvider: provider = BlockDevProvider( flowgraph_mw=flowgraph_mw, docker_mw=docker_mw, + platform_mw=platform_mw, ) return cls(mcp_instance, provider, auto_enable=auto_enable) diff --git a/src/gnuradio_mcp/providers/mcp_runtime.py b/src/gnuradio_mcp/providers/mcp_runtime.py index d72c8f1..9e32d1f 100644 --- a/src/gnuradio_mcp/providers/mcp_runtime.py +++ b/src/gnuradio_mcp/providers/mcp_runtime.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import logging from typing import Any, Callable diff --git a/main.py b/src/gnuradio_mcp/server.py similarity index 61% rename from main.py rename to src/gnuradio_mcp/server.py index 5454f9d..81a8bdb 100644 --- a/main.py +++ b/src/gnuradio_mcp/server.py @@ -1,4 +1,9 @@ -from __future__ import annotations +"""GR-MCP server entry point. + +NOTE: This server uses module-level global state (platform, flowgraph). +It is designed for SINGLE-SESSION use only. Concurrent MCP clients sharing +the same server instance will see (and mutate) the same flowgraph state. +""" import logging import os @@ -39,13 +44,29 @@ for path in oot_candidates: try: result = pmw.add_block_path(path) if result.blocks_added > 0: - logger.info(f"OOT: +{result.blocks_added} blocks from {path}") - except Exception: - pass + logger.info("OOT: +%d blocks from %s", result.blocks_added, path) + except Exception as e: + logger.warning("Failed to load OOT from %s: %s", path, e) McpPlatformProvider.from_platform_middleware(app, pmw) McpRuntimeProvider.create(app) -McpBlockDevProvider.create(app, auto_enable=True) # Tools always available +McpBlockDevProvider.create(app, platform_mw=pmw, auto_enable=True) + + +def main(): + """Entry point for gnuradio-mcp server.""" + import sys + + try: + from importlib.metadata import version + + package_version = version("gnuradio-mcp") + except Exception: + package_version = "dev" + + print(f"gnuradio-mcp v{package_version}", file=sys.stderr) + app.run() + if __name__ == "__main__": - app.run() + main() diff --git a/tests/integration/test_mcp_block_dev.py b/tests/integration/test_mcp_block_dev.py index a7b29ee..a21c669 100644 --- a/tests/integration/test_mcp_block_dev.py +++ b/tests/integration/test_mcp_block_dev.py @@ -270,3 +270,306 @@ class TestToolNotAvailableWhenDisabled: assert "get_block_dev_mode" in tool_names assert "enable_block_dev_mode" in tool_names assert "disable_block_dev_mode" in tool_names + + +class TestProtocolAnalysisTools: + """Tests for protocol analysis and signal detection tools.""" + + @pytest.mark.asyncio + async def test_parse_protocol_spec_gfsk(self, mcp_app): + """Parse a GFSK protocol specification.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + # Note: Parser expects "Xk baud" and "deviation: Xkhz" format + result = await client.call_tool( + name="parse_protocol_spec", + arguments={ + "spec_text": "GFSK signal at 250k baud, deviation: 160khz" + }, + ) + + assert result.data.modulation.scheme == "GFSK" + assert result.data.modulation.symbol_rate == 250000.0 + assert result.data.modulation.deviation == 160000.0 + + @pytest.mark.asyncio + async def test_parse_protocol_spec_lora(self, mcp_app): + """Parse a LoRa/CSS protocol specification.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + result = await client.call_tool( + name="parse_protocol_spec", + arguments={ + "spec_text": """ + Protocol: LoRa + Modulation: CSS (Chirp Spread Spectrum) + Bandwidth: 125 kHz + Preamble: 8 upchirps + Sync word: 0x34 + """ + }, + ) + + assert result.data.name == "LoRa" + assert result.data.modulation.scheme == "CSS" + assert result.data.modulation.bandwidth == 125000.0 + assert result.data.framing is not None + assert result.data.framing.sync_word == "0x34" + assert result.data.framing.preamble_length == 8 + + @pytest.mark.asyncio + async def test_parse_protocol_spec_with_fec(self, mcp_app): + """Parse protocol with FEC encoding.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + result = await client.call_tool( + name="parse_protocol_spec", + arguments={ + "spec_text": """ + FSK at 9600 baud + Hamming FEC with rate 3/4 + Data whitening enabled + """ + }, + ) + + assert result.data.modulation.scheme == "FSK" + assert result.data.encoding is not None + assert result.data.encoding.fec_type == "hamming" + assert result.data.encoding.fec_rate == "3/4" + assert result.data.encoding.whitening is True + + @pytest.mark.asyncio + async def test_generate_decoder_chain_gfsk(self, mcp_app): + """Generate decoder chain for GFSK signal.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + # First parse a protocol (using parser's expected format) + parse_result = await client.call_tool( + name="parse_protocol_spec", + arguments={ + "spec_text": "GFSK at 50k baud, deviation: 25khz" + }, + ) + + # Generate decoder chain from parsed protocol + # Note: Use structured_content (already a dict) for passing to next tool + result = await client.call_tool( + name="generate_decoder_chain", + arguments={ + "protocol": parse_result.structured_content, + "sample_rate": 2000000.0, + }, + ) + + # Should have demodulation blocks + block_types = [b.block_type for b in result.data.blocks] + assert "analog_quadrature_demod_cf" in block_types + assert "digital_symbol_sync_ff" in block_types + assert "digital_binary_slicer_fb" in block_types + + # Should have connections + assert len(result.data.connections) >= 2 + + # Should have sample rate variable + # Note: Access via structured_content since data wraps nested objects + assert result.structured_content["variables"]["samp_rate"] == 2000000.0 + + @pytest.mark.asyncio + async def test_generate_decoder_chain_with_framing(self, mcp_app): + """Generate decoder with sync word correlation.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + # Use parser's expected format for baud rate + parse_result = await client.call_tool( + name="parse_protocol_spec", + arguments={ + "spec_text": """ + FSK at 9.6k baud + Sync word: 0x2DD4 + Preamble: 10101010 pattern + """ + }, + ) + + result = await client.call_tool( + name="generate_decoder_chain", + arguments={"protocol": parse_result.structured_content}, + ) + + # Should have correlator for sync word + block_types = [b.block_type for b in result.data.blocks] + assert "digital_correlate_access_code_tag_bb" in block_types + + @pytest.mark.asyncio + async def test_get_missing_oot_modules_lora(self, mcp_app): + """Identify missing OOT modules for LoRa decoder.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + # Parse LoRa protocol (requires gr-lora_sdr) + parse_result = await client.call_tool( + name="parse_protocol_spec", + arguments={ + "spec_text": """ + Protocol: LoRa + CSS modulation + Bandwidth: 125 kHz + """ + }, + ) + + pipeline_result = await client.call_tool( + name="generate_decoder_chain", + arguments={"protocol": parse_result.structured_content}, + ) + + # Check for missing OOT modules + result = await client.call_tool( + name="get_missing_oot_modules", + arguments={"pipeline": pipeline_result.structured_content}, + ) + + # LoRa blocks require gr-lora_sdr OOT module + # The pipeline should indicate lora_sdr_demod is missing + # which maps to gr-lora_sdr module + # (Only if not installed - test checks the mapping works) + assert isinstance(result.data, list) + + @pytest.mark.asyncio + async def test_protocol_analysis_tools_registered(self, mcp_app): + """Verify protocol analysis tools are registered when enabled.""" + async with Client(mcp_app) as client: + result = await client.call_tool(name="enable_block_dev_mode") + + tool_names = result.data.tools_registered + + # Protocol analysis tools (Phase 3) + assert "parse_protocol_spec" in tool_names + assert "generate_decoder_chain" in tool_names + assert "get_missing_oot_modules" in tool_names + + # Signal analysis tools (Phase 4) + assert "analyze_iq_file" in tool_names + + # OOT export tools (Phase 5) + assert "generate_oot_skeleton" in tool_names + assert "export_block_to_oot" in tool_names + + +class TestOOTExportTools: + """Tests for OOT module export workflow.""" + + @pytest.mark.asyncio + async def test_generate_oot_skeleton(self, mcp_app, tmp_path): + """Generate an empty OOT module skeleton.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + output_dir = str(tmp_path / "gr-test") + + result = await client.call_tool( + name="generate_oot_skeleton", + arguments={ + "module_name": "test", + "output_dir": output_dir, + "author": "Test Author", + "description": "Test module", + }, + ) + + assert result.data.success is True + assert result.data.module_name == "test" + + # Check files were created + assert (tmp_path / "gr-test" / "CMakeLists.txt").exists() + assert (tmp_path / "gr-test" / "python" / "test" / "__init__.py").exists() + assert (tmp_path / "gr-test" / "grc" / "CMakeLists.txt").exists() + + @pytest.mark.asyncio + async def test_export_block_to_oot(self, mcp_app, tmp_path): + """Export a generated block to OOT module.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + # First generate a block + block_result = await client.call_tool( + name="generate_sync_block", + arguments={ + "name": "my_gain", + "description": "Multiply by gain", + "inputs": [{"dtype": "float", "vlen": 1}], + "outputs": [{"dtype": "float", "vlen": 1}], + "parameters": [{"name": "gain", "dtype": "float", "default": 1.0}], + "work_template": "gain", + }, + ) + + # Export to OOT + output_dir = str(tmp_path / "gr-custom") + + result = await client.call_tool( + name="export_block_to_oot", + arguments={ + "generated": block_result.structured_content, + "module_name": "custom", + "output_dir": output_dir, + "author": "Test Author", + }, + ) + + assert result.data.success is True + assert result.data.module_name == "custom" + assert result.data.block_name == "my_gain" + + # Check block files exist + assert (tmp_path / "gr-custom" / "python" / "custom" / "my_gain.py").exists() + assert (tmp_path / "gr-custom" / "grc" / "custom_my_gain.block.yml").exists() + + @pytest.mark.asyncio + async def test_export_full_workflow(self, mcp_app, tmp_path): + """Full workflow: parse protocol → generate chain → export blocks.""" + async with Client(mcp_app) as client: + await client.call_tool(name="enable_block_dev_mode") + + # Generate a custom block for the protocol + block_result = await client.call_tool( + name="generate_sync_block", + arguments={ + "name": "pm_demod", + "description": "Phase demodulator for Apollo USB", + "inputs": [{"dtype": "complex", "vlen": 1}], + "outputs": [{"dtype": "float", "vlen": 1}], + "parameters": [{"name": "sensitivity", "dtype": "float", "default": 1.0}], + "work_logic": "output_items[0][:] = numpy.angle(input_items[0]) * self.sensitivity", + }, + ) + + assert block_result.data.is_valid is True + + # Export to OOT module + output_dir = str(tmp_path / "gr-apollo") + + result = await client.call_tool( + name="export_block_to_oot", + arguments={ + "generated": block_result.structured_content, + "module_name": "apollo", + "output_dir": output_dir, + }, + ) + + assert result.data.success is True + assert result.data.build_ready is True + + # Verify the exported Python source contains our work logic + block_py = tmp_path / "gr-apollo" / "python" / "apollo" / "pm_demod.py" + assert block_py.exists() + content = block_py.read_text() + assert "numpy.angle" in content + assert "sensitivity" in content diff --git a/tests/integration/test_server.py b/tests/integration/test_server.py index 09ebf0d..337ccf2 100644 --- a/tests/integration/test_server.py +++ b/tests/integration/test_server.py @@ -1,7 +1,7 @@ import pytest from fastmcp import Client -from main import app as mcp_app +from gnuradio_mcp.server import app as mcp_app @pytest.fixture