diff --git a/.gitignore b/.gitignore index 4833c540a6..a7b8ae01e0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ openc3/spec/install/outputs openc3/spec/examples.txt coverage/ profile/ +plugins/DEFAULT/ # local env files .env.local @@ -48,4 +49,4 @@ yarn-error.log* # Ignore node modules node_modules -typescript \ No newline at end of file +typescript diff --git a/openc3-init/plugins/packages/openc3-accessor-test/.gitignore b/openc3-init/plugins/packages/openc3-accessor-test/.gitignore new file mode 100644 index 0000000000..801b849788 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/.gitignore @@ -0,0 +1,23 @@ +tools/* +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/openc3-init/plugins/packages/openc3-accessor-test/LICENSE.txt b/openc3-init/plugins/packages/openc3-accessor-test/LICENSE.txt new file mode 100644 index 0000000000..8ba2333102 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/LICENSE.txt @@ -0,0 +1,727 @@ +The OpenC3 software program is a derived work based on the +Ball Aerospace COSMOS software licensed under the +GNU Affero General Public License, version 3 with the Addendums listed below. + +Please note that the verbiage of the AGPL below are copyrighted +by the Free Software Foundation, but the instance of code to which the +licenses refer (the OpenC3 software) is copyrighted by +OpenC3 Inc. and/or Ball Aerospace and Technologies Corp as marked in each file. + +Also please note that the only valid versions of the AGPL +that pertain to OpenC3 are the particular versions of +the license reproduced below (i.e., versions 3, rather than any other +versions of the AGPL), unless explicitly otherwise stated. + + OpenC3 AGPL, version 3 LICENSE ADDENDUMS + OpenC3 Version 5, 16 July 2022 + +The following addendums are made under section 7 of the GNU Affero General +Public License, version 3. + +1. The OpenC3, Inc. copyright and legal notices + on all user interfaces and source files must be preserved or + duplicated on any derivative works. +2. Modified applications and source files must be clearly marked as + "modified" with the modification author and company indicated + immediately following the copyright and legal notices. +3. OpenC3 is trademark of OpenC3, Inc. and may be used for publicity purposes + only in a manner that constitutes "fair use" under applicable trademark + law, except with the express, written permission of OpenC3 Inc. + +----------------------------------------------------------------------------- + +Original Ball Aerospace COSMOS 5 License Text: + +The Ball Aerospace COSMOS software program is licensed under the +GNU Affero General Public License, version 3 with the Addendums listed below. + +Please note that the verbiage of the AGPL below are copyrighted +by the Free Software Foundation, but the instance of code to which the +licenses refer (the Ball Aerospace COSMOS software) is copyrighted by +Ball Aerospace & Technologies Corp. + +Also please note that the only valid versions of the AGPL +that pertain to Ball Aerospace COSMOS are the particular versions of +the license reproduced below (i.e., versions 3, rather than any other +versions of the AGPL), unless explicitly otherwise stated. + + BALL AGPL, version 3 LICENSE ADDENDUMS + Ball Aerospace COSMOS Version 5, 8 January 2020 + +The following addendums are made under section 7 of the GNU Affero General +Public License, version 3. + +1. The Ball Aerospace & Technologies Corp. copyright and legal notices + on all user interfaces and source files must be preserved or + duplicated on any derivative works. +2. Modified applications and source files must be clearly marked as + "modified" with the modification author and company indicated + immediately following the copyright and legal notices. +3. Ball Aerospace COSMOS, Ball, and Ball Aerospace are trademarks of + Ball Corporation and may be used for publicity purposes only in a + manner that constitutes "fair use" under applicable trademark law, + except with the express, written permission of Ball Corporation. + +The AGPL license text follows: + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/openc3-init/plugins/packages/openc3-accessor-test/README.md b/openc3-init/plugins/packages/openc3-accessor-test/README.md new file mode 100644 index 0000000000..c408a0ae6a --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/README.md @@ -0,0 +1,28 @@ +## OpenC3 Accessor Test Plugin + +[Documentation](https://openc3.com) + +This plugin is used to provide examples of using different accessors + +## Getting Started + +1. At the OpenC3 Admin - Plugins, upload the openc3-accessor-test.gem file + +## Contributing + +We encourage you to contribute to OpenC3! + +Contributing is easy. + +1. Fork the project +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +Before any contributions can be incorporated we do require all contributors to agree to a Contributor License Agreement + +This protects both you and us and you retain full rights to any code you write. + +## License + +This OpenC3 plugin is released under the AGPLv3.0 with a few addendums. See [LICENSE.txt](LICENSE.txt) diff --git a/openc3-init/plugins/packages/openc3-accessor-test/Rakefile b/openc3-init/plugins/packages/openc3-accessor-test/Rakefile new file mode 100644 index 0000000000..c1d11ce349 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/Rakefile @@ -0,0 +1,36 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +PLUGIN_NAME = Dir['*.gemspec'][0].split('.')[0..-2].join('.') + +task :require_version do + if ENV['VERSION'] + if ENV['VERSION'] =~ /-/ + # Add Timestamp to prerelease versions + ENV['VERSION'] = ENV['VERSION'] + "." + Time.now.utc.strftime("%Y%m%d%H%M%S") + end + else + puts "VERSION is required: rake VERSION=X.X.X" + exit 1 + end +end + +task :build => [:require_version] do + _, platform, *_ = RUBY_PLATFORM.split("-") + if platform == 'mswin32' or platform == 'mingw32' + puts "Warning: Building gem on Windows will lose file permissions" + end + system("gem build #{PLUGIN_NAME}") +end diff --git a/openc3-init/plugins/packages/openc3-accessor-test/openc3-accessor-test.gemspec b/openc3-init/plugins/packages/openc3-accessor-test/openc3-accessor-test.gemspec new file mode 100644 index 0000000000..7a128a5a2a --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/openc3-accessor-test.gemspec @@ -0,0 +1,37 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +spec = Gem::Specification.new do |s| + s.name = 'openc3-accessor-test' + s.summary = 'OpenC3 Accessor Example Plugin' + s.description = <<-EOF + This plugin is used to provide examples of using different accessors + EOF + s.authors = ['Ryan Melton', 'Jason Thomas'] + s.email = ['ryan@openc3.com', 'jason@openc3.com'] + s.homepage = 'https://github.com/OpenC3/openc3' + + s.platform = Gem::Platform::RUBY + + if ENV['VERSION'] + s.version = ENV['VERSION'].dup + else + time = Time.now.strftime("%Y%m%d%H%M%S") + s.version = '0.0.0' + ".#{time}" + end + s.licenses = ['AGPL-3.0-only', 'Nonstandard'] + + s.files = Dir.glob("{targets,lib,procedures,tools,microservices}/**/*") + %w(Rakefile LICENSE.txt README.md plugin.txt) +end diff --git a/openc3-init/plugins/packages/openc3-accessor-test/plugin.txt b/openc3-init/plugins/packages/openc3-accessor-test/plugin.txt new file mode 100644 index 0000000000..2d6a4ff230 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/plugin.txt @@ -0,0 +1,10 @@ +VARIABLE access_target_name ACCESS +VARIABLE log_retain_time 172800 +VARIABLE reduced_log_retain_time 2592000 + +TARGET ACCESS <%= access_target_name %> + LOG_RETAIN_TIME <%= log_retain_time %> + REDUCED_LOG_RETAIN_TIME <%= reduced_log_retain_time %> + +INTERFACE <%= access_target_name %> simulated_target_interface.rb sim_access.rb + MAP_TARGET <%= access_target_name %> diff --git a/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/_cbor_template.bin b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/_cbor_template.bin new file mode 100644 index 0000000000..2ff3cd7e10 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/_cbor_template.bin @@ -0,0 +1 @@ +£gid_itemeitem1edmore¤eitem2 eitem3û@ ¸Që…eitem4gExampleeitem5„ \ No newline at end of file diff --git a/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/_make_cbor.rb b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/_make_cbor.rb new file mode 100644 index 0000000000..e048abd6d3 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/_make_cbor.rb @@ -0,0 +1,5 @@ +require 'cbor' +data = {"id_item":2, "item1":101, "more": { "item2":12, "item3":3.14, "item4":"Example", "item5":[4, 3, 2, 1] } } +File.open("_cbor_template.bin", 'wb') do |file| + file.write(data.to_cbor) +end diff --git a/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/access_cmds.txt b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/access_cmds.txt new file mode 100644 index 0000000000..8acfac569b --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/access_cmds.txt @@ -0,0 +1,67 @@ +COMMAND <%= target_name %> JSONCMD BIG_ENDIAN "JSON Accessor Command" + ACCESSOR JsonAccessor + TEMPLATE '{"id_item":1, "item1":101, "more": { "item2":12, "item3":3.14, "item4":"Example", "item5":[4, 3, 2, 1] } }' + APPEND_ID_PARAMETER ID_ITEM 32 INT 1 1 1 "Int Item" + KEY $.id_item + APPEND_PARAMETER ITEM1 16 UINT MIN MAX 101 "Int Item 2" + KEY $.item1 + UNITS CELCIUS C + APPEND_PARAMETER ITEM2 16 UINT MIN MAX 12 "Int Item 3" + KEY $.more.item2 + FORMAT_STRING "0x%X" + APPEND_PARAMETER ITEM3 64 FLOAT MIN MAX 3.14 "Float Item" + KEY $.more.item3 + APPEND_PARAMETER ITEM4 128 STRING "Example" "String Item" + KEY $.more.item4 + APPEND_ARRAY_PARAMETER ITEM5 8 UINT 0 "Array Item" + KEY $.more.item5 + +COMMAND <%= target_name %> CBORCMD BIG_ENDIAN "CBOR Accessor Command" + ACCESSOR CborAccessor + TEMPLATE_FILE _cbor_template.bin + APPEND_ID_PARAMETER ID_ITEM 32 INT 2 2 2 "Int Item" + KEY $.id_item + APPEND_PARAMETER ITEM1 16 UINT MIN MAX 101 "Int Item 2" + KEY $.item1 + UNITS CELCIUS C + APPEND_PARAMETER ITEM2 16 UINT MIN MAX 12 "Int Item 3" + KEY $.more.item2 + FORMAT_STRING "0x%X" + APPEND_PARAMETER ITEM3 64 FLOAT MIN MAX 3.14 "Float Item" + KEY $.more.item3 + APPEND_PARAMETER ITEM4 128 STRING "Example" "String Item" + KEY $.more.item4 + APPEND_ARRAY_PARAMETER ITEM5 8 UINT 0 "Array Item" + KEY $.more.item5 + +COMMAND <%= target_name %> XMLCMD BIG_ENDIAN "XML Accessor Command" + ACCESSOR XmlAccessor + TEMPLATE '
  • 3.14
  • Example
' + APPEND_ID_PARAMETER ID_ITEM 32 INT 3 3 3 "Int Item" + KEY "/html/head/script/@src" + APPEND_PARAMETER ITEM1 16 UINT MIN MAX 101 "Int Item 2" + KEY "/html/head/noscript/text()" + UNITS CELCIUS C + APPEND_PARAMETER ITEM2 16 UINT MIN MAX 12 "Int Item 3" + KEY "/html/body/img/@src" + FORMAT_STRING "0x%X" + APPEND_PARAMETER ITEM3 64 FLOAT MIN MAX 3.14 "Float Item" + KEY "/html/body/div/ul/li[1]/text()" + APPEND_PARAMETER ITEM4 128 STRING "Example" "String Item" + KEY "/html/body/div/ul/li[2]/text()" + +COMMAND <%= target_name %> HTMLCMD BIG_ENDIAN "HTML Accessor Command" + ACCESSOR HtmlAccessor + TEMPLATE '4An Image

Example

  • 1
  • 3.14
' + APPEND_ID_PARAMETER ID_ITEM 32 INT 4 4 4 "Int Item" + KEY "/html/head/title/text()" + APPEND_PARAMETER ITEM1 16 UINT MIN MAX 101 "Int Item 2" + KEY "/html/head/script/@src" + UNITS CELCIUS C + APPEND_PARAMETER ITEM2 16 UINT MIN MAX 12 "Int Item 3" + KEY "/html/body/noscript/text()" + FORMAT_STRING "0x%X" + APPEND_PARAMETER ITEM3 64 FLOAT MIN MAX 3.14 "Float Item" + KEY "/html/body/img/@src" + APPEND_PARAMETER ITEM4 128 STRING "Example" "String Item" + KEY "/html/body/p/text()" diff --git a/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/access_tlm.txt b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/access_tlm.txt new file mode 100644 index 0000000000..b455657a7c --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/cmd_tlm/access_tlm.txt @@ -0,0 +1,83 @@ +TELEMETRY <%= target_name %> JSONTLM BIG_ENDIAN "JSON Accessor Telemetry" + ACCESSOR JsonAccessor + # Template is not required for telemetry, but is useful for simulation + TEMPLATE '{"id_item":1, "item1":101, "more": { "item2":12, "item3":3.14, "item4":"Example", "item5":[4, 3, 2, 1] } }' + APPEND_ID_ITEM ID_ITEM 32 INT 1 "Int Item" + KEY $.id_item + APPEND_ITEM ITEM1 16 UINT "Int Item 2" + KEY $.item1 + GENERIC_READ_CONVERSION_START UINT 16 + value * 2 + GENERIC_READ_CONVERSION_END + UNITS CELCIUS C + APPEND_ITEM ITEM2 16 UINT "Int Item 3" + KEY $.more.item2 + FORMAT_STRING "0x%X" + APPEND_ITEM ITEM3 64 FLOAT "Float Item" + KEY $.more.item3 + APPEND_ITEM ITEM4 128 STRING "String Item" + KEY $.more.item4 + APPEND_ARRAY_ITEM ITEM5 8 UINT 0 "Array Item" + KEY $.more.item5 + +TELEMETRY <%= target_name %> CBORTLM BIG_ENDIAN "CBOR Accessor Telemetry" + ACCESSOR CborAccessor + # Template is not required for telemetry, but is useful for simulation + TEMPLATE_FILE _cbor_template.bin + APPEND_ID_ITEM ID_ITEM 32 INT 2 "Int Item" + KEY $.id_item + APPEND_ITEM ITEM1 16 UINT "Int Item 2" + KEY $.item1 + GENERIC_READ_CONVERSION_START UINT 16 + value * 2 + GENERIC_READ_CONVERSION_END + UNITS CELCIUS C + APPEND_ITEM ITEM2 16 UINT "Int Item 3" + KEY $.more.item2 + FORMAT_STRING "0x%X" + APPEND_ITEM ITEM3 64 FLOAT "Float Item" + KEY $.more.item3 + APPEND_ITEM ITEM4 128 STRING "String Item" + KEY $.more.item4 + APPEND_ARRAY_ITEM ITEM5 8 UINT 0 "Array Item" + KEY $.more.item5 + +TELEMETRY <%= target_name %> XMLTLM BIG_ENDIAN "XML Accessor Telemetry" + ACCESSOR XmlAccessor + # Template is not required for telemetry, but is useful for simulation + TEMPLATE '
  • 3.14
  • Example
' + APPEND_ID_ITEM ID_ITEM 32 INT 3 "Int Item" + KEY "/html/head/script/@src" + APPEND_ITEM ITEM1 16 UINT "Int Item 2" + KEY "/html/head/noscript/text()" + GENERIC_READ_CONVERSION_START UINT 16 + value * 2 + GENERIC_READ_CONVERSION_END + UNITS CELCIUS C + APPEND_ITEM ITEM2 16 UINT "Int Item 3" + KEY "/html/body/img/@src" + FORMAT_STRING "0x%X" + APPEND_ITEM ITEM3 64 FLOAT "Float Item" + KEY "/html/body/div/ul/li[1]/text()" + APPEND_ITEM ITEM4 128 STRING "String Item" + KEY "/html/body/div/ul/li[2]/text()" + +TELEMETRY <%= target_name %> HTMLTLM BIG_ENDIAN "HTML Accessor Telemetry" + ACCESSOR HtmlAccessor + # Template is not required for telemetry, but is useful for simulation + TEMPLATE '4An Image

Example

  • 1
  • 3.14
' + APPEND_ID_ITEM ID_ITEM 32 INT 4 "Int Item" + KEY "/html/head/title/text()" + APPEND_ITEM ITEM1 16 UINT "Int Item 2" + KEY "/html/head/script/@src" + GENERIC_READ_CONVERSION_START UINT 16 + value * 2 + GENERIC_READ_CONVERSION_END + UNITS CELCIUS C + APPEND_ITEM ITEM2 16 UINT "Int Item 3" + KEY "/html/body/noscript/text()" + FORMAT_STRING "0x%X" + APPEND_ITEM ITEM3 64 FLOAT "Float Item" + KEY "/html/body/img/@src" + APPEND_ITEM ITEM4 128 STRING "String Item" + KEY "/html/body/p/text()" diff --git a/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/lib/sim_access.rb b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/lib/sim_access.rb new file mode 100644 index 0000000000..7e7536b9be --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/lib/sim_access.rb @@ -0,0 +1,57 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# Provides a demonstration of accessors + +require 'openc3' + +module OpenC3 + class SimAccess < SimulatedTarget + def set_rates + set_rate('JSONTLM', 100) + set_rate('CBORTLM', 100) + set_rate('XMLTLM', 100) + set_rate('HTMLTLM', 100) + end + + def tick_period_seconds + return 1 # Override this method to optimize + end + + def tick_increment + return 100 # Override this method to optimize + end + + def write(packet) + name = packet.packet_name.upcase + + json_packet = @tlm_packets['JSONTLM'] + cbor_packet = @tlm_packets['CBORTLM'] + xml_packet = @tlm_packets['XMLTLM'] + html_packet = @tlm_packets['HTMLTLM'] + + case name + when 'JSONCMD' + json_packet.buffer = packet.buffer + when 'CBORCMD' + cbor_packet.buffer = packet.buffer + when 'XMLCMD' + xml_packet.buffer = packet.buffer + when 'HTMLCMD' + html_packet.buffer = packet.buffer + end + end + end +end diff --git a/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/target.txt b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/target.txt new file mode 100644 index 0000000000..c9714396c5 --- /dev/null +++ b/openc3-init/plugins/packages/openc3-accessor-test/targets/ACCESS/target.txt @@ -0,0 +1,4 @@ +IGNORE_PARAMETER ID_ITEM + +CMD_UNIQUE_ID_MODE +TLM_UNIQUE_ID_MODE \ No newline at end of file diff --git a/openc3-init/plugins/packages/openc3-tool-cmdtlmserver/src/tools/CmdTlmServer/RawDialog.vue b/openc3-init/plugins/packages/openc3-tool-cmdtlmserver/src/tools/CmdTlmServer/RawDialog.vue index d6cc4d5b04..7fa6de945e 100644 --- a/openc3-init/plugins/packages/openc3-tool-cmdtlmserver/src/tools/CmdTlmServer/RawDialog.vue +++ b/openc3-init/plugins/packages/openc3-tool-cmdtlmserver/src/tools/CmdTlmServer/RawDialog.vue @@ -173,24 +173,38 @@ export default { this.api .get_tlm_buffer(this.targetName, this.packetName) .then((result) => { + let buffer_data = result.buffer + if (buffer_data.raw !== undefined) { + buffer_data = buffer_data.raw + } else { + let utf8Encode = new TextEncoder() + buffer_data = utf8Encode.encode(buffer_data) + } this.receivedTime = new Date(result.time / 1000000) this.receivedCount = result.received_count this.rawData = 'Address Data Ascii\n' + '---------------------------------------------------------------------------\n' + - this.formatBuffer(result.buffer.raw) + this.formatBuffer(buffer_data) }) }, updateCommand: function () { this.api .get_cmd_buffer(this.targetName, this.packetName) .then((result) => { + let buffer_data = result.buffer + if (buffer_data.raw !== undefined) { + buffer_data = buffer_data.raw + } else { + let utf8Encode = new TextEncoder() + buffer_data = utf8Encode.encode(buffer_data) + } this.receivedTime = new Date(result.time / 1000000) this.receivedCount = result.received_count this.rawData = 'Address Data Ascii\n' + '---------------------------------------------------------------------------\n' + - this.formatBuffer(result.buffer.raw) + this.formatBuffer(buffer_data) }) }, // TODO: Perhaps move this to a utility library diff --git a/openc3/data/config/command_modifiers.yaml b/openc3/data/config/command_modifiers.yaml index 8713828224..22d908f3e5 100644 --- a/openc3/data/config/command_modifiers.yaml +++ b/openc3/data/config/command_modifiers.yaml @@ -158,3 +158,33 @@ HAZARDOUS: required: false description: Description for why the command is hazardous which must be enclosed with quotes values: "['\"].*['\"]" +ACCESSOR: + summary: Defines the class used to read and write raw values from the packet + description: Defines the class that is used too read raw values from the packet. Defaults to BinaryAccessor. + Provided accessors also include JsonAccessor, CborAccessor, HtmlAccessor, and XmlAccessor. + parameters: + - name: Accessor Class Name + required: true + description: The name of the accessor class + values: .+ + since: 5.0.10 +TEMPLATE: + summary: Defines a template string used to initialize the command before default values are filled in + description: Generally the template string is formatted in JSON or HTML and then values are filled in with + command parameters. Must be UTF-8 encoded. + parameters: + - name: Template + required: true + description: The template string which should be enclosed in quotes + values: "['\"].*['\"]" + since: 5.0.10 +TEMPLATE_FILE: + summary: Defines a template file used to initialize the command before default values are filled in + description: Generally the template file is formatted in JSON or HTML and then values are filled in with + command parameters. Can be binary or UTF-8. + parameters: + - name: Template File Path + required: true + description: The relative path to the template file. Filename should generally start with an underscore. + values: .+ + since: 5.0.10 \ No newline at end of file diff --git a/openc3/data/config/item_modifiers.yaml b/openc3/data/config/item_modifiers.yaml index d8fbe4d869..288245999f 100644 --- a/openc3/data/config/item_modifiers.yaml +++ b/openc3/data/config/item_modifiers.yaml @@ -219,3 +219,13 @@ LIMITS_RESPONSE: description: Variable length number of options that will be passed to the class constructor values: .+ +KEY: + summary: Defines the key used to access this raw value in the packet. + description: Keys are often JsonPath or XPath strings + example: KEY $.book.title + parameters: + - name: Key string + required: true + description: The key to access this item + values: .+ + since: 5.0.10 \ No newline at end of file diff --git a/openc3/data/config/param_item_modifiers.yaml b/openc3/data/config/param_item_modifiers.yaml index 6c800eec24..c8efb50429 100644 --- a/openc3/data/config/param_item_modifiers.yaml +++ b/openc3/data/config/param_item_modifiers.yaml @@ -50,3 +50,13 @@ OVERLAP: If an item's bit offset overlaps another item, OpenC3 issues a warning. This keyword explicitly allows an item to overlap another and supresses the warning message. since: 4.4.1 +KEY: + summary: Defines the key used to access this raw value in the packet. + description: Keys are often JsonPath or XPath strings + example: KEY $.book.title + parameters: + - name: Key string + required: true + description: The key to access this item + values: .+ + since: 5.0.10 diff --git a/openc3/data/config/telemetry_modifiers.yaml b/openc3/data/config/telemetry_modifiers.yaml index 236dd87bc0..127dc188b2 100644 --- a/openc3/data/config/telemetry_modifiers.yaml +++ b/openc3/data/config/telemetry_modifiers.yaml @@ -157,3 +157,13 @@ HIDDEN: It also hides this telemetry from appearing in the Script Runner popup helper when writing scripts. The telemetry still exists in the system and can received and checked by scripts. +ACCESSOR: + summary: Defines the class used to read and write raw values from the packet + description: Defines the class that is used too read raw values from the packet. Defaults to BinaryAccessor. + Provided accessors also include JsonAccessor, CborAccessor, HtmlAccessor, and XmlAccessor. + parameters: + - name: Accessor Class Name + required: true + description: The name of the accessor class + values: .+ + since: 5.0.10 \ No newline at end of file diff --git a/openc3/ext/openc3/ext/packet/packet.c b/openc3/ext/openc3/ext/packet/packet.c index 11f8e87de1..23afb4c347 100644 --- a/openc3/ext/openc3/ext/packet/packet.c +++ b/openc3/ext/openc3/ext/packet/packet.c @@ -57,6 +57,7 @@ static ID id_ivar_packet_name = 0; static ID id_ivar_description = 0; static ID id_ivar_stored = 0; static ID id_ivar_extra = 0; +static ID id_ivar_template = 0; /* Sets the target name this packet is associated with. Unidentified packets * will have target name set to nil. @@ -193,6 +194,22 @@ static VALUE packet_initialize(int argc, VALUE *argv, VALUE self) switch (argc) { + case 0: + target_name = Qnil; + packet_name = Qnil; + default_endianness = symbol_BIG_ENDIAN; + description = Qnil; + buffer = Qnil; + item_class = cPacketItem; + break; + case 1: + target_name = argv[0]; + packet_name = Qnil; + default_endianness = symbol_BIG_ENDIAN; + description = Qnil; + buffer = Qnil; + item_class = cPacketItem; + break; case 2: target_name = argv[0]; packet_name = argv[1]; @@ -235,7 +252,7 @@ static VALUE packet_initialize(int argc, VALUE *argv, VALUE self) break; default: /* Invalid number of arguments given */ - rb_raise(rb_eArgError, "wrong number of arguments (%d for 2..6)", argc); + rb_raise(rb_eArgError, "wrong number of arguments (%d for 0..6)", argc); break; }; @@ -264,7 +281,7 @@ static VALUE packet_initialize(int argc, VALUE *argv, VALUE self) rb_ivar_set(self, id_ivar_disabled, Qfalse); rb_ivar_set(self, id_ivar_stored, Qfalse); rb_ivar_set(self, id_ivar_extra, Qnil); - + rb_ivar_set(self, id_ivar_template, Qnil); return self; } @@ -305,6 +322,7 @@ void Init_packet(void) id_ivar_description = rb_intern("@description"); id_ivar_stored = rb_intern("@stored"); id_ivar_extra = rb_intern("@extra"); + id_ivar_template = rb_intern("@template"); cPacket = rb_define_class_under(mOpenC3, "Packet", cStructure); rb_define_method(cPacket, "initialize", packet_initialize, -1); diff --git a/openc3/ext/openc3/ext/structure/structure.c b/openc3/ext/openc3/ext/structure/structure.c index ee9cf211c8..72a9f6158f 100644 --- a/openc3/ext/openc3/ext/structure/structure.c +++ b/openc3/ext/openc3/ext/structure/structure.c @@ -44,6 +44,7 @@ static VALUE MAX_INT64 = Qnil; static VALUE MAX_UINT64 = Qnil; static VALUE mOpenC3 = Qnil; +static VALUE cAccessor = Qnil; static VALUE cBinaryAccessor = Qnil; static VALUE cStructure = Qnil; static VALUE cStructureItem = Qnil; @@ -51,6 +52,7 @@ static VALUE cStructureItem = Qnil; static ID id_method_to_s = 0; static ID id_method_raise_buffer_error = 0; static ID id_method_read_array = 0; +static ID id_method_read_item = 0; static ID id_method_force_encoding = 0; static ID id_method_freeze = 0; static ID id_method_slice = 0; @@ -78,6 +80,7 @@ static ID id_ivar_fixed_size = 0; static ID id_ivar_short_buffer_allowed = 0; static ID id_ivar_mutex = 0; static ID id_ivar_create_index = 0; +static ID id_ivar_accessor = 0; static ID id_const_ASCII_8BIT_STRING = 0; static ID id_const_ZERO_STRING = 0; @@ -1298,11 +1301,8 @@ static VALUE structure_length(VALUE self) static VALUE read_item_internal(VALUE self, VALUE item, VALUE buffer) { - volatile VALUE bit_offset = Qnil; - volatile VALUE bit_size = Qnil; volatile VALUE data_type = Qnil; - volatile VALUE array_size = Qnil; - volatile VALUE endianness = Qnil; + volatile VALUE accessor = Qnil; data_type = rb_ivar_get(item, id_ivar_data_type); if (data_type == symbol_DERIVED) @@ -1314,18 +1314,9 @@ static VALUE read_item_internal(VALUE self, VALUE item, VALUE buffer) { buffer = rb_funcall(self, id_method_allocate_buffer_if_needed, 0); } - bit_offset = rb_ivar_get(item, id_ivar_bit_offset); - bit_size = rb_ivar_get(item, id_ivar_bit_size); - array_size = rb_ivar_get(item, id_ivar_array_size); - endianness = rb_ivar_get(item, id_ivar_endianness); - if (RTEST(array_size)) - { - return rb_funcall(cBinaryAccessor, id_method_read_array, 6, bit_offset, bit_size, data_type, array_size, buffer, endianness); - } - else - { - return binary_accessor_read(cBinaryAccessor, bit_offset, bit_size, data_type, buffer, endianness); - } + + accessor = rb_ivar_get(self, id_ivar_accessor); + return rb_funcall(accessor, id_method_read_item, 2, item, buffer); } /* @@ -1562,6 +1553,7 @@ static VALUE structure_initialize(int argc, VALUE *argv, VALUE self) rb_ivar_set(self, id_ivar_fixed_size, Qtrue); rb_ivar_set(self, id_ivar_short_buffer_allowed, Qfalse); rb_ivar_set(self, id_ivar_mutex, Qnil); + rb_ivar_set(self, id_ivar_accessor, cBinaryAccessor); } else { @@ -1607,11 +1599,13 @@ void Init_structure(void) volatile VALUE ascii_8bit_string = Qnil; mOpenC3 = rb_define_module("OpenC3"); - cBinaryAccessor = rb_define_class_under(mOpenC3, "BinaryAccessor", rb_cObject); + cAccessor = rb_define_class_under(mOpenC3, "Accessor", rb_cObject); + cBinaryAccessor = rb_define_class_under(mOpenC3, "BinaryAccessor", cAccessor); id_method_to_s = rb_intern("to_s"); id_method_raise_buffer_error = rb_intern("raise_buffer_error"); id_method_read_array = rb_intern("read_array"); + id_method_read_item = rb_intern("read_item"); id_method_force_encoding = rb_intern("force_encoding"); id_method_freeze = rb_intern("freeze"); id_method_slice = rb_intern("slice"); @@ -1670,6 +1664,7 @@ void Init_structure(void) id_ivar_short_buffer_allowed = rb_intern("@short_buffer_allowed"); id_ivar_mutex = rb_intern("@mutex"); id_ivar_create_index = rb_intern("@create_index"); + id_ivar_accessor = rb_intern("@accessor"); symbol_LITTLE_ENDIAN = ID2SYM(rb_intern("LITTLE_ENDIAN")); symbol_BIG_ENDIAN = ID2SYM(rb_intern("BIG_ENDIAN")); diff --git a/openc3/lib/openc3.rb b/openc3/lib/openc3.rb index 455feb78f2..c87a73a37c 100644 --- a/openc3/lib/openc3.rb +++ b/openc3/lib/openc3.rb @@ -37,6 +37,7 @@ require 'openc3/top_level' require 'openc3/core_ext' require 'openc3/utilities' +require 'openc3/accessors' require 'openc3/conversions' require 'openc3/interfaces' require 'openc3/processors' diff --git a/openc3/lib/openc3/accessors.rb b/openc3/lib/openc3/accessors.rb new file mode 100644 index 0000000000..c8dc1b4208 --- /dev/null +++ b/openc3/lib/openc3/accessors.rb @@ -0,0 +1,23 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +module OpenC3 + autoload(:Accessor, 'openc3/accessors/accessor.rb') + autoload(:BinaryAccessor, 'openc3/accessors/binary_accessor.rb') + autoload(:CborAccessor, 'openc3/accessors/cbor_accessor.rb') + autoload(:HtmlAccessor, 'openc3/accessors/html_accessor.rb') + autoload(:JsonAccessor, 'openc3/accessors/json_accessor.rb') + autoload(:XmlAccessor, 'openc3/accessors/xml_accessor.rb') +end diff --git a/openc3/lib/openc3/accessors/accessor.rb b/openc3/lib/openc3/accessors/accessor.rb new file mode 100644 index 0000000000..d3f22c8eb2 --- /dev/null +++ b/openc3/lib/openc3/accessors/accessor.rb @@ -0,0 +1,71 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +module OpenC3 + class Accessor + def read_item(item, buffer) + raise "Must be defined by subclass" + end + + def write_item(item, value, buffer) + raise "Must be defined by subclass" + end + + def self.read_items(items, buffer) + result = {} + items.each do |item| + result[item.name] = read_item(item, buffer) + end + return result + end + + def self.write_items(items, values, buffer) + items.each_with_index do |item, index| + write_item(item, values[index], buffer) + end + return buffer + end + + def self.convert_to_type(value, item) + data_type = item.data_type + if (data_type == :STRING) || (data_type == :BLOCK) + ####################################### + # Handle :STRING and :BLOCK data types + ####################################### + value = value.to_s + + elsif (data_type == :INT) || (data_type == :UINT) + ################################### + # Handle :INT data type + ################################### + value = Integer(value) + + elsif data_type == :FLOAT + ########################## + # Handle :FLOAT data type + ########################## + value = Float(value) + + else + ############################ + # Handle Unknown data types + ############################ + + raise(ArgumentError, "data_type #{data_type} is not recognized") + end + return value + end + end +end \ No newline at end of file diff --git a/openc3/lib/openc3/accessors/binary_accessor.rb b/openc3/lib/openc3/accessors/binary_accessor.rb new file mode 100644 index 0000000000..7d2721aee7 --- /dev/null +++ b/openc3/lib/openc3/accessors/binary_accessor.rb @@ -0,0 +1,1226 @@ +# encoding: ascii-8bit + +# Copyright 2022 Ball Aerospace & Technologies Corp. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# Modified by OpenC3, Inc. +# All changes Copyright 2022, OpenC3, Inc. +# All Rights Reserved + +# This file contains the implementation of the BinaryAccessor class. +# This class allows for easy reading and writing of binary data in Ruby + +require 'openc3/accessors/accessor' +require 'openc3/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT'] + +module OpenC3 + # Provides methods for binary reading and writing + class BinaryAccessor < Accessor + # Constants for ruby packing directives + PACK_8_BIT_INT = 'c' + PACK_NATIVE_16_BIT_INT = 's' + PACK_LITTLE_ENDIAN_16_BIT_UINT = 'v' + PACK_BIG_ENDIAN_16_BIT_UINT = 'n' + PACK_NATIVE_32_BIT_INT = 'l' + PACK_NATIVE_32_BIT_UINT = 'L' + PACK_NATIVE_64_BIT_INT = 'q' + PACK_NATIVE_64_BIT_UINT = 'Q' + PACK_LITTLE_ENDIAN_32_BIT_UINT = 'V' + PACK_BIG_ENDIAN_32_BIT_UINT = 'N' + PACK_LITTLE_ENDIAN_32_BIT_FLOAT = 'e' + PACK_LITTLE_ENDIAN_64_BIT_FLOAT = 'E' + PACK_BIG_ENDIAN_32_BIT_FLOAT = 'g' + PACK_BIG_ENDIAN_64_BIT_FLOAT = 'G' + PACK_NULL_TERMINATED_STRING = 'Z*' + PACK_BLOCK = 'a*' + PACK_8_BIT_INT_ARRAY = 'c*' + PACK_8_BIT_UINT_ARRAY = 'C*' + PACK_NATIVE_16_BIT_INT_ARRAY = 's*' + PACK_BIG_ENDIAN_16_BIT_UINT_ARRAY = 'n*' + PACK_LITTLE_ENDIAN_16_BIT_UINT_ARRAY = 'v*' + PACK_NATIVE_32_BIT_INT_ARRAY = 'l*' + PACK_BIG_ENDIAN_32_BIT_UINT_ARRAY = 'N*' + PACK_LITTLE_ENDIAN_32_BIT_UINT_ARRAY = 'V*' + PACK_NATIVE_64_BIT_INT_ARRAY = 'q*' + PACK_NATIVE_64_BIT_UINT_ARRAY = 'Q*' + PACK_LITTLE_ENDIAN_32_BIT_FLOAT_ARRAY = 'e*' + PACK_LITTLE_ENDIAN_64_BIT_FLOAT_ARRAY = 'E*' + PACK_BIG_ENDIAN_32_BIT_FLOAT_ARRAY = 'g*' + PACK_BIG_ENDIAN_64_BIT_FLOAT_ARRAY = 'G*' + + if RUBY_ENGINE != 'ruby' or ENV['OPENC3_NO_EXT'] + MIN_INT8 = -128 + MAX_INT8 = 127 + MAX_UINT8 = 255 + MIN_INT16 = -32768 + MAX_INT16 = 32767 + MAX_UINT16 = 65535 + MIN_INT32 = -(2**31) + MAX_INT32 = (2**31) - 1 + MAX_UINT32 = (2**32) - 1 + MIN_INT64 = -(2**63) + MAX_INT64 = (2**63) - 1 + MAX_UINT64 = (2**64) - 1 + end + + # Additional Constants + ZERO_STRING = "\000" + + # Valid data types + DATA_TYPES = [:INT, :UINT, :FLOAT, :STRING, :BLOCK] + + # Valid overflow types + OVERFLOW_TYPES = [:TRUNCATE, :SATURATE, :ERROR, :ERROR_ALLOW_HEX] + + protected + + # Determines the endianness of the host running this code + # + # This method is protected to force the use of the constant + # HOST_ENDIANNESS rather than this method + # + # @return [Symbol] :BIG_ENDIAN or :LITTLE_ENDIAN + def self.get_host_endianness + value = 0x01020304 + packed = [value].pack(PACK_NATIVE_32_BIT_UINT) + unpacked = packed.unpack(PACK_LITTLE_ENDIAN_32_BIT_UINT)[0] + if unpacked == value + :LITTLE_ENDIAN + else + :BIG_ENDIAN + end + end + + def self.raise_buffer_error(read_write, buffer, data_type, given_bit_offset, given_bit_size) + raise ArgumentError, "#{buffer.length} byte buffer insufficient to #{read_write} #{data_type} at bit_offset #{given_bit_offset} with bit_size #{given_bit_size}" + end + + public + + # Store the host endianness so that it only has to be determined once + HOST_ENDIANNESS = get_host_endianness() + # Valid endianess + ENDIANNESS = [:BIG_ENDIAN, :LITTLE_ENDIAN] + + def self.read_item(item, buffer) + return nil if item.data_type == :DERIVED + if item.array_size + return read_array(item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness) + else + return read(item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness) + end + end + + def self.write_item(item, value, buffer) + return nil if item.data_type == :DERIVED + if item.array_size + return write_array(value, item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness, item.overflow) + else + return write(value, item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness, item.overflow) + end + end + + if RUBY_ENGINE != 'ruby' or ENV['OPENC3_NO_EXT'] + # Reads binary data of any data type from a buffer + # + # @param bit_offset [Integer] Bit offset to the start of the item. A + # negative number means to offset from the end of the buffer. + # @param bit_size [Integer] Size of the item in bits + # @param data_type [Symbol] {DATA_TYPES} + # @param buffer [String] Binary string buffer to read from + # @param endianness [Symbol] {ENDIANNESS} + # @return [Integer] value read from the buffer + def self.read(bit_offset, bit_size, data_type, buffer, endianness) + given_bit_offset = bit_offset + given_bit_size = bit_size + + bit_offset = check_bit_offset_and_size(:read, given_bit_offset, given_bit_size, data_type, buffer) + + # If passed a negative bit size with strings or blocks + # recalculate based on the buffer length + if (bit_size <= 0) && ((data_type == :STRING) || (data_type == :BLOCK)) + bit_size = (buffer.length * 8) - bit_offset + bit_size + if bit_size == 0 + return "" + elsif bit_size < 0 + raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) + end + end + + result, lower_bound, upper_bound = check_bounds_and_buffer_size(bit_offset, bit_size, buffer.length, endianness, data_type) + raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) unless result + + if (data_type == :STRING) || (data_type == :BLOCK) + ####################################### + # Handle :STRING and :BLOCK data types + ####################################### + + if byte_aligned(bit_offset) + if data_type == :STRING + return buffer[lower_bound..upper_bound].unpack('Z*')[0] + else + return buffer[lower_bound..upper_bound].unpack('a*')[0] + end + else + raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") + end + + elsif (data_type == :INT) || (data_type == :UINT) + ################################### + # Handle :INT and :UINT data types + ################################### + + if byte_aligned(bit_offset) && even_bit_size(bit_size) + + if data_type == :INT + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit :INT + ########################################################### + + case bit_size + when 8 + return buffer[lower_bound].unpack(PACK_8_BIT_INT)[0] + when 16 + if endianness == HOST_ENDIANNESS + return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_16_BIT_INT)[0] + else # endianness != HOST_ENDIANNESS + temp = buffer[lower_bound..upper_bound].reverse + return temp.unpack(PACK_NATIVE_16_BIT_INT)[0] + end + when 32 + if endianness == HOST_ENDIANNESS + return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_32_BIT_INT)[0] + else # endianness != HOST_ENDIANNESS + temp = buffer[lower_bound..upper_bound].reverse + return temp.unpack(PACK_NATIVE_32_BIT_INT)[0] + end + when 64 + if endianness == HOST_ENDIANNESS + return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_INT)[0] + else # endianness != HOST_ENDIANNESS + temp = buffer[lower_bound..upper_bound].reverse + return temp.unpack(PACK_NATIVE_64_BIT_INT)[0] + end + end + else # data_type == :UINT + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit :UINT + ########################################################### + + case bit_size + when 8 + return buffer.getbyte(lower_bound) + when 16 + if endianness == :BIG_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_16_BIT_UINT)[0] + else # endianness == :LITTLE_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_16_BIT_UINT)[0] + end + when 32 + if endianness == :BIG_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_UINT)[0] + else # endianness == :LITTLE_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_UINT)[0] + end + when 64 + if endianness == HOST_ENDIANNESS + return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_UINT)[0] + else # endianness != HOST_ENDIANNESS + temp = buffer[lower_bound..upper_bound].reverse + return temp.unpack(PACK_NATIVE_64_BIT_UINT)[0] + end + end + end + + else + ########################## + # Handle :INT and :UINT Bitfields + ########################## + + # Extract Data for Bitfield + if endianness == :LITTLE_ENDIAN + # Bitoffset always refers to the most significant bit of a bitfield + num_bytes = (((bit_offset % 8) + bit_size - 1) / 8) + 1 + upper_bound = bit_offset / 8 + lower_bound = upper_bound - num_bytes + 1 + + if lower_bound < 0 + raise(ArgumentError, "LITTLE_ENDIAN bitfield with bit_offset #{given_bit_offset} and bit_size #{given_bit_size} is invalid") + end + + temp_data = buffer[lower_bound..upper_bound].reverse + else + temp_data = buffer[lower_bound..upper_bound] + end + + # Determine temp upper bound + temp_upper = upper_bound - lower_bound + + # Handle Bitfield + start_bits = bit_offset % 8 + start_mask = ~(0xFF << (8 - start_bits)) + total_bits = (temp_upper + 1) * 8 + right_shift = total_bits - start_bits - bit_size + + # Mask off unwanted bits at beginning + temp = temp_data.getbyte(0) & start_mask + + if upper_bound > lower_bound + # Combine bytes into a FixNum + temp_data[1..temp_upper].each_byte { |temp_value| temp = temp << 8; temp = temp + temp_value } + end + + # Shift off unwanted bits at end + temp = temp >> right_shift + + if data_type == :INT + # Convert to negative if necessary + if (bit_size > 1) && (temp[bit_size - 1] == 1) + temp = -((1 << bit_size) - temp) + end + end + + return temp + end + + elsif data_type == :FLOAT + ########################## + # Handle :FLOAT data type + ########################## + + if byte_aligned(bit_offset) + case bit_size + when 32 + if endianness == :BIG_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_FLOAT)[0] + else # endianness == :LITTLE_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT)[0] + end + when 64 + if endianness == :BIG_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_64_BIT_FLOAT)[0] + else # endianness == :LITTLE_ENDIAN + return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT)[0] + end + else + raise(ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}") + end + else + raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") + end + + else + ############################ + # Handle Unknown data types + ############################ + + raise(ArgumentError, "data_type #{data_type} is not recognized") + end + + return return_value + end + + # Writes binary data of any data type to a buffer + # + # @param value [Varies] Value to write into the buffer + # @param bit_offset [Integer] Bit offset to the start of the item. A + # negative number means to offset from the end of the buffer. + # @param bit_size [Integer] Size of the item in bits + # @param data_type [Symbol] {DATA_TYPES} + # @param buffer [String] Binary string buffer to write to + # @param endianness [Symbol] {ENDIANNESS} + # @param overflow [Symbol] {OVERFLOW_TYPES} + # @return [Integer] value passed in as a parameter + def self.write(value, bit_offset, bit_size, data_type, buffer, endianness, overflow) + given_bit_offset = bit_offset + given_bit_size = bit_size + + bit_offset = check_bit_offset_and_size(:write, given_bit_offset, given_bit_size, data_type, buffer) + + # If passed a negative bit size with strings or blocks + # recalculate based on the value length in bytes + if (bit_size <= 0) && ((data_type == :STRING) || (data_type == :BLOCK)) + value = value.to_s + bit_size = value.length * 8 + end + + result, lower_bound, upper_bound = check_bounds_and_buffer_size(bit_offset, bit_size, buffer.length, endianness, data_type) + raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) if !result && (given_bit_size > 0) + + # Check overflow type + if (overflow != :TRUNCATE) && (overflow != :SATURATE) && (overflow != :ERROR) && (overflow != :ERROR_ALLOW_HEX) + raise(ArgumentError, "unknown overflow type #{overflow}") + end + + if (data_type == :STRING) || (data_type == :BLOCK) + ####################################### + # Handle :STRING and :BLOCK data types + ####################################### + value = value.to_s + + if byte_aligned(bit_offset) + temp = value + if given_bit_size <= 0 + end_bytes = -(given_bit_size / 8) + old_upper_bound = buffer.length - 1 - end_bytes + # Lower bound + end_bytes can never be more than 1 byte outside of the given buffer + if (lower_bound + end_bytes) > buffer.length + raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) + end + + if old_upper_bound < lower_bound + # String was completely empty + if end_bytes > 0 + # Preserve bytes at end of buffer + buffer << "\000" * value.length + buffer[lower_bound + value.length, end_bytes] = buffer[lower_bound, end_bytes] + end + elsif bit_size == 0 + # Remove entire string + buffer[lower_bound, old_upper_bound - lower_bound + 1] = '' + elsif upper_bound < old_upper_bound + # Remove extra bytes from old string + buffer[upper_bound + 1, old_upper_bound - upper_bound] = '' + elsif (upper_bound > old_upper_bound) && (end_bytes > 0) + # Preserve bytes at end of buffer + diff = upper_bound - old_upper_bound + buffer << "\000" * diff + buffer[upper_bound + 1, end_bytes] = buffer[old_upper_bound + 1, end_bytes] + end + else # given_bit_size > 0 + byte_size = bit_size / 8 + if value.length < byte_size + # Pad the requested size with zeros + temp = value.ljust(byte_size, "\000") + elsif value.length > byte_size + if overflow == :TRUNCATE + # Resize the value to fit the field + value[byte_size, value.length - byte_size] = '' + else + raise(ArgumentError, "value of #{value.length} bytes does not fit into #{byte_size} bytes for data_type #{data_type}") + end + end + end + if bit_size != 0 + buffer[lower_bound, temp.length] = temp + end + else + raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") + end + + elsif (data_type == :INT) || (data_type == :UINT) + ################################### + # Handle :INT data type + ################################### + value = Integer(value) + min_value, max_value, hex_max_value = get_check_overflow_ranges(bit_size, data_type) + value = check_overflow(value, min_value, max_value, hex_max_value, bit_size, data_type, overflow) + + if byte_aligned(bit_offset) && even_bit_size(bit_size) + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit + ########################################################### + + if data_type == :INT + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit :INT + ########################################################### + + case bit_size + when 8 + buffer.setbyte(lower_bound, value) + when 16 + if endianness == HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_16_BIT_INT) + else # endianness != HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_16_BIT_INT).reverse + end + when 32 + if endianness == HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_32_BIT_INT) + else # endianness != HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_32_BIT_INT).reverse + end + when 64 + if endianness == HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_INT) + else # endianness != HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_INT).reverse + end + end + else # data_type == :UINT + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit :UINT + ########################################################### + + case bit_size + when 8 + buffer.setbyte(lower_bound, value) + when 16 + if endianness == :BIG_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_16_BIT_UINT) + else # endianness == :LITTLE_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_16_BIT_UINT) + end + when 32 + if endianness == :BIG_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_32_BIT_UINT) + else # endianness == :LITTLE_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_32_BIT_UINT) + end + when 64 + if endianness == HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_UINT) + else # endianness != HOST_ENDIANNESS + buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_UINT).reverse + end + end + end + + else + ########################################################### + # Handle bit fields + ########################################################### + + # Extract Existing Data + if endianness == :LITTLE_ENDIAN + # Bitoffset always refers to the most significant bit of a bitfield + num_bytes = (((bit_offset % 8) + bit_size - 1) / 8) + 1 + upper_bound = bit_offset / 8 + lower_bound = upper_bound - num_bytes + 1 + if lower_bound < 0 + raise(ArgumentError, "LITTLE_ENDIAN bitfield with bit_offset #{given_bit_offset} and bit_size #{given_bit_size} is invalid") + end + + temp_data = buffer[lower_bound..upper_bound].reverse + else + temp_data = buffer[lower_bound..upper_bound] + end + + # Determine temp upper bound + temp_upper = upper_bound - lower_bound + + # Determine Values needed to Handle Bitfield + start_bits = bit_offset % 8 + start_mask = (0xFF << (8 - start_bits)) + total_bits = (temp_upper + 1) * 8 + end_bits = total_bits - start_bits - bit_size + end_mask = ~(0xFF << end_bits) + + # Add in Start Bits + temp = temp_data.getbyte(0) & start_mask + + # Adjust value to correct number of bits + temp_mask = (2**bit_size) - 1 + temp_value = value & temp_mask + + # Add in New Data + temp = (temp << (bit_size - (8 - start_bits))) + temp_value + + # Add in Remainder of Existing Data + temp = (temp << end_bits) + (temp_data.getbyte(temp_upper) & end_mask) + + # Extract into an array of bytes + temp_array = [] + (0..temp_upper).each { temp_array.insert(0, (temp & 0xFF)); temp = temp >> 8 } + + # Store into data + if endianness == :LITTLE_ENDIAN + buffer[lower_bound..upper_bound] = temp_array.pack(PACK_8_BIT_UINT_ARRAY).reverse + else + buffer[lower_bound..upper_bound] = temp_array.pack(PACK_8_BIT_UINT_ARRAY) + end + + end + + elsif data_type == :FLOAT + ########################## + # Handle :FLOAT data type + ########################## + value = Float(value) + + if byte_aligned(bit_offset) + case bit_size + when 32 + if endianness == :BIG_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_32_BIT_FLOAT) + else # endianness == :LITTLE_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT) + end + when 64 + if endianness == :BIG_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_64_BIT_FLOAT) + else # endianness == :LITTLE_ENDIAN + buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT) + end + else + raise(ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}") + end + else + raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") + end + + else + ############################ + # Handle Unknown data types + ############################ + + raise(ArgumentError, "data_type #{data_type} is not recognized") + end + + return value + end + + protected + + # Check the bit size and bit offset for problems. Recalulate the bit offset + # and return back through the passed in pointer. + def self.check_bit_offset_and_size(read_or_write, given_bit_offset, given_bit_size, data_type, buffer) + bit_offset = given_bit_offset + + if (given_bit_size <= 0) && (data_type != :STRING) && (data_type != :BLOCK) + raise(ArgumentError, "bit_size #{given_bit_size} must be positive for data types other than :STRING and :BLOCK") + end + + if (given_bit_size <= 0) && (given_bit_offset < 0) + raise(ArgumentError, "negative or zero bit_sizes (#{given_bit_size}) cannot be given with negative bit_offsets (#{given_bit_offset})") + end + + if given_bit_offset < 0 + bit_offset = (buffer.length * 8) + bit_offset + if bit_offset < 0 + raise_buffer_error(read_or_write, buffer, data_type, given_bit_offset, given_bit_size) + end + end + + return bit_offset + end + + # Calculate the bounds of the string to access the item based on the bit_offset and bit_size. + # Also determine if the buffer size is sufficient. + def self.check_bounds_and_buffer_size(bit_offset, bit_size, buffer_length, endianness, data_type) + result = true # Assume ok + + # Define bounds of string to access this item + lower_bound = bit_offset / 8 + upper_bound = (bit_offset + bit_size - 1) / 8 + + # Sanity check buffer size + if upper_bound >= buffer_length + # If it's not the special case of little endian bit field then we fail and return false + if !((endianness == :LITTLE_ENDIAN) && + ((data_type == :INT) || (data_type == :UINT)) && + # Not byte aligned with an even bit size + (!((byte_aligned(bit_offset)) && (even_bit_size(bit_size)))) && + (lower_bound < buffer_length) + ) + result = false + end + end + return result, lower_bound, upper_bound + end + + def self.get_check_overflow_ranges(bit_size, data_type) + min_value = 0 # Default for UINT cases + + case bit_size + when 8 + hex_max_value = MAX_UINT8 + if data_type == :INT + min_value = MIN_INT8 + max_value = MAX_INT8 + else + max_value = MAX_UINT8 + end + when 16 + hex_max_value = MAX_UINT16 + if data_type == :INT + min_value = MIN_INT16 + max_value = MAX_INT16 + else + max_value = MAX_UINT16 + end + when 32 + hex_max_value = MAX_UINT32 + if data_type == :INT + min_value = MIN_INT32 + max_value = MAX_INT32 + else + max_value = MAX_UINT32 + end + when 64 + hex_max_value = MAX_UINT64 + if data_type == :INT + min_value = MIN_INT64 + max_value = MAX_INT64 + else + max_value = MAX_UINT64 + end + else # Bitfield + if data_type == :INT + # Note signed integers must allow up to the maximum unsigned value to support values given in hex + if bit_size > 1 + max_value = 2**(bit_size - 1) + # min_value = -(2 ** bit_size - 1) + min_value = -max_value + # max_value = (2 ** bit_size - 1) - 1 + max_value -= 1 + # hex_max_value = (2 ** bit_size) - 1 + hex_max_value = (2**bit_size) - 1 + else # 1-bit signed + min_value = -1 + max_value = 1 + hex_max_value = 1 + end + else + max_value = (2**bit_size) - 1 + hex_max_value = max_value + end + end + + return min_value, max_value, hex_max_value + end + + def self.byte_aligned(value) + (value % 8) == 0 + end + + def self.even_bit_size(bit_size) + (bit_size == 8) || (bit_size == 16) || (bit_size == 32) || (bit_size == 64) + end + + public + + end + + # Reads an array of binary data of any data type from a buffer + # + # @param bit_offset [Integer] Bit offset to the start of the array. A + # negative number means to offset from the end of the buffer. + # @param bit_size [Integer] Size of each item in the array in bits + # @param data_type [Symbol] {DATA_TYPES} + # @param array_size [Integer] Size in bits of the array. 0 or negative means + # fill the array with as many bit_size number of items that exist (negative + # means excluding the final X number of bits). + # @param buffer [String] Binary string buffer to read from + # @param endianness [Symbol] {ENDIANNESS} + # @return [Array] Array created from reading the buffer + def self.read_array(bit_offset, bit_size, data_type, array_size, buffer, endianness) + # Save given values of bit offset, bit size, and array_size + given_bit_offset = bit_offset + given_bit_size = bit_size + given_array_size = array_size + + # Handle negative and zero bit sizes + raise ArgumentError, "bit_size #{given_bit_size} must be positive for arrays" if bit_size <= 0 + + # Handle negative bit offsets + if bit_offset < 0 + bit_offset = ((buffer.length * 8) + bit_offset) + raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) if bit_offset < 0 + end + + # Handle negative and zero array sizes + if array_size <= 0 + if given_bit_offset < 0 + raise ArgumentError, "negative or zero array_size (#{given_array_size}) cannot be given with negative bit_offset (#{given_bit_offset})" + else + array_size = ((buffer.length * 8) - bit_offset + array_size) + if array_size == 0 + return [] + elsif array_size < 0 + raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) + end + end + end + + # Calculate number of items in the array + # If there is a remainder then we have a problem + raise ArgumentError, "array_size #{given_array_size} not a multiple of bit_size #{given_bit_size}" if array_size % bit_size != 0 + + num_items = array_size / bit_size + + # Define bounds of string to access this item + lower_bound = bit_offset / 8 + upper_bound = (bit_offset + array_size - 1) / 8 + + # Check for byte alignment + byte_aligned = ((bit_offset % 8) == 0) + + case data_type + when :STRING, :BLOCK + ####################################### + # Handle :STRING and :BLOCK data types + ####################################### + + if byte_aligned + value = [] + num_items.times do + value << self.read(bit_offset, bit_size, data_type, buffer, endianness) + bit_offset += bit_size + end + else + raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" + end + + when :INT, :UINT + ################################### + # Handle :INT and :UINT data types + ################################### + + if byte_aligned and (bit_size == 8 or bit_size == 16 or bit_size == 32 or bit_size == 64) + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit :INT and :UINT + ########################################################### + + case bit_size + when 8 + if data_type == :INT + value = buffer[lower_bound..upper_bound].unpack(PACK_8_BIT_INT_ARRAY) + else # data_type == :UINT + value = buffer[lower_bound..upper_bound].unpack(PACK_8_BIT_UINT_ARRAY) + end + + when 16 + if data_type == :INT + if endianness == HOST_ENDIANNESS + value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_16_BIT_INT_ARRAY) + else # endianness != HOST_ENDIANNESS + temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 2) + value = temp.to_s.unpack(PACK_NATIVE_16_BIT_INT_ARRAY) + end + else # data_type == :UINT + if endianness == :BIG_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_16_BIT_UINT_ARRAY) + else # endianness == :LITTLE_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_16_BIT_UINT_ARRAY) + end + end + + when 32 + if data_type == :INT + if endianness == HOST_ENDIANNESS + value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_32_BIT_INT_ARRAY) + else # endianness != HOST_ENDIANNESS + temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 4) + value = temp.to_s.unpack(PACK_NATIVE_32_BIT_INT_ARRAY) + end + else # data_type == :UINT + if endianness == :BIG_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_UINT_ARRAY) + else # endianness == :LITTLE_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_UINT_ARRAY) + end + end + + when 64 + if data_type == :INT + if endianness == HOST_ENDIANNESS + value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_INT_ARRAY) + else # endianness != HOST_ENDIANNESS + temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 8) + value = temp.to_s.unpack(PACK_NATIVE_64_BIT_INT_ARRAY) + end + else # data_type == :UINT + if endianness == HOST_ENDIANNESS + value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_UINT_ARRAY) + else # endianness != HOST_ENDIANNESS + temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 8) + value = temp.to_s.unpack(PACK_NATIVE_64_BIT_UINT_ARRAY) + end + end + end + + else + ################################## + # Handle :INT and :UINT Bitfields + ################################## + raise ArgumentError, "read_array does not support little endian bit fields with bit_size greater than 1-bit" if endianness == :LITTLE_ENDIAN and bit_size > 1 + + value = [] + num_items.times do + value << self.read(bit_offset, bit_size, data_type, buffer, endianness) + bit_offset += bit_size + end + end + + when :FLOAT + ########################## + # Handle :FLOAT data type + ########################## + + if byte_aligned + case bit_size + when 32 + if endianness == :BIG_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_FLOAT_ARRAY) + else # endianness == :LITTLE_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT_ARRAY) + end + + when 64 + if endianness == :BIG_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_64_BIT_FLOAT_ARRAY) + else # endianness == :LITTLE_ENDIAN + value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT_ARRAY) + end + + else + raise ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}" + end + + else + raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" + end + + else + ############################ + # Handle Unknown data types + ############################ + + raise ArgumentError, "data_type #{data_type} is not recognized" + end + + value + end # def read_array + + # Writes an array of binary data of any data type to a buffer + # + # @param values [Array] Values to write into the buffer + # @param bit_offset [Integer] Bit offset to the start of the array. A + # negative number means to offset from the end of the buffer. + # @param bit_size [Integer] Size of each item in the array in bits + # @param data_type [Symbol] {DATA_TYPES} + # @param array_size [Integer] Size in bits of the array as represented in the buffer. + # Size 0 means to fill the buffer with as many bit_size number of items that exist + # (negative means excluding the final X number of bits). + # @param buffer [String] Binary string buffer to write to + # @param endianness [Symbol] {ENDIANNESS} + # @return [Array] values passed in as a parameter + def self.write_array(values, bit_offset, bit_size, data_type, array_size, buffer, endianness, overflow) + # Save given values of bit offset, bit size, and array_size + given_bit_offset = bit_offset + given_bit_size = bit_size + given_array_size = array_size + + # Verify an array was given + raise ArgumentError, "values must be an Array type class is #{values.class}" unless values.kind_of? Array + + # Handle negative and zero bit sizes + raise ArgumentError, "bit_size #{given_bit_size} must be positive for arrays" if bit_size <= 0 + + # Handle negative bit offsets + if bit_offset < 0 + bit_offset = ((buffer.length * 8) + bit_offset) + raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) if bit_offset < 0 + end + + # Handle negative and zero array sizes + if array_size <= 0 + if given_bit_offset < 0 + raise ArgumentError, "negative or zero array_size (#{given_array_size}) cannot be given with negative bit_offset (#{given_bit_offset})" + else + end_bytes = -(given_array_size / 8) + lower_bound = bit_offset / 8 + upper_bound = (bit_offset + (bit_size * values.length) - 1) / 8 + old_upper_bound = buffer.length - 1 - end_bytes + + if upper_bound < old_upper_bound + # Remove extra bytes from old buffer + buffer[(upper_bound + 1)..old_upper_bound] = '' + elsif upper_bound > old_upper_bound + # Grow buffer and preserve bytes at end of buffer if necesssary + buffer_length = buffer.length + diff = upper_bound - old_upper_bound + buffer << ZERO_STRING * diff + if end_bytes > 0 + buffer[(upper_bound + 1)..(buffer.length - 1)] = buffer[(old_upper_bound + 1)..(buffer_length - 1)] + end + end + + array_size = ((buffer.length * 8) - bit_offset + array_size) + end + end + + # Get data bounds for this array + lower_bound = bit_offset / 8 + upper_bound = (bit_offset + array_size - 1) / 8 + num_bytes = upper_bound - lower_bound + 1 + + # Check for byte alignment + byte_aligned = ((bit_offset % 8) == 0) + + # Calculate the number of writes + num_writes = array_size / bit_size + # Check for a negative array_size and adjust the number of writes + # to simply be the number of values in the passed in array + if given_array_size <= 0 + num_writes = values.length + end + + # Ensure the buffer has enough room + if bit_offset + num_writes * bit_size > buffer.length * 8 + raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) + end + + # Ensure the given_array_size is an even multiple of bit_size + raise ArgumentError, "array_size #{given_array_size} not a multiple of bit_size #{given_bit_size}" if array_size % bit_size != 0 + + raise ArgumentError, "too many values #{values.length} for given array_size #{given_array_size} and bit_size #{given_bit_size}" if num_writes < values.length + + # Check overflow type + raise "unknown overflow type #{overflow}" unless OVERFLOW_TYPES.include?(overflow) + + case data_type + when :STRING, :BLOCK + ####################################### + # Handle :STRING and :BLOCK data types + ####################################### + + if byte_aligned + num_writes.times do |index| + self.write(values[index], bit_offset, bit_size, data_type, buffer, endianness, overflow) + bit_offset += bit_size + end + else + raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" + end + + when :INT, :UINT + ################################### + # Handle :INT and :UINT data types + ################################### + + if byte_aligned and (bit_size == 8 or bit_size == 16 or bit_size == 32 or bit_size == 64) + ########################################################### + # Handle byte-aligned 8, 16, 32, and 64 bit :INT and :UINT + ########################################################### + + case bit_size + when 8 + if data_type == :INT + values = self.check_overflow_array(values, MIN_INT8, MAX_INT8, MAX_UINT8, bit_size, data_type, overflow) + packed = values.pack(PACK_8_BIT_INT_ARRAY) + else # data_type == :UINT + values = self.check_overflow_array(values, 0, MAX_UINT8, MAX_UINT8, bit_size, data_type, overflow) + packed = values.pack(PACK_8_BIT_UINT_ARRAY) + end + + when 16 + if data_type == :INT + values = self.check_overflow_array(values, MIN_INT16, MAX_INT16, MAX_UINT16, bit_size, data_type, overflow) + if endianness == HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_16_BIT_INT_ARRAY) + else # endianness != HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_16_BIT_INT_ARRAY) + self.byte_swap_buffer!(packed, 2) + end + else # data_type == :UINT + values = self.check_overflow_array(values, 0, MAX_UINT16, MAX_UINT16, bit_size, data_type, overflow) + if endianness == :BIG_ENDIAN + packed = values.pack(PACK_BIG_ENDIAN_16_BIT_UINT_ARRAY) + else # endianness == :LITTLE_ENDIAN + packed = values.pack(PACK_LITTLE_ENDIAN_16_BIT_UINT_ARRAY) + end + end + + when 32 + if data_type == :INT + values = self.check_overflow_array(values, MIN_INT32, MAX_INT32, MAX_UINT32, bit_size, data_type, overflow) + if endianness == HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_32_BIT_INT_ARRAY) + else # endianness != HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_32_BIT_INT_ARRAY) + self.byte_swap_buffer!(packed, 4) + end + else # data_type == :UINT + values = self.check_overflow_array(values, 0, MAX_UINT32, MAX_UINT32, bit_size, data_type, overflow) + if endianness == :BIG_ENDIAN + packed = values.pack(PACK_BIG_ENDIAN_32_BIT_UINT_ARRAY) + else # endianness == :LITTLE_ENDIAN + packed = values.pack(PACK_LITTLE_ENDIAN_32_BIT_UINT_ARRAY) + end + end + + when 64 + if data_type == :INT + values = self.check_overflow_array(values, MIN_INT64, MAX_INT64, MAX_UINT64, bit_size, data_type, overflow) + if endianness == HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_64_BIT_INT_ARRAY) + else # endianness != HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_64_BIT_INT_ARRAY) + self.byte_swap_buffer!(packed, 8) + end + else # data_type == :UINT + values = self.check_overflow_array(values, 0, MAX_UINT64, MAX_UINT64, bit_size, data_type, overflow) + if endianness == HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_64_BIT_UINT_ARRAY) + else # endianness != HOST_ENDIANNESS + packed = values.pack(PACK_NATIVE_64_BIT_UINT_ARRAY) + self.byte_swap_buffer!(packed, 8) + end + end + end + + # Adjust packed size to hold number of items written + buffer[lower_bound..upper_bound] = adjust_packed_size(num_bytes, packed) if num_bytes > 0 + + else + ################################## + # Handle :INT and :UINT Bitfields + ################################## + + raise ArgumentError, "write_array does not support little endian bit fields with bit_size greater than 1-bit" if endianness == :LITTLE_ENDIAN and bit_size > 1 + + num_writes.times do |index| + self.write(values[index], bit_offset, bit_size, data_type, buffer, endianness, overflow) + bit_offset += bit_size + end + end + + when :FLOAT + ########################## + # Handle :FLOAT data type + ########################## + + if byte_aligned + case bit_size + when 32 + if endianness == :BIG_ENDIAN + packed = values.pack(PACK_BIG_ENDIAN_32_BIT_FLOAT_ARRAY) + else # endianness == :LITTLE_ENDIAN + packed = values.pack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT_ARRAY) + end + + when 64 + if endianness == :BIG_ENDIAN + packed = values.pack(PACK_BIG_ENDIAN_64_BIT_FLOAT_ARRAY) + else # endianness == :LITTLE_ENDIAN + packed = values.pack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT_ARRAY) + end + + else + raise ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}" + end + + # Adjust packed size to hold number of items written + buffer[lower_bound..upper_bound] = adjust_packed_size(num_bytes, packed) if num_bytes > 0 + + else + raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" + end + + else + ############################ + # Handle Unknown data types + ############################ + raise ArgumentError, "data_type #{data_type} is not recognized" + end # case data_type + + values + end # def write_array + + # Adjusts the packed array to be the given number of bytes + # + # @param num_bytes [Integer] The desired number of bytes + # @param packed [Array] The packed data buffer + def self.adjust_packed_size(num_bytes, packed) + difference = num_bytes - packed.length + if difference > 0 + packed << (ZERO_STRING * difference) + elsif difference < 0 + packed = packed[0..(packed.length - 1 + difference)] + end + packed + end + + # Byte swaps every X bytes of data in a buffer overwriting the buffer + # + # @param buffer [String] Buffer to modify + # @param num_bytes_per_word [Integer] Number of bytes per word that will be swapped + # @return [String] buffer passed in as a parameter + def self.byte_swap_buffer!(buffer, num_bytes_per_word) + num_swaps = buffer.length / num_bytes_per_word + index = 0 + num_swaps.times do + range = index..(index + num_bytes_per_word - 1) + buffer[range] = buffer[range].reverse + index += num_bytes_per_word + end + buffer + end + + # Byte swaps every X bytes of data in a buffer into a new buffer + # + # @param buffer [String] Buffer that will be copied then modified + # @param num_bytes_per_word [Integer] Number of bytes per word that will be swapped + # @return [String] modified buffer + def self.byte_swap_buffer(buffer, num_bytes_per_word) + buffer = buffer.clone + self.byte_swap_buffer!(buffer, num_bytes_per_word) + end + + # Checks for overflow of an integer data type + # + # @param value [Integer] Value to write into the buffer + # @param min_value [Integer] Minimum allowed value + # @param max_value [Integer] Maximum allowed value + # @param hex_max_value [Integer] Maximum allowed value if specified in hex + # @param bit_size [Integer] Size of the item in bits + # @param data_type [Symbol] {DATA_TYPES} + # @param overflow [Symbol] {OVERFLOW_TYPES} + # @return [Integer] Potentially modified value + def self.check_overflow(value, min_value, max_value, hex_max_value, bit_size, data_type, overflow) + if overflow == :TRUNCATE + # Note this will always convert to unsigned equivalent for signed integers + value = value % (hex_max_value + 1) + else + if value > max_value + if overflow == :SATURATE + value = max_value + else + if overflow == :ERROR or value > hex_max_value + raise ArgumentError, "value of #{value} invalid for #{bit_size}-bit #{data_type}" + end + end + elsif value < min_value + if overflow == :SATURATE + value = min_value + else + raise ArgumentError, "value of #{value} invalid for #{bit_size}-bit #{data_type}" + end + end + end + value + end + + # Checks for overflow of an array of integer data types + # + # @param values [Array[Integer]] Values to write into the buffer + # @param min_value [Integer] Minimum allowed value + # @param max_value [Integer] Maximum allowed value + # @param hex_max_value [Integer] Maximum allowed value if specified in hex + # @param bit_size [Integer] Size of the item in bits + # @param data_type [Symbol] {DATA_TYPES} + # @param overflow [Symbol] {OVERFLOW_TYPES} + # @return [Array[Integer]] Potentially modified values + def self.check_overflow_array(values, min_value, max_value, hex_max_value, bit_size, data_type, overflow) + if overflow != :TRUNCATE + values.each_with_index do |value, index| + values[index] = check_overflow(value, min_value, max_value, hex_max_value, bit_size, data_type, overflow) + end + end + values + end + end # class BinaryAccessor +end diff --git a/openc3/lib/openc3/accessors/cbor_accessor.rb b/openc3/lib/openc3/accessors/cbor_accessor.rb new file mode 100644 index 0000000000..c81ddfaa13 --- /dev/null +++ b/openc3/lib/openc3/accessors/cbor_accessor.rb @@ -0,0 +1,83 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +require 'cbor' +require 'openc3/accessors/json_accessor' + +module OpenC3 + class CborAccessor < JsonAccessor + def self.read_item(item, buffer) + return nil if item.data_type == :DERIVED + if String === buffer + parsed = CBOR.decode(buffer) + else + parsed = buffer + end + return super(item, parsed) + end + + def self.write_item(item, value, buffer) + return nil if item.data_type == :DERIVED + + # Convert to ruby objects + if String === buffer + decoded = CBOR.decode(buffer) + else + decoded = buffer + end + + # Write the value + write_item_internal(item, value, decoded) + + # Update buffer + if String === buffer + buffer.replace(decoded.to_cbor) + end + + return buffer + end + + def self.read_items(items, buffer) + # Prevent JsonPath from decoding every call + if String === buffer + decoded = CBOR.decode(buffer) + else + decoded = buffer + end + super(items, decoded) + end + + def self.write_items(items, values, buffer) + # Convert to ruby objects + if String === buffer + decoded = CBOR.decode(buffer) + else + decoded = buffer + end + + items.each_with_index do |item, index| + write_item_internal(item, values[index], decoded) + end + + # Update buffer + if String === buffer + buffer.replace(decoded.to_cbor) + end + + return buffer + end + + end +end \ No newline at end of file diff --git a/openc3/lib/openc3/accessors/html_accessor.rb b/openc3/lib/openc3/accessors/html_accessor.rb new file mode 100644 index 0000000000..db5498da28 --- /dev/null +++ b/openc3/lib/openc3/accessors/html_accessor.rb @@ -0,0 +1,28 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +require 'openc3/accessors/xml_accessor' + +module OpenC3 + class HtmlAccessor < XmlAccessor + def self.buffer_to_doc(buffer) + Nokogiri.HTML(buffer) + end + + def self.doc_to_buffer(doc) + doc.to_html + end + end +end \ No newline at end of file diff --git a/openc3/lib/openc3/accessors/json_accessor.rb b/openc3/lib/openc3/accessors/json_accessor.rb new file mode 100644 index 0000000000..b1520863c7 --- /dev/null +++ b/openc3/lib/openc3/accessors/json_accessor.rb @@ -0,0 +1,131 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +require 'json' +require 'jsonpath' +require 'openc3/accessors/accessor' + +module OpenC3 + class JsonAccessor < Accessor + def self.read_item(item, buffer) + return nil if item.data_type == :DERIVED + return JsonPath.on(buffer, item.key).first + end + + def self.write_item(item, value, buffer) + return nil if item.data_type == :DERIVED + + # Convert to ruby objects + if String === buffer + decoded = JSON.parse(buffer, :allow_nan => true) + else + decoded = buffer + end + + # Write the value + write_item_internal(item, value, decoded) + + # Update buffer + if String === buffer + buffer.replace(JSON.generate(decoded, :allow_nan => true)) + end + + return buffer + end + + def self.read_items(items, buffer) + # Prevent JsonPath from decoding every call + if String === buffer + decoded = JSON.parse(buffer) + else + decoded = buffer + end + super(items, decoded) + end + + def self.write_items(items, values, buffer) + # Start with an empty object if no buffer + buffer.replace("{}") if buffer.length == 0 or buffer[0] == "\x00" + + # Convert to ruby objects + if String === buffer + decoded = JSON.parse(buffer, :allow_nan => true) + else + decoded = buffer + end + + items.each_with_index do |item, index| + write_item_internal(item, values[index], decoded) + end + + # Update buffer + if String === buffer + buffer.replace(JSON.generate(decoded, :allow_nan => true)) + end + + return buffer + end + + def self.write_item_internal(item, value, decoded) + return nil if item.data_type == :DERIVED + + # Save traversal state + parent_node = nil + parent_key = nil + node = decoded + + # Parse the JsonPath + json_path = JsonPath.new(item.key) + + # Handle each token + json_path.path.each do |token| + case token + when '$' + # Ignore start - it is implied + next + when /\[.*\]/ + # Array or Hash Index + if token.index("'") # Hash index + key = token[2..-3] + if not (Hash === node) + node = {} + parent_node[parent_key] = node + end + parent_node = node + parent_key = key + node = node[key] + else # Array index + key = token[1..-2].to_i + if not (Array === node) + node = [] + parent_node[parent_key] = node + end + parent_node = node + parent_key = key + node = node[key] + end + else + raise "Unsupported key/token: #{item.key} - #{token}" + end + end + if parent_node + parent_node[parent_key] = value + else + decoded.replace(value) + end + return decoded + end + end +end \ No newline at end of file diff --git a/openc3/lib/openc3/accessors/xml_accessor.rb b/openc3/lib/openc3/accessors/xml_accessor.rb new file mode 100644 index 0000000000..ed521eda42 --- /dev/null +++ b/openc3/lib/openc3/accessors/xml_accessor.rb @@ -0,0 +1,67 @@ +# encoding: ascii-8bit + +# Copyright 2022 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +require 'nokogiri' +require 'openc3/accessors/accessor' + +module OpenC3 + class XmlAccessor < Accessor + def self.read_item(item, buffer) + return nil if item.data_type == :DERIVED + doc = buffer_to_doc(buffer) + return convert_to_type(doc.xpath(item.key).first.to_s, item) + end + + def self.write_item(item, value, buffer) + return nil if item.data_type == :DERIVED + doc = buffer_to_doc(buffer) + node = doc.xpath(item.key).first + node.content = value.to_s + buffer.replace(doc_to_buffer(doc)) + end + + def self.read_items(items, buffer) + doc = buffer_to_doc(buffer) + result = {} + items.each do |item| + if item.data_type == :DERIVED + result[item.name] = nil + else + result[item.name] = convert_to_type(doc.xpath(item.key).first.to_s, item) + end + end + return result + end + + def self.write_items(items, values, buffer) + doc = buffer_to_doc(buffer) + items.each_with_index do |item, index| + next if item.data_type == :DERIVED + node = doc.xpath(item.key).first + node.content = values[index].to_s + end + buffer.replace(doc_to_buffer(doc)) + end + + def self.buffer_to_doc(buffer) + Nokogiri.XML(buffer) + end + + def self.doc_to_buffer(doc) + doc.to_xml + end + + end +end \ No newline at end of file diff --git a/openc3/lib/openc3/config/config_parser.rb b/openc3/lib/openc3/config/config_parser.rb index 8d8a364716..a391de5222 100644 --- a/openc3/lib/openc3/config/config_parser.rb +++ b/openc3/lib/openc3/config/config_parser.rb @@ -161,14 +161,20 @@ def render(template_name, options = {}) if options[:locals] options[:locals].each { |key, value| b.local_variable_set(key, value) } end + + return ERB.new(read_file(template_name), trim_mode: "-").result(b) + end + + # Can be called during parsing to read a referenced file + def read_file(filename) # Assume the file is there. If not we raise a pretty obvious error - if File.expand_path(template_name) == template_name # absolute path - path = template_name + if File.expand_path(filename) == filename # absolute path + path = filename else # relative to the current @filename - path = File.join(File.dirname(@filename), template_name) + path = File.join(File.dirname(@filename), filename) end OpenC3.set_working_dir(File.dirname(path)) do - return ERB.new(File.read(path), trim_mode: "-").result(b) + return File.read(path) end end diff --git a/openc3/lib/openc3/models/cvt_model.rb b/openc3/lib/openc3/models/cvt_model.rb index 702fd37632..1127bf2e21 100644 --- a/openc3/lib/openc3/models/cvt_model.rb +++ b/openc3/lib/openc3/models/cvt_model.rb @@ -26,16 +26,7 @@ class CvtModel @overrides = {} def self.build_json_from_packet(packet) - json_hash = {} - packet.sorted_items.each do |item| - json_hash[item.name] = packet.read_item(item, :RAW) - json_hash["#{item.name}__C"] = packet.read_item(item, :CONVERTED) if item.read_conversion or item.states - json_hash["#{item.name}__F"] = packet.read_item(item, :FORMATTED) if item.format_string - json_hash["#{item.name}__U"] = packet.read_item(item, :WITH_UNITS) if item.units - limits_state = item.limits.state - json_hash["#{item.name}__L"] = limits_state if limits_state - end - json_hash + packet.decom end # Delete the current value table for a target diff --git a/openc3/lib/openc3/packets/binary_accessor.rb b/openc3/lib/openc3/packets/binary_accessor.rb index ba457ffdea..bad4e6763e 100644 --- a/openc3/lib/openc3/packets/binary_accessor.rb +++ b/openc3/lib/openc3/packets/binary_accessor.rb @@ -1,1207 +1,2 @@ -# encoding: ascii-8bit - -# Copyright 2022 Ball Aerospace & Technologies Corp. -# All Rights Reserved. -# -# This program is free software; you can modify and/or redistribute it -# under the terms of the GNU Affero General Public License -# as published by the Free Software Foundation; version 3 with -# attribution addendums as found in the LICENSE.txt -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. -# All Rights Reserved - -# This file contains the implementation of the BinaryAccessor class. -# This class allows for easy reading and writing of binary data in Ruby - -require 'openc3/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT'] - -module OpenC3 - # Provides methods for binary reading and writing - class BinaryAccessor - # Constants for ruby packing directives - PACK_8_BIT_INT = 'c' - PACK_NATIVE_16_BIT_INT = 's' - PACK_LITTLE_ENDIAN_16_BIT_UINT = 'v' - PACK_BIG_ENDIAN_16_BIT_UINT = 'n' - PACK_NATIVE_32_BIT_INT = 'l' - PACK_NATIVE_32_BIT_UINT = 'L' - PACK_NATIVE_64_BIT_INT = 'q' - PACK_NATIVE_64_BIT_UINT = 'Q' - PACK_LITTLE_ENDIAN_32_BIT_UINT = 'V' - PACK_BIG_ENDIAN_32_BIT_UINT = 'N' - PACK_LITTLE_ENDIAN_32_BIT_FLOAT = 'e' - PACK_LITTLE_ENDIAN_64_BIT_FLOAT = 'E' - PACK_BIG_ENDIAN_32_BIT_FLOAT = 'g' - PACK_BIG_ENDIAN_64_BIT_FLOAT = 'G' - PACK_NULL_TERMINATED_STRING = 'Z*' - PACK_BLOCK = 'a*' - PACK_8_BIT_INT_ARRAY = 'c*' - PACK_8_BIT_UINT_ARRAY = 'C*' - PACK_NATIVE_16_BIT_INT_ARRAY = 's*' - PACK_BIG_ENDIAN_16_BIT_UINT_ARRAY = 'n*' - PACK_LITTLE_ENDIAN_16_BIT_UINT_ARRAY = 'v*' - PACK_NATIVE_32_BIT_INT_ARRAY = 'l*' - PACK_BIG_ENDIAN_32_BIT_UINT_ARRAY = 'N*' - PACK_LITTLE_ENDIAN_32_BIT_UINT_ARRAY = 'V*' - PACK_NATIVE_64_BIT_INT_ARRAY = 'q*' - PACK_NATIVE_64_BIT_UINT_ARRAY = 'Q*' - PACK_LITTLE_ENDIAN_32_BIT_FLOAT_ARRAY = 'e*' - PACK_LITTLE_ENDIAN_64_BIT_FLOAT_ARRAY = 'E*' - PACK_BIG_ENDIAN_32_BIT_FLOAT_ARRAY = 'g*' - PACK_BIG_ENDIAN_64_BIT_FLOAT_ARRAY = 'G*' - - if RUBY_ENGINE != 'ruby' or ENV['OPENC3_NO_EXT'] - MIN_INT8 = -128 - MAX_INT8 = 127 - MAX_UINT8 = 255 - MIN_INT16 = -32768 - MAX_INT16 = 32767 - MAX_UINT16 = 65535 - MIN_INT32 = -(2**31) - MAX_INT32 = (2**31) - 1 - MAX_UINT32 = (2**32) - 1 - MIN_INT64 = -(2**63) - MAX_INT64 = (2**63) - 1 - MAX_UINT64 = (2**64) - 1 - end - - # Additional Constants - ZERO_STRING = "\000" - - # Valid data types - DATA_TYPES = [:INT, :UINT, :FLOAT, :STRING, :BLOCK] - - # Valid overflow types - OVERFLOW_TYPES = [:TRUNCATE, :SATURATE, :ERROR, :ERROR_ALLOW_HEX] - - protected - - # Determines the endianness of the host running this code - # - # This method is protected to force the use of the constant - # HOST_ENDIANNESS rather than this method - # - # @return [Symbol] :BIG_ENDIAN or :LITTLE_ENDIAN - def self.get_host_endianness - value = 0x01020304 - packed = [value].pack(PACK_NATIVE_32_BIT_UINT) - unpacked = packed.unpack(PACK_LITTLE_ENDIAN_32_BIT_UINT)[0] - if unpacked == value - :LITTLE_ENDIAN - else - :BIG_ENDIAN - end - end - - def self.raise_buffer_error(read_write, buffer, data_type, given_bit_offset, given_bit_size) - raise ArgumentError, "#{buffer.length} byte buffer insufficient to #{read_write} #{data_type} at bit_offset #{given_bit_offset} with bit_size #{given_bit_size}" - end - - public - - # Store the host endianness so that it only has to be determined once - HOST_ENDIANNESS = get_host_endianness() - # Valid endianess - ENDIANNESS = [:BIG_ENDIAN, :LITTLE_ENDIAN] - - if RUBY_ENGINE != 'ruby' or ENV['OPENC3_NO_EXT'] - # Reads binary data of any data type from a buffer - # - # @param bit_offset [Integer] Bit offset to the start of the item. A - # negative number means to offset from the end of the buffer. - # @param bit_size [Integer] Size of the item in bits - # @param data_type [Symbol] {DATA_TYPES} - # @param buffer [String] Binary string buffer to read from - # @param endianness [Symbol] {ENDIANNESS} - # @return [Integer] value read from the buffer - def self.read(bit_offset, bit_size, data_type, buffer, endianness) - given_bit_offset = bit_offset - given_bit_size = bit_size - - bit_offset = check_bit_offset_and_size(:read, given_bit_offset, given_bit_size, data_type, buffer) - - # If passed a negative bit size with strings or blocks - # recalculate based on the buffer length - if (bit_size <= 0) && ((data_type == :STRING) || (data_type == :BLOCK)) - bit_size = (buffer.length * 8) - bit_offset + bit_size - if bit_size == 0 - return "" - elsif bit_size < 0 - raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) - end - end - - result, lower_bound, upper_bound = check_bounds_and_buffer_size(bit_offset, bit_size, buffer.length, endianness, data_type) - raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) unless result - - if (data_type == :STRING) || (data_type == :BLOCK) - ####################################### - # Handle :STRING and :BLOCK data types - ####################################### - - if byte_aligned(bit_offset) - if data_type == :STRING - return buffer[lower_bound..upper_bound].unpack('Z*')[0] - else - return buffer[lower_bound..upper_bound].unpack('a*')[0] - end - else - raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") - end - - elsif (data_type == :INT) || (data_type == :UINT) - ################################### - # Handle :INT and :UINT data types - ################################### - - if byte_aligned(bit_offset) && even_bit_size(bit_size) - - if data_type == :INT - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit :INT - ########################################################### - - case bit_size - when 8 - return buffer[lower_bound].unpack(PACK_8_BIT_INT)[0] - when 16 - if endianness == HOST_ENDIANNESS - return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_16_BIT_INT)[0] - else # endianness != HOST_ENDIANNESS - temp = buffer[lower_bound..upper_bound].reverse - return temp.unpack(PACK_NATIVE_16_BIT_INT)[0] - end - when 32 - if endianness == HOST_ENDIANNESS - return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_32_BIT_INT)[0] - else # endianness != HOST_ENDIANNESS - temp = buffer[lower_bound..upper_bound].reverse - return temp.unpack(PACK_NATIVE_32_BIT_INT)[0] - end - when 64 - if endianness == HOST_ENDIANNESS - return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_INT)[0] - else # endianness != HOST_ENDIANNESS - temp = buffer[lower_bound..upper_bound].reverse - return temp.unpack(PACK_NATIVE_64_BIT_INT)[0] - end - end - else # data_type == :UINT - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit :UINT - ########################################################### - - case bit_size - when 8 - return buffer.getbyte(lower_bound) - when 16 - if endianness == :BIG_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_16_BIT_UINT)[0] - else # endianness == :LITTLE_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_16_BIT_UINT)[0] - end - when 32 - if endianness == :BIG_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_UINT)[0] - else # endianness == :LITTLE_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_UINT)[0] - end - when 64 - if endianness == HOST_ENDIANNESS - return buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_UINT)[0] - else # endianness != HOST_ENDIANNESS - temp = buffer[lower_bound..upper_bound].reverse - return temp.unpack(PACK_NATIVE_64_BIT_UINT)[0] - end - end - end - - else - ########################## - # Handle :INT and :UINT Bitfields - ########################## - - # Extract Data for Bitfield - if endianness == :LITTLE_ENDIAN - # Bitoffset always refers to the most significant bit of a bitfield - num_bytes = (((bit_offset % 8) + bit_size - 1) / 8) + 1 - upper_bound = bit_offset / 8 - lower_bound = upper_bound - num_bytes + 1 - - if lower_bound < 0 - raise(ArgumentError, "LITTLE_ENDIAN bitfield with bit_offset #{given_bit_offset} and bit_size #{given_bit_size} is invalid") - end - - temp_data = buffer[lower_bound..upper_bound].reverse - else - temp_data = buffer[lower_bound..upper_bound] - end - - # Determine temp upper bound - temp_upper = upper_bound - lower_bound - - # Handle Bitfield - start_bits = bit_offset % 8 - start_mask = ~(0xFF << (8 - start_bits)) - total_bits = (temp_upper + 1) * 8 - right_shift = total_bits - start_bits - bit_size - - # Mask off unwanted bits at beginning - temp = temp_data.getbyte(0) & start_mask - - if upper_bound > lower_bound - # Combine bytes into a FixNum - temp_data[1..temp_upper].each_byte { |temp_value| temp = temp << 8; temp = temp + temp_value } - end - - # Shift off unwanted bits at end - temp = temp >> right_shift - - if data_type == :INT - # Convert to negative if necessary - if (bit_size > 1) && (temp[bit_size - 1] == 1) - temp = -((1 << bit_size) - temp) - end - end - - return temp - end - - elsif data_type == :FLOAT - ########################## - # Handle :FLOAT data type - ########################## - - if byte_aligned(bit_offset) - case bit_size - when 32 - if endianness == :BIG_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_FLOAT)[0] - else # endianness == :LITTLE_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT)[0] - end - when 64 - if endianness == :BIG_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_64_BIT_FLOAT)[0] - else # endianness == :LITTLE_ENDIAN - return buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT)[0] - end - else - raise(ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}") - end - else - raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") - end - - else - ############################ - # Handle Unknown data types - ############################ - - raise(ArgumentError, "data_type #{data_type} is not recognized") - end - - return return_value - end - - # Writes binary data of any data type to a buffer - # - # @param value [Varies] Value to write into the buffer - # @param bit_offset [Integer] Bit offset to the start of the item. A - # negative number means to offset from the end of the buffer. - # @param bit_size [Integer] Size of the item in bits - # @param data_type [Symbol] {DATA_TYPES} - # @param buffer [String] Binary string buffer to write to - # @param endianness [Symbol] {ENDIANNESS} - # @param overflow [Symbol] {OVERFLOW_TYPES} - # @return [Integer] value passed in as a parameter - def self.write(value, bit_offset, bit_size, data_type, buffer, endianness, overflow) - given_bit_offset = bit_offset - given_bit_size = bit_size - - bit_offset = check_bit_offset_and_size(:write, given_bit_offset, given_bit_size, data_type, buffer) - - # If passed a negative bit size with strings or blocks - # recalculate based on the value length in bytes - if (bit_size <= 0) && ((data_type == :STRING) || (data_type == :BLOCK)) - value = value.to_s - bit_size = value.length * 8 - end - - result, lower_bound, upper_bound = check_bounds_and_buffer_size(bit_offset, bit_size, buffer.length, endianness, data_type) - raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) if !result && (given_bit_size > 0) - - # Check overflow type - if (overflow != :TRUNCATE) && (overflow != :SATURATE) && (overflow != :ERROR) && (overflow != :ERROR_ALLOW_HEX) - raise(ArgumentError, "unknown overflow type #{overflow}") - end - - if (data_type == :STRING) || (data_type == :BLOCK) - ####################################### - # Handle :STRING and :BLOCK data types - ####################################### - value = value.to_s - - if byte_aligned(bit_offset) - temp = value - if given_bit_size <= 0 - end_bytes = -(given_bit_size / 8) - old_upper_bound = buffer.length - 1 - end_bytes - # Lower bound + end_bytes can never be more than 1 byte outside of the given buffer - if (lower_bound + end_bytes) > buffer.length - raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) - end - - if old_upper_bound < lower_bound - # String was completely empty - if end_bytes > 0 - # Preserve bytes at end of buffer - buffer << "\000" * value.length - buffer[lower_bound + value.length, end_bytes] = buffer[lower_bound, end_bytes] - end - elsif bit_size == 0 - # Remove entire string - buffer[lower_bound, old_upper_bound - lower_bound + 1] = '' - elsif upper_bound < old_upper_bound - # Remove extra bytes from old string - buffer[upper_bound + 1, old_upper_bound - upper_bound] = '' - elsif (upper_bound > old_upper_bound) && (end_bytes > 0) - # Preserve bytes at end of buffer - diff = upper_bound - old_upper_bound - buffer << "\000" * diff - buffer[upper_bound + 1, end_bytes] = buffer[old_upper_bound + 1, end_bytes] - end - else # given_bit_size > 0 - byte_size = bit_size / 8 - if value.length < byte_size - # Pad the requested size with zeros - temp = value.ljust(byte_size, "\000") - elsif value.length > byte_size - if overflow == :TRUNCATE - # Resize the value to fit the field - value[byte_size, value.length - byte_size] = '' - else - raise(ArgumentError, "value of #{value.length} bytes does not fit into #{byte_size} bytes for data_type #{data_type}") - end - end - end - if bit_size != 0 - buffer[lower_bound, temp.length] = temp - end - else - raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") - end - - elsif (data_type == :INT) || (data_type == :UINT) - ################################### - # Handle :INT data type - ################################### - value = Integer(value) - min_value, max_value, hex_max_value = get_check_overflow_ranges(bit_size, data_type) - value = check_overflow(value, min_value, max_value, hex_max_value, bit_size, data_type, overflow) - - if byte_aligned(bit_offset) && even_bit_size(bit_size) - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit - ########################################################### - - if data_type == :INT - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit :INT - ########################################################### - - case bit_size - when 8 - buffer.setbyte(lower_bound, value) - when 16 - if endianness == HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_16_BIT_INT) - else # endianness != HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_16_BIT_INT).reverse - end - when 32 - if endianness == HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_32_BIT_INT) - else # endianness != HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_32_BIT_INT).reverse - end - when 64 - if endianness == HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_INT) - else # endianness != HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_INT).reverse - end - end - else # data_type == :UINT - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit :UINT - ########################################################### - - case bit_size - when 8 - buffer.setbyte(lower_bound, value) - when 16 - if endianness == :BIG_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_16_BIT_UINT) - else # endianness == :LITTLE_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_16_BIT_UINT) - end - when 32 - if endianness == :BIG_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_32_BIT_UINT) - else # endianness == :LITTLE_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_32_BIT_UINT) - end - when 64 - if endianness == HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_UINT) - else # endianness != HOST_ENDIANNESS - buffer[lower_bound..upper_bound] = [value].pack(PACK_NATIVE_64_BIT_UINT).reverse - end - end - end - - else - ########################################################### - # Handle bit fields - ########################################################### - - # Extract Existing Data - if endianness == :LITTLE_ENDIAN - # Bitoffset always refers to the most significant bit of a bitfield - num_bytes = (((bit_offset % 8) + bit_size - 1) / 8) + 1 - upper_bound = bit_offset / 8 - lower_bound = upper_bound - num_bytes + 1 - if lower_bound < 0 - raise(ArgumentError, "LITTLE_ENDIAN bitfield with bit_offset #{given_bit_offset} and bit_size #{given_bit_size} is invalid") - end - - temp_data = buffer[lower_bound..upper_bound].reverse - else - temp_data = buffer[lower_bound..upper_bound] - end - - # Determine temp upper bound - temp_upper = upper_bound - lower_bound - - # Determine Values needed to Handle Bitfield - start_bits = bit_offset % 8 - start_mask = (0xFF << (8 - start_bits)) - total_bits = (temp_upper + 1) * 8 - end_bits = total_bits - start_bits - bit_size - end_mask = ~(0xFF << end_bits) - - # Add in Start Bits - temp = temp_data.getbyte(0) & start_mask - - # Adjust value to correct number of bits - temp_mask = (2**bit_size) - 1 - temp_value = value & temp_mask - - # Add in New Data - temp = (temp << (bit_size - (8 - start_bits))) + temp_value - - # Add in Remainder of Existing Data - temp = (temp << end_bits) + (temp_data.getbyte(temp_upper) & end_mask) - - # Extract into an array of bytes - temp_array = [] - (0..temp_upper).each { temp_array.insert(0, (temp & 0xFF)); temp = temp >> 8 } - - # Store into data - if endianness == :LITTLE_ENDIAN - buffer[lower_bound..upper_bound] = temp_array.pack(PACK_8_BIT_UINT_ARRAY).reverse - else - buffer[lower_bound..upper_bound] = temp_array.pack(PACK_8_BIT_UINT_ARRAY) - end - - end - - elsif data_type == :FLOAT - ########################## - # Handle :FLOAT data type - ########################## - value = Float(value) - - if byte_aligned(bit_offset) - case bit_size - when 32 - if endianness == :BIG_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_32_BIT_FLOAT) - else # endianness == :LITTLE_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT) - end - when 64 - if endianness == :BIG_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_BIG_ENDIAN_64_BIT_FLOAT) - else # endianness == :LITTLE_ENDIAN - buffer[lower_bound..upper_bound] = [value].pack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT) - end - else - raise(ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}") - end - else - raise(ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}") - end - - else - ############################ - # Handle Unknown data types - ############################ - - raise(ArgumentError, "data_type #{data_type} is not recognized") - end - - return value - end - - protected - - # Check the bit size and bit offset for problems. Recalulate the bit offset - # and return back through the passed in pointer. - def self.check_bit_offset_and_size(read_or_write, given_bit_offset, given_bit_size, data_type, buffer) - bit_offset = given_bit_offset - - if (given_bit_size <= 0) && (data_type != :STRING) && (data_type != :BLOCK) - raise(ArgumentError, "bit_size #{given_bit_size} must be positive for data types other than :STRING and :BLOCK") - end - - if (given_bit_size <= 0) && (given_bit_offset < 0) - raise(ArgumentError, "negative or zero bit_sizes (#{given_bit_size}) cannot be given with negative bit_offsets (#{given_bit_offset})") - end - - if given_bit_offset < 0 - bit_offset = (buffer.length * 8) + bit_offset - if bit_offset < 0 - raise_buffer_error(read_or_write, buffer, data_type, given_bit_offset, given_bit_size) - end - end - - return bit_offset - end - - # Calculate the bounds of the string to access the item based on the bit_offset and bit_size. - # Also determine if the buffer size is sufficient. - def self.check_bounds_and_buffer_size(bit_offset, bit_size, buffer_length, endianness, data_type) - result = true # Assume ok - - # Define bounds of string to access this item - lower_bound = bit_offset / 8 - upper_bound = (bit_offset + bit_size - 1) / 8 - - # Sanity check buffer size - if upper_bound >= buffer_length - # If it's not the special case of little endian bit field then we fail and return false - if !((endianness == :LITTLE_ENDIAN) && - ((data_type == :INT) || (data_type == :UINT)) && - # Not byte aligned with an even bit size - (!((byte_aligned(bit_offset)) && (even_bit_size(bit_size)))) && - (lower_bound < buffer_length) - ) - result = false - end - end - return result, lower_bound, upper_bound - end - - def self.get_check_overflow_ranges(bit_size, data_type) - min_value = 0 # Default for UINT cases - - case bit_size - when 8 - hex_max_value = MAX_UINT8 - if data_type == :INT - min_value = MIN_INT8 - max_value = MAX_INT8 - else - max_value = MAX_UINT8 - end - when 16 - hex_max_value = MAX_UINT16 - if data_type == :INT - min_value = MIN_INT16 - max_value = MAX_INT16 - else - max_value = MAX_UINT16 - end - when 32 - hex_max_value = MAX_UINT32 - if data_type == :INT - min_value = MIN_INT32 - max_value = MAX_INT32 - else - max_value = MAX_UINT32 - end - when 64 - hex_max_value = MAX_UINT64 - if data_type == :INT - min_value = MIN_INT64 - max_value = MAX_INT64 - else - max_value = MAX_UINT64 - end - else # Bitfield - if data_type == :INT - # Note signed integers must allow up to the maximum unsigned value to support values given in hex - if bit_size > 1 - max_value = 2**(bit_size - 1) - # min_value = -(2 ** bit_size - 1) - min_value = -max_value - # max_value = (2 ** bit_size - 1) - 1 - max_value -= 1 - # hex_max_value = (2 ** bit_size) - 1 - hex_max_value = (2**bit_size) - 1 - else # 1-bit signed - min_value = -1 - max_value = 1 - hex_max_value = 1 - end - else - max_value = (2**bit_size) - 1 - hex_max_value = max_value - end - end - - return min_value, max_value, hex_max_value - end - - def self.byte_aligned(value) - (value % 8) == 0 - end - - def self.even_bit_size(bit_size) - (bit_size == 8) || (bit_size == 16) || (bit_size == 32) || (bit_size == 64) - end - - public - - end - - # Reads an array of binary data of any data type from a buffer - # - # @param bit_offset [Integer] Bit offset to the start of the array. A - # negative number means to offset from the end of the buffer. - # @param bit_size [Integer] Size of each item in the array in bits - # @param data_type [Symbol] {DATA_TYPES} - # @param array_size [Integer] Size in bits of the array. 0 or negative means - # fill the array with as many bit_size number of items that exist (negative - # means excluding the final X number of bits). - # @param buffer [String] Binary string buffer to read from - # @param endianness [Symbol] {ENDIANNESS} - # @return [Array] Array created from reading the buffer - def self.read_array(bit_offset, bit_size, data_type, array_size, buffer, endianness) - # Save given values of bit offset, bit size, and array_size - given_bit_offset = bit_offset - given_bit_size = bit_size - given_array_size = array_size - - # Handle negative and zero bit sizes - raise ArgumentError, "bit_size #{given_bit_size} must be positive for arrays" if bit_size <= 0 - - # Handle negative bit offsets - if bit_offset < 0 - bit_offset = ((buffer.length * 8) + bit_offset) - raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) if bit_offset < 0 - end - - # Handle negative and zero array sizes - if array_size <= 0 - if given_bit_offset < 0 - raise ArgumentError, "negative or zero array_size (#{given_array_size}) cannot be given with negative bit_offset (#{given_bit_offset})" - else - array_size = ((buffer.length * 8) - bit_offset + array_size) - if array_size == 0 - return [] - elsif array_size < 0 - raise_buffer_error(:read, buffer, data_type, given_bit_offset, given_bit_size) - end - end - end - - # Calculate number of items in the array - # If there is a remainder then we have a problem - raise ArgumentError, "array_size #{given_array_size} not a multiple of bit_size #{given_bit_size}" if array_size % bit_size != 0 - - num_items = array_size / bit_size - - # Define bounds of string to access this item - lower_bound = bit_offset / 8 - upper_bound = (bit_offset + array_size - 1) / 8 - - # Check for byte alignment - byte_aligned = ((bit_offset % 8) == 0) - - case data_type - when :STRING, :BLOCK - ####################################### - # Handle :STRING and :BLOCK data types - ####################################### - - if byte_aligned - value = [] - num_items.times do - value << self.read(bit_offset, bit_size, data_type, buffer, endianness) - bit_offset += bit_size - end - else - raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" - end - - when :INT, :UINT - ################################### - # Handle :INT and :UINT data types - ################################### - - if byte_aligned and (bit_size == 8 or bit_size == 16 or bit_size == 32 or bit_size == 64) - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit :INT and :UINT - ########################################################### - - case bit_size - when 8 - if data_type == :INT - value = buffer[lower_bound..upper_bound].unpack(PACK_8_BIT_INT_ARRAY) - else # data_type == :UINT - value = buffer[lower_bound..upper_bound].unpack(PACK_8_BIT_UINT_ARRAY) - end - - when 16 - if data_type == :INT - if endianness == HOST_ENDIANNESS - value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_16_BIT_INT_ARRAY) - else # endianness != HOST_ENDIANNESS - temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 2) - value = temp.to_s.unpack(PACK_NATIVE_16_BIT_INT_ARRAY) - end - else # data_type == :UINT - if endianness == :BIG_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_16_BIT_UINT_ARRAY) - else # endianness == :LITTLE_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_16_BIT_UINT_ARRAY) - end - end - - when 32 - if data_type == :INT - if endianness == HOST_ENDIANNESS - value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_32_BIT_INT_ARRAY) - else # endianness != HOST_ENDIANNESS - temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 4) - value = temp.to_s.unpack(PACK_NATIVE_32_BIT_INT_ARRAY) - end - else # data_type == :UINT - if endianness == :BIG_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_UINT_ARRAY) - else # endianness == :LITTLE_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_UINT_ARRAY) - end - end - - when 64 - if data_type == :INT - if endianness == HOST_ENDIANNESS - value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_INT_ARRAY) - else # endianness != HOST_ENDIANNESS - temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 8) - value = temp.to_s.unpack(PACK_NATIVE_64_BIT_INT_ARRAY) - end - else # data_type == :UINT - if endianness == HOST_ENDIANNESS - value = buffer[lower_bound..upper_bound].unpack(PACK_NATIVE_64_BIT_UINT_ARRAY) - else # endianness != HOST_ENDIANNESS - temp = self.byte_swap_buffer(buffer[lower_bound..upper_bound], 8) - value = temp.to_s.unpack(PACK_NATIVE_64_BIT_UINT_ARRAY) - end - end - end - - else - ################################## - # Handle :INT and :UINT Bitfields - ################################## - raise ArgumentError, "read_array does not support little endian bit fields with bit_size greater than 1-bit" if endianness == :LITTLE_ENDIAN and bit_size > 1 - - value = [] - num_items.times do - value << self.read(bit_offset, bit_size, data_type, buffer, endianness) - bit_offset += bit_size - end - end - - when :FLOAT - ########################## - # Handle :FLOAT data type - ########################## - - if byte_aligned - case bit_size - when 32 - if endianness == :BIG_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_32_BIT_FLOAT_ARRAY) - else # endianness == :LITTLE_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT_ARRAY) - end - - when 64 - if endianness == :BIG_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_BIG_ENDIAN_64_BIT_FLOAT_ARRAY) - else # endianness == :LITTLE_ENDIAN - value = buffer[lower_bound..upper_bound].unpack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT_ARRAY) - end - - else - raise ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}" - end - - else - raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" - end - - else - ############################ - # Handle Unknown data types - ############################ - - raise ArgumentError, "data_type #{data_type} is not recognized" - end - - value - end # def read_array - - # Writes an array of binary data of any data type to a buffer - # - # @param values [Array] Values to write into the buffer - # @param bit_offset [Integer] Bit offset to the start of the array. A - # negative number means to offset from the end of the buffer. - # @param bit_size [Integer] Size of each item in the array in bits - # @param data_type [Symbol] {DATA_TYPES} - # @param array_size [Integer] Size in bits of the array as represented in the buffer. - # Size 0 means to fill the buffer with as many bit_size number of items that exist - # (negative means excluding the final X number of bits). - # @param buffer [String] Binary string buffer to write to - # @param endianness [Symbol] {ENDIANNESS} - # @return [Array] values passed in as a parameter - def self.write_array(values, bit_offset, bit_size, data_type, array_size, buffer, endianness, overflow) - # Save given values of bit offset, bit size, and array_size - given_bit_offset = bit_offset - given_bit_size = bit_size - given_array_size = array_size - - # Verify an array was given - raise ArgumentError, "values must be an Array type class is #{values.class}" unless values.kind_of? Array - - # Handle negative and zero bit sizes - raise ArgumentError, "bit_size #{given_bit_size} must be positive for arrays" if bit_size <= 0 - - # Handle negative bit offsets - if bit_offset < 0 - bit_offset = ((buffer.length * 8) + bit_offset) - raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) if bit_offset < 0 - end - - # Handle negative and zero array sizes - if array_size <= 0 - if given_bit_offset < 0 - raise ArgumentError, "negative or zero array_size (#{given_array_size}) cannot be given with negative bit_offset (#{given_bit_offset})" - else - end_bytes = -(given_array_size / 8) - lower_bound = bit_offset / 8 - upper_bound = (bit_offset + (bit_size * values.length) - 1) / 8 - old_upper_bound = buffer.length - 1 - end_bytes - - if upper_bound < old_upper_bound - # Remove extra bytes from old buffer - buffer[(upper_bound + 1)..old_upper_bound] = '' - elsif upper_bound > old_upper_bound - # Grow buffer and preserve bytes at end of buffer if necesssary - buffer_length = buffer.length - diff = upper_bound - old_upper_bound - buffer << ZERO_STRING * diff - if end_bytes > 0 - buffer[(upper_bound + 1)..(buffer.length - 1)] = buffer[(old_upper_bound + 1)..(buffer_length - 1)] - end - end - - array_size = ((buffer.length * 8) - bit_offset + array_size) - end - end - - # Get data bounds for this array - lower_bound = bit_offset / 8 - upper_bound = (bit_offset + array_size - 1) / 8 - num_bytes = upper_bound - lower_bound + 1 - - # Check for byte alignment - byte_aligned = ((bit_offset % 8) == 0) - - # Calculate the number of writes - num_writes = array_size / bit_size - # Check for a negative array_size and adjust the number of writes - # to simply be the number of values in the passed in array - if given_array_size <= 0 - num_writes = values.length - end - - # Ensure the buffer has enough room - if bit_offset + num_writes * bit_size > buffer.length * 8 - raise_buffer_error(:write, buffer, data_type, given_bit_offset, given_bit_size) - end - - # Ensure the given_array_size is an even multiple of bit_size - raise ArgumentError, "array_size #{given_array_size} not a multiple of bit_size #{given_bit_size}" if array_size % bit_size != 0 - - raise ArgumentError, "too many values #{values.length} for given array_size #{given_array_size} and bit_size #{given_bit_size}" if num_writes < values.length - - # Check overflow type - raise "unknown overflow type #{overflow}" unless OVERFLOW_TYPES.include?(overflow) - - case data_type - when :STRING, :BLOCK - ####################################### - # Handle :STRING and :BLOCK data types - ####################################### - - if byte_aligned - num_writes.times do |index| - self.write(values[index], bit_offset, bit_size, data_type, buffer, endianness, overflow) - bit_offset += bit_size - end - else - raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" - end - - when :INT, :UINT - ################################### - # Handle :INT and :UINT data types - ################################### - - if byte_aligned and (bit_size == 8 or bit_size == 16 or bit_size == 32 or bit_size == 64) - ########################################################### - # Handle byte-aligned 8, 16, 32, and 64 bit :INT and :UINT - ########################################################### - - case bit_size - when 8 - if data_type == :INT - values = self.check_overflow_array(values, MIN_INT8, MAX_INT8, MAX_UINT8, bit_size, data_type, overflow) - packed = values.pack(PACK_8_BIT_INT_ARRAY) - else # data_type == :UINT - values = self.check_overflow_array(values, 0, MAX_UINT8, MAX_UINT8, bit_size, data_type, overflow) - packed = values.pack(PACK_8_BIT_UINT_ARRAY) - end - - when 16 - if data_type == :INT - values = self.check_overflow_array(values, MIN_INT16, MAX_INT16, MAX_UINT16, bit_size, data_type, overflow) - if endianness == HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_16_BIT_INT_ARRAY) - else # endianness != HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_16_BIT_INT_ARRAY) - self.byte_swap_buffer!(packed, 2) - end - else # data_type == :UINT - values = self.check_overflow_array(values, 0, MAX_UINT16, MAX_UINT16, bit_size, data_type, overflow) - if endianness == :BIG_ENDIAN - packed = values.pack(PACK_BIG_ENDIAN_16_BIT_UINT_ARRAY) - else # endianness == :LITTLE_ENDIAN - packed = values.pack(PACK_LITTLE_ENDIAN_16_BIT_UINT_ARRAY) - end - end - - when 32 - if data_type == :INT - values = self.check_overflow_array(values, MIN_INT32, MAX_INT32, MAX_UINT32, bit_size, data_type, overflow) - if endianness == HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_32_BIT_INT_ARRAY) - else # endianness != HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_32_BIT_INT_ARRAY) - self.byte_swap_buffer!(packed, 4) - end - else # data_type == :UINT - values = self.check_overflow_array(values, 0, MAX_UINT32, MAX_UINT32, bit_size, data_type, overflow) - if endianness == :BIG_ENDIAN - packed = values.pack(PACK_BIG_ENDIAN_32_BIT_UINT_ARRAY) - else # endianness == :LITTLE_ENDIAN - packed = values.pack(PACK_LITTLE_ENDIAN_32_BIT_UINT_ARRAY) - end - end - - when 64 - if data_type == :INT - values = self.check_overflow_array(values, MIN_INT64, MAX_INT64, MAX_UINT64, bit_size, data_type, overflow) - if endianness == HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_64_BIT_INT_ARRAY) - else # endianness != HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_64_BIT_INT_ARRAY) - self.byte_swap_buffer!(packed, 8) - end - else # data_type == :UINT - values = self.check_overflow_array(values, 0, MAX_UINT64, MAX_UINT64, bit_size, data_type, overflow) - if endianness == HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_64_BIT_UINT_ARRAY) - else # endianness != HOST_ENDIANNESS - packed = values.pack(PACK_NATIVE_64_BIT_UINT_ARRAY) - self.byte_swap_buffer!(packed, 8) - end - end - end - - # Adjust packed size to hold number of items written - buffer[lower_bound..upper_bound] = adjust_packed_size(num_bytes, packed) if num_bytes > 0 - - else - ################################## - # Handle :INT and :UINT Bitfields - ################################## - - raise ArgumentError, "write_array does not support little endian bit fields with bit_size greater than 1-bit" if endianness == :LITTLE_ENDIAN and bit_size > 1 - - num_writes.times do |index| - self.write(values[index], bit_offset, bit_size, data_type, buffer, endianness, overflow) - bit_offset += bit_size - end - end - - when :FLOAT - ########################## - # Handle :FLOAT data type - ########################## - - if byte_aligned - case bit_size - when 32 - if endianness == :BIG_ENDIAN - packed = values.pack(PACK_BIG_ENDIAN_32_BIT_FLOAT_ARRAY) - else # endianness == :LITTLE_ENDIAN - packed = values.pack(PACK_LITTLE_ENDIAN_32_BIT_FLOAT_ARRAY) - end - - when 64 - if endianness == :BIG_ENDIAN - packed = values.pack(PACK_BIG_ENDIAN_64_BIT_FLOAT_ARRAY) - else # endianness == :LITTLE_ENDIAN - packed = values.pack(PACK_LITTLE_ENDIAN_64_BIT_FLOAT_ARRAY) - end - - else - raise ArgumentError, "bit_size is #{given_bit_size} but must be 32 or 64 for data_type #{data_type}" - end - - # Adjust packed size to hold number of items written - buffer[lower_bound..upper_bound] = adjust_packed_size(num_bytes, packed) if num_bytes > 0 - - else - raise ArgumentError, "bit_offset #{given_bit_offset} is not byte aligned for data_type #{data_type}" - end - - else - ############################ - # Handle Unknown data types - ############################ - raise ArgumentError, "data_type #{data_type} is not recognized" - end # case data_type - - values - end # def write_array - - # Adjusts the packed array to be the given number of bytes - # - # @param num_bytes [Integer] The desired number of bytes - # @param packed [Array] The packed data buffer - def self.adjust_packed_size(num_bytes, packed) - difference = num_bytes - packed.length - if difference > 0 - packed << (ZERO_STRING * difference) - elsif difference < 0 - packed = packed[0..(packed.length - 1 + difference)] - end - packed - end - - # Byte swaps every X bytes of data in a buffer overwriting the buffer - # - # @param buffer [String] Buffer to modify - # @param num_bytes_per_word [Integer] Number of bytes per word that will be swapped - # @return [String] buffer passed in as a parameter - def self.byte_swap_buffer!(buffer, num_bytes_per_word) - num_swaps = buffer.length / num_bytes_per_word - index = 0 - num_swaps.times do - range = index..(index + num_bytes_per_word - 1) - buffer[range] = buffer[range].reverse - index += num_bytes_per_word - end - buffer - end - - # Byte swaps every X bytes of data in a buffer into a new buffer - # - # @param buffer [String] Buffer that will be copied then modified - # @param num_bytes_per_word [Integer] Number of bytes per word that will be swapped - # @return [String] modified buffer - def self.byte_swap_buffer(buffer, num_bytes_per_word) - buffer = buffer.clone - self.byte_swap_buffer!(buffer, num_bytes_per_word) - end - - # Checks for overflow of an integer data type - # - # @param value [Integer] Value to write into the buffer - # @param min_value [Integer] Minimum allowed value - # @param max_value [Integer] Maximum allowed value - # @param hex_max_value [Integer] Maximum allowed value if specified in hex - # @param bit_size [Integer] Size of the item in bits - # @param data_type [Symbol] {DATA_TYPES} - # @param overflow [Symbol] {OVERFLOW_TYPES} - # @return [Integer] Potentially modified value - def self.check_overflow(value, min_value, max_value, hex_max_value, bit_size, data_type, overflow) - if overflow == :TRUNCATE - # Note this will always convert to unsigned equivalent for signed integers - value = value % (hex_max_value + 1) - else - if value > max_value - if overflow == :SATURATE - value = max_value - else - if overflow == :ERROR or value > hex_max_value - raise ArgumentError, "value of #{value} invalid for #{bit_size}-bit #{data_type}" - end - end - elsif value < min_value - if overflow == :SATURATE - value = min_value - else - raise ArgumentError, "value of #{value} invalid for #{bit_size}-bit #{data_type}" - end - end - end - value - end - - # Checks for overflow of an array of integer data types - # - # @param values [Array[Integer]] Values to write into the buffer - # @param min_value [Integer] Minimum allowed value - # @param max_value [Integer] Maximum allowed value - # @param hex_max_value [Integer] Maximum allowed value if specified in hex - # @param bit_size [Integer] Size of the item in bits - # @param data_type [Symbol] {DATA_TYPES} - # @param overflow [Symbol] {OVERFLOW_TYPES} - # @return [Array[Integer]] Potentially modified values - def self.check_overflow_array(values, min_value, max_value, hex_max_value, bit_size, data_type, overflow) - if overflow != :TRUNCATE - values.each_with_index do |value, index| - values[index] = check_overflow(value, min_value, max_value, hex_max_value, bit_size, data_type, overflow) - end - end - values - end - end # class BinaryAccessor -end +# This file remains here for backwards compatibility +require 'openc3/accessors/binary_accessor' \ No newline at end of file diff --git a/openc3/lib/openc3/packets/packet.rb b/openc3/lib/openc3/packets/packet.rb index 3a4abbc962..b068fcffce 100644 --- a/openc3/lib/openc3/packets/packet.rb +++ b/openc3/lib/openc3/packets/packet.rb @@ -21,6 +21,7 @@ require 'openc3/packets/structure' require 'openc3/packets/packet_item' require 'openc3/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT'] +require 'base64' module OpenC3 # Adds features common to all OpenC3 packets of data to the Structure class. @@ -85,6 +86,9 @@ class Packet < Structure # @return [Symbol] :CMD or :TLM attr_accessor :cmd_or_tlm + # @return [String] Base data for building packet + attr_reader :template + # Valid format types VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED, :WITH_UNITS] @@ -98,7 +102,7 @@ class Packet < Structure # @param buffer [String] String buffer to hold the packet data # @param item_class [Class] Class used to instantiate items (Must be a # subclass of PacketItem) - def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, description = nil, buffer = nil, item_class = PacketItem) + def initialize(target_name = nil, packet_name = nil, default_endianness = :BIG_ENDIAN, description = nil, buffer = nil, item_class = PacketItem) super(default_endianness, buffer, item_class) # Explictly call the defined setter methods self.target_name = target_name @@ -123,6 +127,7 @@ def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, descr @stored = false @extra = nil @cmd_or_tlm = nil + @template = nil end # Sets the target name this packet is associated with. Unidentified packets @@ -290,7 +295,9 @@ def buffer=(buffer) begin internal_buffer_equals(buffer) rescue RuntimeError - Logger.instance.error "#{@target_name} #{@packet_name} received with actual packet length of #{buffer.length} but defined length of #{@defined_length}" + if BinaryAccessor == @accessor + Logger.instance.error "#{@target_name} #{@packet_name} received with actual packet length of #{buffer.length} but defined length of #{@defined_length}" + end end @read_conversion_cache.clear if @read_conversion_cache process() @@ -353,6 +360,20 @@ def limits_change_callback=(limits_change_callback) end end + # Sets the template data for the packet + # Template data is used as the default buffer contents if provided + # + # @param hazardous_description [String] Hazardous description of the packet + def template=(template) + if template + raise ArgumentError, "template must be a String but is a #{template.class}" unless String === template + + @template = template.freeze + else + @template = nil + end + end + # Review bit offset to look for overlapping definitions. This will allow # gaps in the packet, but not allow the same bits to be used for multiple # variables. @@ -531,11 +552,16 @@ def get_item(name) # @param value_type [Symbol] How to convert the item before returning it. # Must be one of {VALUE_TYPES} # @param buffer (see Structure#read_item) + # @param given_raw Given raw value to optimize # @return The value. :FORMATTED and :WITH_UNITS values are always returned # as Strings. :RAW values will match their data_type. :CONVERTED values # can be any type. - def read_item(item, value_type = :CONVERTED, buffer = @buffer) - value = super(item, :RAW, buffer) + def read_item(item, value_type = :CONVERTED, buffer = @buffer, given_raw = nil) + if given_raw + value = given_raw + else + value = super(item, :RAW, buffer) + end derived_raw = false if item.data_type == :DERIVED && value_type == :RAW value_type = :CONVERTED @@ -632,6 +658,31 @@ def read_item(item, value_type = :CONVERTED, buffer = @buffer) return value end + # Read a list of items in the structure + # + # @param items [StructureItem] Array of PacketItem or one of its subclasses + # @param value_type [Symbol] Value type to read for every item + # @param buffer [String] The binary buffer to read the items from + # @return Hash of read names and values + def read_items(items, value_type = :RAW, buffer = @buffer, raw_value = nil) + buffer = allocate_buffer_if_needed() unless buffer + if value_type == :RAW + result = super(items, value_type, buffer) + # Must handle DERIVED special + items.each do |item| + if item.data_type == :DERIVED + result[item.name] = read_item(item, value_type, buffer) + end + end + else + result = {} + items.each do |item| + result[item.name] = read_item(item, value_type, buffer) + end + end + return result + end + # Write an item in the packet # # @param item [PacketItem] Instance of PacketItem or one of its subclasses @@ -674,6 +725,24 @@ def write_item(item, value, value_type = :CONVERTED, buffer = @buffer) end end + # Write values to the buffer based on the item definitions + # + # @param items [StructureItem] Array of StructureItem or one of its subclasses + # @param value [Object] Array of values based on the item definitions. + # @param value_type [Symbol] Value type of each item to write + # @param buffer [String] The binary buffer to write the values to + def write_items(items, values, value_type = :RAW, buffer = @buffer) + buffer = allocate_buffer_if_needed() unless buffer + if value_type == :RAW + return super(items, values, value_type, buffer) + else + items.each_with_index do |item, index| + write_item(item, values[index], value_type, buffer) + end + end + return buffer + end + # Read an item in the packet by name # # @param name [String] Name of the item to read @@ -738,12 +807,17 @@ def formatted(value_type = :CONVERTED, indent = 0, buffer = @buffer, ignored = n # # @param buffer [String] Raw buffer of binary data # @param skip_item_names [Array] Array of item names to skip - def restore_defaults(buffer = @buffer, skip_item_names = nil) + # @param use_templase [Boolean] Apply template before setting defaults (or not) + def restore_defaults(buffer = @buffer, skip_item_names = nil, use_template = true) + buffer = allocate_buffer_if_needed() unless buffer upcase_skip_item_names = skip_item_names.map(&:upcase) if skip_item_names + buffer.replace(@template) if @template and use_template @sorted_items.each do |item| next if RESERVED_ITEM_NAMES.include?(item.name) - write_item(item, item.default, :CONVERTED, buffer) unless skip_item_names and upcase_skip_item_names.include?(item.name) + unless item.default.nil? + write_item(item, item.default, :CONVERTED, buffer) unless skip_item_names and upcase_skip_item_names.include?(item.name) + end end end @@ -986,6 +1060,8 @@ def as_json(*a) config['disabled'] = true if @disabled config['hidden'] = true if @hidden config['stale'] = true if @stale + config['accessor'] = @accessor.to_s + config['template'] = Base64.encode64(@template) if @template if @processors processors = [] @@ -1024,6 +1100,14 @@ def self.from_json(hash) packet.disabled = hash['disabled'] packet.hidden = hash['hidden'] # packet.stale is read only + if hash['accessor'] + begin + packet.accessor = OpenC3::const_get(hash['accessor']) + rescue => error + Logger.instance.error "#{packet.target_name} #{packet.packet_name} accessor of #{hash['accessor']} could not be found due to #{error}" + end + end + packet.template = Base64.decode64(hash['template']) if hash['template'] packet.meta = hash['meta'] # Can't convert processors hash['items'].each do |item| @@ -1032,6 +1116,22 @@ def self.from_json(hash) packet end + def decom + # Read all the RAW at once because this could be optimized by the accessor + json_hash = read_items(@sorted_items) + + # Now read all other value types - no accessor required + @sorted_items.each do |item| + given_raw = json_hash[item.name] + json_hash["#{item.name}__C"] = read_item(item, :CONVERTED, @buffer, given_raw) if item.states or (item.read_conversion and item.data_type != :DERIVED) + json_hash["#{item.name}__F"] = read_item(item, :FORMATTED, @buffer, given_raw) if item.format_string + json_hash["#{item.name}__U"] = read_item(item, :WITH_UNITS, @buffer, given_raw) if item.units + limits_state = item.limits.state + json_hash["#{item.name}__L"] = limits_state if limits_state + end + json_hash + end + protected # Performs packet specific processing on the packet. diff --git a/openc3/lib/openc3/packets/packet_config.rb b/openc3/lib/openc3/packets/packet_config.rb index 5a7c658c94..bb27c3c635 100644 --- a/openc3/lib/openc3/packets/packet_config.rb +++ b/openc3/lib/openc3/packets/packet_config.rb @@ -209,7 +209,7 @@ def process_file(filename, process_target_name) 'PARAMETER', 'ID_ITEM', 'ID_PARAMETER', 'ARRAY_ITEM', 'ARRAY_PARAMETER', 'APPEND_ITEM',\ 'APPEND_PARAMETER', 'APPEND_ID_ITEM', 'APPEND_ID_PARAMETER', 'APPEND_ARRAY_ITEM',\ 'APPEND_ARRAY_PARAMETER', 'ALLOW_SHORT', 'HAZARDOUS', 'PROCESSOR', 'META',\ - 'DISABLE_MESSAGES', 'HIDDEN', 'DISABLED' + 'DISABLE_MESSAGES', 'HIDDEN', 'DISABLED', 'ACCESSOR', 'TEMPLATE', 'TEMPLATE_FILE' raise parser.error("No current packet for #{keyword}") unless @current_packet process_current_packet(parser, keyword, params) @@ -221,7 +221,7 @@ def process_file(filename, process_target_name) 'POLY_WRITE_CONVERSION', 'SEG_POLY_READ_CONVERSION', 'SEG_POLY_WRITE_CONVERSION',\ 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'REQUIRED',\ 'LIMITS', 'LIMITS_RESPONSE', 'UNITS', 'FORMAT_STRING', 'DESCRIPTION',\ - 'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP' + 'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY' raise parser.error("No current item for #{keyword}") unless @current_item process_current_item(parser, keyword, params) @@ -427,7 +427,30 @@ def process_current_packet(parser, keyword, params) parser.verify_num_parameters(0, 0, usage) @current_packet.hidden = true @current_packet.disabled = true + when 'ACCESSOR' + usage = "#{keyword} " + parser.verify_num_parameters(1, 1, usage) + begin + klass = OpenC3.require_class(params[0]) + @current_packet.accessor = klass + rescue Exception => err + raise parser.error(err) + end + + when 'TEMPLATE' + usage = "#{keyword}