diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/CMakeLists.txt b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/CMakeLists.txt new file mode 100644 index 0000000..574f6cb --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.28) +project(librmcs_wrapper VERSION 3 LANGUAGES C CXX) +set(LIBRMCS_PROJECT_VERSION "3.0.1-0.dev.4.gbaf538b") +set(LIBRMCS_DEBIAN_VERSION "3.0.1~0.dev.4.gbaf538b") +add_subdirectory(host) diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/CMakePresets.json b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/CMakePresets.json new file mode 100644 index 0000000..a5bc2fa --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 4, + "include": [ + "host/CMakePresets.json" + ] +} diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/LICENSE b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/include/librmcs/data/datas.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/include/librmcs/data/datas.hpp new file mode 100644 index 0000000..1c13254 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/include/librmcs/data/datas.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +namespace librmcs::data { + +enum class DataId : uint8_t { + kExtend = 0, + + kGpio = 1, + + kCan0 = 2, + kCan1 = 3, + kCan2 = 4, + kCan3 = 5, + kCan4 = 6, + kCan5 = 7, + kCan6 = 8, + kCan7 = 9, + + kUartDbus = 10, + kUart0 = 11, + kUart1 = 12, + kUart2 = 13, + kUart3 = 14, + + kImu = 15, +}; + +struct CanDataView { + uint32_t can_id; + std::span can_data; + bool is_fdcan = false; + bool is_extended_can_id = false; + bool is_remote_transmission = false; +}; + +struct UartDataView { + std::span uart_data; + bool idle_delimited = false; +}; + +struct GpioDigitalDataView { + uint8_t channel; + bool high; +}; + +struct GpioAnalogDataView { + uint8_t channel; + uint16_t value; +}; + +struct GpioReadConfigView { + uint8_t channel; + uint16_t period_ms = 0; + bool asap = false; + bool rising_edge = false; + bool falling_edge = false; +}; + +struct AccelerometerDataView { + int16_t x; + int16_t y; + int16_t z; +}; + +struct GyroscopeDataView { + int16_t x; + int16_t y; + int16_t z; +}; + +class DataCallback { +public: + DataCallback() = default; + DataCallback(const DataCallback&) = delete; + DataCallback& operator=(const DataCallback&) = delete; + DataCallback(DataCallback&&) = delete; + DataCallback& operator=(DataCallback&&) = delete; + virtual ~DataCallback() = default; + + // `*_receive_callback` returns `true` if id is valid + virtual bool can_receive_callback(DataId id, const CanDataView& data) = 0; + + virtual bool uart_receive_callback(DataId id, const UartDataView& data) = 0; + + virtual void gpio_digital_read_result_callback(const GpioDigitalDataView& data) = 0; + + virtual void gpio_analog_read_result_callback(const GpioAnalogDataView& data) = 0; + + virtual void accelerometer_receive_callback(const AccelerometerDataView& data) = 0; + + virtual void gyroscope_receive_callback(const GyroscopeDataView& data) = 0; +}; + +} // namespace librmcs::data diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/setup-clangd b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/setup-clangd new file mode 100644 index 0000000..c64b8f9 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/setup-clangd @@ -0,0 +1,45 @@ +#!/usr/bin/env sh +set -eu + +usage() { + cat <<'EOF' +Usage: setup-clangd +Creates symlinks in core/: + .clangd -> ..//.clangd + compile_commands.json -> ..//build/compile_commands.json + +Example: setup-clangd firmware/rmcs_board +EOF +} + +if [ "$#" -ne 1 ]; then + usage + exit 1 +fi + +core_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +root_dir=$(CDPATH='' cd -- "$core_dir/.." && pwd) +target="$root_dir/$1" + +if [ ! -d "$target" ]; then + echo "Error: directory '$1' does not exist." >&2 + exit 1 +fi + +link_clangd="$core_dir/.clangd" +link_compile="$core_dir/compile_commands.json" + +for link in "$link_clangd" "$link_compile"; do + if [ -L "$link" ]; then + rm -f "$link" + elif [ -e "$link" ]; then + echo "Error: $link exists and is not a symlink." >&2 + exit 1 + fi +done + +( + cd "$core_dir" + ln -s "../$1/.clangd" .clangd + ln -s "../$1/build/compile_commands.json" compile_commands.json +) diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/coroutine/lifo.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/coroutine/lifo.hpp new file mode 100644 index 0000000..bcd624b --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/coroutine/lifo.hpp @@ -0,0 +1,272 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "core/src/utility/assert.hpp" +#include "core/src/utility/stack_allocator.hpp" + +namespace librmcs::core::coroutine { + +class LifoContext { +public: + constexpr explicit LifoContext(std::span buffer) noexcept + : stack_allocator_(buffer) {} + + constexpr void* allocate(std::size_t n) noexcept { return stack_allocator_.allocate(n); } + + constexpr void deallocate(void* p, std::size_t n) noexcept { + stack_allocator_.deallocate(p, n); + } + + constexpr bool resume() noexcept { + if (!waiting_coroutine_ || waiting_coroutine_.done()) + return false; + + auto h = waiting_coroutine_; + waiting_coroutine_ = nullptr; + h.resume(); + + return true; + } + + struct Awaiter { + constexpr explicit Awaiter(LifoContext& ctx) noexcept + : context(ctx) {} + + static constexpr bool await_ready() noexcept { return false; } + + constexpr void await_suspend(std::coroutine_handle<> h) noexcept { + context.waiting_coroutine_ = h; + } + + static constexpr void await_resume() noexcept {} + + LifoContext& context; + }; + constexpr Awaiter suspend() noexcept { return Awaiter{*this}; } + +private: + utility::StackAllocator stack_allocator_; + + std::coroutine_handle<> waiting_coroutine_{nullptr}; +}; + +template +class InlineLifoContext : public LifoContext { +public: + constexpr InlineLifoContext() noexcept + : LifoContext(stack_buffer_) {} + + constexpr LifoContext& context() { return static_cast(*this); } + +private: + alignas(std::max_align_t) std::byte stack_buffer_[stack_size]{}; +}; + +template +struct LifoStackedPromise { + template + requires(static_context == nullptr) + static void* operator new(std::size_t n, LifoContext& context, const Args&...) noexcept { + // Allocate coroutine frame from the LIFO context and append a back-pointer trailer. + constexpr std::size_t align = alignof(LifoContext*); + n = (n + align - 1) & ~(align - 1); + + auto* p = static_cast(context.allocate(n + sizeof(LifoContext*))); + if (!p) + return nullptr; + new (p + n) LifoContext*(&context); + + return static_cast(p); + } + + template + requires(static_context != nullptr) + static void* operator new(std::size_t n, const Args&...) noexcept { + return static_context->allocate(n); + } + + // NOLINTNEXTLINE(misc-new-delete-overloads) + static void operator delete(void* p, std::size_t n) noexcept { + if constexpr (static_context == nullptr) { + constexpr std::size_t align = alignof(LifoContext*); + n = (n + align - 1) & ~(align - 1); + + // Recover the original LifoContext from the trailer and deallocate the whole block. + auto* context = + *std::launder(reinterpret_cast(static_cast(p) + n)); + context->deallocate(p, n + sizeof(LifoContext*)); + } else { + static_context->deallocate(p, n); + } + } +}; + +template +requires( + std::is_same_v || (std::is_move_assignable_v && std::is_default_constructible_v)) +class LifoTask { +public: + struct promise_type : LifoStackedPromise { + friend class LifoTask; + + [[noreturn]] static LifoTask get_return_object_on_allocation_failure() noexcept { + utility::assert_failed_debug(); + } + + constexpr LifoTask get_return_object() noexcept { + return LifoTask{std::coroutine_handle::from_promise(*this)}; + } + + static constexpr std::suspend_never initial_suspend() noexcept { return {}; } + + struct FinalAwaiter { + static constexpr bool await_ready() noexcept { return false; } + + static constexpr std::coroutine_handle<> + await_suspend(std::coroutine_handle h) noexcept { + auto& promise = h.promise(); + + if (promise.continuation_) + return promise.continuation_; + + return std::noop_coroutine(); + } + + constexpr void await_resume() noexcept {} + }; + static constexpr FinalAwaiter final_suspend() noexcept { return {}; } + + void return_value(T value) noexcept(std::is_nothrow_move_assignable_v) { + result_ = std::move(value); + } + + [[noreturn]] static void unhandled_exception() noexcept { utility::assert_failed_debug(); } + + private: + T result_; + std::coroutine_handle<> continuation_{nullptr}; + }; + + LifoTask(const LifoTask&) = delete; + LifoTask& operator=(const LifoTask&) = delete; + LifoTask(LifoTask&&) = delete; + LifoTask& operator=(LifoTask&&) = delete; + + ~LifoTask() noexcept { handle_.destroy(); } + + struct TaskAwaiter { + const std::coroutine_handle handle; + + constexpr bool await_ready() const noexcept { return handle.done(); } + + constexpr void await_suspend(std::coroutine_handle<> awaiting_coroutine) const noexcept { + handle.promise().continuation_ = awaiting_coroutine; + } + + T await_resume() noexcept(std::is_nothrow_move_constructible_v) { + utility::assert_debug(handle.done()); + return std::move(handle.promise().result_); + } + }; + constexpr TaskAwaiter operator co_await() && noexcept { return TaskAwaiter{handle_}; } + + [[nodiscard]] constexpr void* identifier() const noexcept { return handle_.address(); } + + [[nodiscard]] constexpr bool done() const noexcept { return handle_.done(); } + + T& result() noexcept { + utility::assert_debug(handle_.done()); + return handle_.promise().result_; + } + + const T& result() const noexcept { + utility::assert_debug(handle_.done()); + return handle_.promise().result_; + } + +private: + explicit constexpr LifoTask(std::coroutine_handle h) noexcept + : handle_(h) {} + + const std::coroutine_handle handle_; +}; + +template +class LifoTask { +public: + struct promise_type : LifoStackedPromise { + friend class LifoTask; + + [[noreturn]] static LifoTask get_return_object_on_allocation_failure() noexcept { + utility::assert_failed_debug(); + } + + constexpr LifoTask get_return_object() noexcept { + return LifoTask{std::coroutine_handle::from_promise(*this)}; + } + + static constexpr std::suspend_never initial_suspend() noexcept { return {}; } + + struct FinalAwaiter { + static constexpr bool await_ready() noexcept { return false; } + + static constexpr std::coroutine_handle<> + await_suspend(std::coroutine_handle h) noexcept { + auto& promise = h.promise(); + + if (promise.continuation_) + return promise.continuation_; + + return std::noop_coroutine(); + } + + constexpr void await_resume() noexcept {} + }; + static constexpr FinalAwaiter final_suspend() noexcept { return {}; } + + constexpr void return_void() noexcept {} + + [[noreturn]] static void unhandled_exception() noexcept { utility::assert_failed_debug(); } + + private: + std::coroutine_handle<> continuation_{nullptr}; + }; + + LifoTask(const LifoTask&) = delete; + LifoTask& operator=(const LifoTask&) = delete; + LifoTask(LifoTask&&) = delete; + LifoTask& operator=(LifoTask&&) = delete; + + ~LifoTask() noexcept { handle_.destroy(); } + + struct TaskAwaiter { + const std::coroutine_handle handle; + + constexpr bool await_ready() const noexcept { return handle.done(); } + + constexpr void await_suspend(std::coroutine_handle<> awaiting_coroutine) const noexcept { + handle.promise().continuation_ = awaiting_coroutine; + } + + constexpr void await_resume() noexcept { utility::assert_debug(handle.done()); } + }; + constexpr TaskAwaiter operator co_await() && noexcept { return TaskAwaiter{handle_}; } + + [[nodiscard]] constexpr bool done() const noexcept { return handle_.done(); } + + constexpr void result() const noexcept { utility::assert_debug(handle_.done()); } + +private: + explicit constexpr LifoTask(std::coroutine_handle h) noexcept + : handle_(h) {} + + const std::coroutine_handle handle_; +}; + +} // namespace librmcs::core::coroutine diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/constant.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/constant.hpp new file mode 100644 index 0000000..b652e54 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/constant.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace librmcs::core::protocol { + +static constexpr std::size_t kProtocolBufferSize = 1023; + +} diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/deserializer.cpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/deserializer.cpp new file mode 100644 index 0000000..2f03bbf --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/deserializer.cpp @@ -0,0 +1,285 @@ +#include "deserializer.hpp" + +#include +#include +#include + +#include "core/include/librmcs/data/datas.hpp" +#include "core/src/coroutine/lifo.hpp" +#include "core/src/protocol/protocol.hpp" +#include "core/src/utility/assert.hpp" + +namespace librmcs::core::protocol { + +coroutine::LifoTask Deserializer::process_stream() { + while (true) { + utility::assert_debug(pending_bytes_ == 0 && requested_bytes_ == 0); + + FieldId id; + { + awaiting_field_first_byte_ = true; + const auto* header_bytes = co_await peek_bytes(sizeof(FieldHeader)); + // Logically impossible; stack unwinding is invalid here. + utility::assert_debug(header_bytes); + auto header = FieldHeader::CRef{header_bytes}; + id = header.get(); + awaiting_field_first_byte_ = false; + } + if (id == FieldId::kExtend) { + const auto* header_bytes = co_await peek_bytes(sizeof(FieldHeaderExtended)); + if (!header_bytes) [[unlikely]] { + enter_discard_mode(); + continue; + } + auto header = FieldHeaderExtended::CRef{header_bytes}; + id = header.get(); + consume_peeked_partial(sizeof(FieldHeader)); + } + + bool success = false; + switch (id) { + case FieldId::kCan0: + case FieldId::kCan1: + case FieldId::kCan2: + case FieldId::kCan3: + case FieldId::kCan4: + case FieldId::kCan5: + case FieldId::kCan6: + case FieldId::kCan7: success = co_await process_can_field(id); break; + case FieldId::kUartDbus: + case FieldId::kUart0: + case FieldId::kUart1: + case FieldId::kUart2: + case FieldId::kUart3: success = co_await process_uart_field(id); break; + case FieldId::kGpio: success = co_await process_gpio_field(id); break; + case FieldId::kImu: success = co_await process_imu_field(id); break; + default: break; + } + if (!success) + enter_discard_mode(); + } +} + +coroutine::LifoTask Deserializer::process_can_field(FieldId field_id) { + data::CanDataView data_view; + uint8_t can_data_length = 0; + { + const auto* header_bytes = co_await peek_bytes(sizeof(CanHeader)); + if (!header_bytes) [[unlikely]] + co_return false; + auto header = CanHeader::CRef{header_bytes}; + + data_view.is_fdcan = header.get(); + if (data_view.is_fdcan) [[unlikely]] + co_return false; + + data_view.is_extended_can_id = header.get(); + data_view.is_remote_transmission = header.get(); + can_data_length = static_cast(header.get()); + } + + if (data_view.is_extended_can_id) { + const auto* header_ext_bytes = co_await peek_bytes(sizeof(CanHeaderExtended)); + if (!header_ext_bytes) [[unlikely]] + co_return false; + auto header = CanHeaderExtended::CRef{header_ext_bytes}; + + data_view.can_id = header.get(); + can_data_length = can_data_length ? header.get() + 1 : 0; + } else { + const auto* header_std_bytes = co_await peek_bytes(sizeof(CanHeaderStandard)); + if (!header_std_bytes) [[unlikely]] + co_return false; + auto header = CanHeaderStandard::CRef{header_std_bytes}; + + data_view.can_id = header.get(); + can_data_length = can_data_length ? header.get() + 1 : 0; + } + consume_peeked(); + + if (can_data_length) { + const auto* can_data_bytes = co_await peek_bytes(can_data_length); + if (!can_data_bytes) [[unlikely]] + co_return false; + data_view.can_data = std::span{can_data_bytes, can_data_length}; + consume_peeked(); + } else { + data_view.can_data = std::span{}; + } + + callback_.can_deserialized_callback(field_id, data_view); + + co_return true; +} + +coroutine::LifoTask Deserializer::process_uart_field(FieldId field_id) { + data::UartDataView data_view; + uint16_t uart_data_length; + { + const auto* header_bytes = co_await peek_bytes(sizeof(UartHeader)); + if (!header_bytes) [[unlikely]] + co_return false; + auto header = UartHeader::CRef{header_bytes}; + data_view.idle_delimited = header.get(); + + if (!header.get()) { + uart_data_length = header.get(); + } else { + const auto* header_ext_bytes = co_await peek_bytes(sizeof(UartHeaderExtended)); + if (!header_ext_bytes) [[unlikely]] + co_return false; + auto header_ext = UartHeaderExtended::CRef{header_ext_bytes}; + uart_data_length = header_ext.get(); + if (uart_data_length > sizeof(pending_bytes_buffer_)) [[unlikely]] + co_return false; + } + } + consume_peeked(); + + if (uart_data_length) { + const auto* uart_data_bytes = co_await peek_bytes(uart_data_length); + if (!uart_data_bytes) [[unlikely]] + co_return false; + data_view.uart_data = std::span{uart_data_bytes, uart_data_length}; + consume_peeked(); + } else { + data_view.uart_data = std::span{}; + } + + callback_.uart_deserialized_callback(field_id, data_view); + + co_return true; +} + +coroutine::LifoTask Deserializer::process_gpio_field(FieldId) { + GpioHeader::PayloadEnum payload_type; + std::uint8_t channel = 0; + { + const auto* header_bytes = co_await peek_bytes(sizeof(GpioHeader)); + if (!header_bytes) [[unlikely]] + co_return false; + + auto header = GpioHeader::CRef{header_bytes}; + payload_type = header.get(); + channel = header.get(); + consume_peeked(); + } + + switch (payload_type) { + case GpioHeader::PayloadEnum::kDigitalWriteLow: + case GpioHeader::PayloadEnum::kDigitalWriteHigh: { + data::GpioDigitalDataView data_view{}; + data_view.channel = channel; + data_view.high = payload_type == GpioHeader::PayloadEnum::kDigitalWriteHigh; + callback_.gpio_digital_data_deserialized_callback(data_view); + break; + } + case GpioHeader::PayloadEnum::kAnalogWrite: { + const auto* payload_bytes = co_await peek_bytes(sizeof(GpioAnalogPayload)); + if (!payload_bytes) [[unlikely]] + co_return false; + + auto payload = GpioAnalogPayload::CRef{payload_bytes}; + data::GpioAnalogDataView data_view{}; + data_view.channel = channel; + data_view.value = payload.get(); + consume_peeked(); + + callback_.gpio_analog_data_deserialized_callback(data_view); + break; + } + case GpioHeader::PayloadEnum::kDigitalRead: + case GpioHeader::PayloadEnum::kAnalogRead: { + const auto* payload_bytes = co_await peek_bytes(sizeof(GpioReadConfigPayload)); + if (!payload_bytes) [[unlikely]] + co_return false; + + auto payload = GpioReadConfigPayload::CRef{payload_bytes}; + data::GpioReadConfigView data_view{}; + data_view.channel = channel; + data_view.asap = payload.get(); + data_view.rising_edge = payload.get(); + data_view.falling_edge = payload.get(); + data_view.period_ms = payload.get(); + consume_peeked(); + + if (payload_type == GpioHeader::PayloadEnum::kDigitalRead) { + callback_.gpio_digital_read_config_deserialized_callback(data_view); + } else { + callback_.gpio_analog_read_config_deserialized_callback(data_view); + } + break; + } + case GpioHeader::PayloadEnum::kDigitalReadResultLow: + case GpioHeader::PayloadEnum::kDigitalReadResultHigh: { + data::GpioDigitalDataView data_view{}; + data_view.channel = channel; + data_view.high = payload_type == GpioHeader::PayloadEnum::kDigitalReadResultHigh; + callback_.gpio_digital_data_deserialized_callback(data_view); + break; + } + case GpioHeader::PayloadEnum::kAnalogReadResult: { + const auto* payload_bytes = co_await peek_bytes(sizeof(GpioAnalogPayload)); + if (!payload_bytes) [[unlikely]] + co_return false; + + auto payload = GpioAnalogPayload::CRef{payload_bytes}; + data::GpioAnalogDataView data_view{}; + data_view.channel = channel; + data_view.value = payload.get(); + consume_peeked(); + + callback_.gpio_analog_data_deserialized_callback(data_view); + break; + } + default: co_return false; + } + + co_return true; +} + +coroutine::LifoTask Deserializer::process_imu_field(FieldId) { + ImuHeader::PayloadEnum payload_type; + { + const auto* header_bytes = co_await peek_bytes(sizeof(ImuHeader)); + if (!header_bytes) [[unlikely]] + co_return false; + + auto header = ImuHeader::CRef{header_bytes}; + payload_type = header.get(); + consume_peeked(); + } + + switch (payload_type) { + case ImuHeader::PayloadEnum::kAccelerometer: { + data::AccelerometerDataView data_view{}; + const auto* payload_bytes = co_await peek_bytes(sizeof(ImuAccelerometerPayload)); + if (!payload_bytes) [[unlikely]] + co_return false; + auto payload = ImuAccelerometerPayload::CRef{payload_bytes}; + data_view.x = payload.get(); + data_view.y = payload.get(); + data_view.z = payload.get(); + consume_peeked(); + callback_.accelerometer_deserialized_callback(data_view); + break; + } + case ImuHeader::PayloadEnum::kGyroscope: { + data::GyroscopeDataView data_view{}; + const auto* payload_bytes = co_await peek_bytes(sizeof(ImuGyroscopePayload)); + if (!payload_bytes) [[unlikely]] + co_return false; + auto payload = ImuGyroscopePayload::CRef{payload_bytes}; + data_view.x = payload.get(); + data_view.y = payload.get(); + data_view.z = payload.get(); + consume_peeked(); + callback_.gyroscope_deserialized_callback(data_view); + break; + } + default: co_return false; + } + co_return true; +} + +} // namespace librmcs::core::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/deserializer.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/deserializer.hpp new file mode 100644 index 0000000..533f98c --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/deserializer.hpp @@ -0,0 +1,264 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "core/include/librmcs/data/datas.hpp" +#include "core/src/coroutine/lifo.hpp" +#include "core/src/protocol/constant.hpp" +#include "core/src/protocol/protocol.hpp" +#include "core/src/utility/assert.hpp" + +namespace librmcs::core::protocol { + +class DeserializeCallback { +public: + DeserializeCallback() = default; + DeserializeCallback(const DeserializeCallback&) = delete; + DeserializeCallback& operator=(const DeserializeCallback&) = delete; + DeserializeCallback(DeserializeCallback&&) = delete; + DeserializeCallback& operator=(DeserializeCallback&&) = delete; + virtual ~DeserializeCallback() = default; + + virtual void can_deserialized_callback(FieldId id, const data::CanDataView& data) = 0; + + virtual void uart_deserialized_callback(FieldId id, const data::UartDataView& data) = 0; + + virtual void gpio_digital_data_deserialized_callback(const data::GpioDigitalDataView& data) = 0; + + virtual void gpio_analog_data_deserialized_callback(const data::GpioAnalogDataView& data) = 0; + + virtual void + gpio_digital_read_config_deserialized_callback(const data::GpioReadConfigView& data) = 0; + + virtual void + gpio_analog_read_config_deserialized_callback(const data::GpioReadConfigView& data) = 0; + + virtual void accelerometer_deserialized_callback(const data::AccelerometerDataView& data) = 0; + + virtual void gyroscope_deserialized_callback(const data::GyroscopeDataView& data) = 0; + + virtual void error_callback() = 0; +}; + +class Deserializer : private coroutine::InlineLifoContext<1024> { + friend coroutine::LifoStackedPromise<>; + +public: + constexpr explicit Deserializer(DeserializeCallback& callback) + : callback_(callback) + , main_task_(process_stream()) {} + + ~Deserializer() { finish_transfer(); } + Deserializer(const Deserializer&) = delete; + Deserializer& operator=(const Deserializer&) = delete; + Deserializer(Deserializer&&) = delete; + Deserializer& operator=(Deserializer&&) = delete; + + // Feed a new chunk of input bytes to satisfy the current peek_bytes() request. + // May resume the coroutine immediately if the request can be fully satisfied. + void feed(std::span buffer) { + utility::assert_debug(requested_bytes_); + utility::assert_debug(pending_bytes_ < requested_bytes_); + + if (buffer.empty() || discard_mode_) + return; + + input_cursor_ = buffer.data(); + input_end_ = buffer.data() + buffer.size(); + + if (!pending_bytes_ && buffer.size() >= requested_bytes_) { + // Fast path: current input already satisfies the request, no need to copy. + resume(); + return; + } + + auto required = static_cast(requested_bytes_ - pending_bytes_); + const bool sufficient = buffer.size() >= required; + + const size_t copied = sufficient ? required : buffer.size(); + std::memcpy(pending_bytes_buffer_ + pending_bytes_, input_cursor_, copied); + + pending_bytes_ += copied; + input_cursor_ += copied; + + if (sufficient) + resume(); + } + + void finish_transfer() { + if (awaiting_field_first_byte_) { + discard_mode_ = false; + return; + } + + // Input ended while parsing a field; truncated field. + // Cancel the outstanding peek by making await_resume() return nullptr. + discard_mode_ = true; + resume(); + discard_mode_ = false; + + utility::assert_debug(awaiting_field_first_byte_); + } + +private: + coroutine::LifoTask process_stream(); + + coroutine::LifoTask process_can_field(FieldId field_id); + + coroutine::LifoTask process_uart_field(FieldId field_id); + + coroutine::LifoTask process_gpio_field(FieldId field_id); + + coroutine::LifoTask process_imu_field(FieldId field_id); + + // Await until at least `size` contiguous bytes are available at the current read position. + // Returns a pointer to a contiguous region of at least `size` bytes. + // (from input buffer or pending cache) + struct PeekBytesAwaiter { + constexpr explicit PeekBytesAwaiter(Deserializer& owner, size_t size) noexcept + : owner_(owner) + , scheduler_awaiter_(owner_.suspend()) { + utility::assert_debug(size); + // Request must fit cache and not shrink a previous request. + utility::assert_debug(size <= sizeof(pending_bytes_buffer_)); + utility::assert_debug(size >= owner.requested_bytes_); + owner.requested_bytes_ = size; + } + + constexpr bool await_ready() noexcept { + utility::assert_debug( + !owner_.input_cursor_ || owner_.input_end_ >= owner_.input_cursor_); + const auto remaining = owner_.input_cursor_ + ? static_cast(owner_.input_end_ - owner_.input_cursor_) + : 0; + + const auto requested_bytes = owner_.requested_bytes_; + const auto pending_bytes = owner_.pending_bytes_; + utility::assert_debug(requested_bytes >= pending_bytes); + + // Fast path: satisfy the request directly from the input span (no copy). + if (!pending_bytes && remaining >= requested_bytes) + return true; + + const auto total_available = pending_bytes + remaining; + + // Not enough bytes: append everything we have into the pending cache and suspend. + if (total_available < requested_bytes) { + if (remaining) { + std::memcpy( + owner_.pending_bytes_buffer_ + pending_bytes, owner_.input_cursor_, + remaining); + } + owner_.pending_bytes_ = total_available; + owner_.input_cursor_ = owner_.input_end_; + return false; + } + + // Enough bytes, but split across chunks: top up the pending cache to satisfy peek. + utility::assert_debug(pending_bytes); + const auto required = static_cast(requested_bytes - pending_bytes); + if (required) { + utility::assert_debug(owner_.input_cursor_); + std::memcpy( + owner_.pending_bytes_buffer_ + pending_bytes, owner_.input_cursor_, required); + owner_.input_cursor_ += required; + owner_.pending_bytes_ = requested_bytes; + } + return true; + } + + constexpr void await_suspend(std::coroutine_handle<> h) noexcept { + scheduler_awaiter_.await_suspend(h); + } + + constexpr const std::byte* await_resume() const noexcept { + utility::assert_debug(owner_.requested_bytes_); + // Discard mode cancels outstanding peeks by returning nullptr. + if (owner_.discard_mode_) [[unlikely]] { + return nullptr; + } else if (owner_.pending_bytes_) { + utility::assert_debug(owner_.pending_bytes_ == owner_.requested_bytes_); + return owner_.pending_bytes_buffer_; + } else { + utility::assert_debug(owner_.input_cursor_); + utility::assert_debug(owner_.input_end_ >= owner_.input_cursor_); + utility::assert_debug( + std::cmp_greater_equal( + owner_.input_end_ - owner_.input_cursor_, owner_.requested_bytes_)); + + return owner_.input_cursor_; + } + } + + protected: + Deserializer& owner_; + + private: + coroutine::LifoContext::Awaiter scheduler_awaiter_; + }; + + // IMPORTANT CALL CONTRACT: + // 1. Within a single parsing path, the pointer returned from any `co_await peek_bytes(...)` may + // only be used until the next `co_await peek_bytes(...)` is issued; after a subsequent peek, + // all previously returned pointers must be treated as invalid and never dereferenced. + // 2. Only "expanding the window" is allowed: each subsequent `peek_bytes(new_size)` must + // satisfy `new_size >= old_size`. + // REVIEWERS: Please verify all call sites strictly follow this contract. + PeekBytesAwaiter peek_bytes(size_t size) { return PeekBytesAwaiter{*this, size}; } + + void consume_peeked() { + utility::assert_debug(requested_bytes_); + if (pending_bytes_) { + utility::assert_debug(pending_bytes_ == requested_bytes_); + pending_bytes_ = 0; + } else { + input_cursor_ += requested_bytes_; + } + requested_bytes_ = 0; + } + + void consume_peeked_partial(size_t n) { + utility::assert_debug(requested_bytes_); + utility::assert_debug(n < requested_bytes_); + + if (pending_bytes_) { + utility::assert_debug(pending_bytes_ == requested_bytes_); + std::memmove(pending_bytes_buffer_, pending_bytes_buffer_ + n, requested_bytes_ - n); + pending_bytes_ -= n; + } else { + input_cursor_ += n; + } + + requested_bytes_ -= n; + } + + void enter_discard_mode() { + callback_.error_callback(); + + // - Treat the current input chunk as exhausted (so the next peek_bytes() suspends) + // - Ignore subsequent feed() calls while discard_mode_ is true + // Recovery is driven by finish_transfer(): when suspended waiting for the first header + // byte (awaiting_field_first_byte_ == true), finish_transfer() clears discard_mode_ + // and returns, allowing parsing to resume on the next transfer. + + pending_bytes_ = requested_bytes_ = 0; + input_cursor_ = input_end_; + discard_mode_ = true; + } + + bool discard_mode_ = false; + bool awaiting_field_first_byte_ = false; + DeserializeCallback& callback_; + + size_t pending_bytes_ = 0, requested_bytes_ = 0; + alignas(std::max_align_t) std::byte pending_bytes_buffer_[kProtocolBufferSize]; + + const std::byte *input_cursor_ = nullptr, *input_end_ = nullptr; + coroutine::LifoTask main_task_; +}; + +} // namespace librmcs::core::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/protocol.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/protocol.hpp new file mode 100644 index 0000000..47f7870 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/protocol.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include + +#include "core/include/librmcs/data/datas.hpp" +#include "core/src/utility/bitfield.hpp" + +namespace librmcs::core::protocol { + +using FieldId = data::DataId; + +namespace layouts { + +using utility::BitfieldMember; + +struct FieldHeaderLayout { + using Id = utility::BitfieldMember<0, 4, FieldId>; +}; + +struct FieldHeaderExtendedLayout { + using IdExtended = utility::BitfieldMember<4, 8, FieldId>; +}; + +struct CanHeaderLayout { + using IsFdCan = BitfieldMember<4, 1>; // Currently invalid, reserved only + using IsExtendedCanId = BitfieldMember<5, 1>; + using IsRemoteTransmission = BitfieldMember<6, 1>; + using HasCanData = BitfieldMember<7, 1>; +}; + +struct CanHeaderStandardLayout { + using CanId = BitfieldMember<8, 11>; + using DataLengthCode = BitfieldMember<8 + 13, 3>; +}; + +struct CanHeaderExtendedLayout { + using CanId = BitfieldMember<8, 29>; + using DataLengthCode = BitfieldMember<8 + 29, 3>; +}; + +struct UartHeaderLayout { + using IdleDelimited = BitfieldMember<4, 1>; + using IsExtendedLength = BitfieldMember<5, 1>; + using DataLength = BitfieldMember<6, 2>; +}; + +struct UartHeaderExtendedLayout { + using DataLengthExtended = BitfieldMember<6, 10>; +}; + +} // namespace layouts + +struct FieldHeader + : utility::Bitfield<1> + , layouts::FieldHeaderLayout {}; + +struct FieldHeaderExtended + : utility::Bitfield<2> + , layouts::FieldHeaderLayout + , layouts::FieldHeaderExtendedLayout {}; + +struct CanHeader + : utility::Bitfield<1> + , layouts::CanHeaderLayout {}; + +struct CanHeaderStandard + : utility::Bitfield<3> + , layouts::CanHeaderLayout + , layouts::CanHeaderStandardLayout {}; + +struct CanHeaderExtended + : utility::Bitfield<5> + , layouts::CanHeaderLayout + , layouts::CanHeaderExtendedLayout {}; + +struct UartHeader + : utility::Bitfield<1> + , layouts::UartHeaderLayout {}; + +struct UartHeaderExtended + : utility::Bitfield<2> + , layouts::UartHeaderLayout + , layouts::UartHeaderExtendedLayout {}; + +struct GpioHeader : utility::Bitfield<2> { + enum class PayloadEnum : uint8_t { + kDigitalWriteLow = 0b0000, + kDigitalWriteHigh = 0b0001, + kAnalogWrite = 0b0010, + kDigitalRead = 0b0100, + kAnalogRead = 0b0110, + kDigitalReadResultLow = 0b1000, + kDigitalReadResultHigh = 0b1001, + kAnalogReadResult = 0b1010, + }; + + using PayloadType = utility::BitfieldMember<4, 4, PayloadEnum>; + using Channel = utility::BitfieldMember<8, 8>; +}; + +struct GpioReadConfigPayload : utility::Bitfield<2> { + using Asap = utility::BitfieldMember<0, 1>; + using RisingEdge = utility::BitfieldMember<1, 1>; + using FallingEdge = utility::BitfieldMember<2, 1>; + using PeriodMs = utility::BitfieldMember<3, 13, uint16_t>; +}; + +struct GpioAnalogPayload : utility::Bitfield<2> { + using Value = utility::BitfieldMember<0, 16, uint16_t>; +}; + +struct ImuHeader : utility::Bitfield<1> { + enum class PayloadEnum : uint8_t { + kAccelerometer = 0, + kGyroscope = 1, + }; + using PayloadType = utility::BitfieldMember<4, 4, PayloadEnum>; +}; + +struct ImuAccelerometerPayload : utility::Bitfield<6> { + using X = utility::BitfieldMember<0, 16, int16_t>; + using Y = utility::BitfieldMember<16, 16, int16_t>; + using Z = utility::BitfieldMember<32, 16, int16_t>; +}; + +struct ImuGyroscopePayload : utility::Bitfield<6> { + using X = utility::BitfieldMember<0, 16, int16_t>; + using Y = utility::BitfieldMember<16, 16, int16_t>; + using Z = utility::BitfieldMember<32, 16, int16_t>; +}; + +} // namespace librmcs::core::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/serializer.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/serializer.hpp new file mode 100644 index 0000000..553d062 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/protocol/serializer.hpp @@ -0,0 +1,437 @@ +#pragma once + +#include +#include +#include +#include + +#include "core/include/librmcs/data/datas.hpp" +#include "core/src/protocol/constant.hpp" +#include "core/src/protocol/protocol.hpp" +#include "core/src/utility/assert.hpp" +#include "core/src/utility/verify.hpp" + +namespace librmcs::core::protocol { + +class SerializeBuffer { +public: + SerializeBuffer() = default; + SerializeBuffer(const SerializeBuffer&) = delete; + SerializeBuffer& operator=(const SerializeBuffer&) = delete; + SerializeBuffer(SerializeBuffer&&) = delete; + SerializeBuffer& operator=(SerializeBuffer&&) = delete; + virtual ~SerializeBuffer() noexcept = default; + + virtual std::span allocate(std::size_t size) noexcept = 0; +}; + +class Serializer { +public: + enum class SerializeResult : std::uint8_t { kSuccess = 0, kBadAlloc = 1, kInvalidArgument = 2 }; + + explicit Serializer(SerializeBuffer& buffer) noexcept + : buffer_(buffer) {} + + SerializeResult write_can(FieldId field_id, const data::CanDataView& view) noexcept { + const std::size_t required = required_can_size(field_id, view); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, field_id); + + const std::size_t can_data_length = view.can_data.size(); + const bool has_data = can_data_length != 0; + const std::uint8_t data_length_code = + has_data ? static_cast(can_data_length - 1) : 0; + + if (view.is_extended_can_id) { + auto header = CanHeaderExtended::Ref(cursor); + cursor += sizeof(CanHeaderExtended); + header.set(false); + header.set(true); + header.set(view.is_remote_transmission); + header.set(has_data); + header.set(view.can_id); + header.set(data_length_code); + } else { + auto header = CanHeaderStandard::Ref(cursor); + cursor += sizeof(CanHeaderStandard); + header.set(false); + header.set(false); + header.set(view.is_remote_transmission); + header.set(has_data); + header.set(view.can_id); + header.set(data_length_code); + } + + if (has_data) { + std::memcpy(cursor, view.can_data.data(), can_data_length); + cursor += can_data_length; + } + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_uart( + FieldId field_id, const data::UartDataView& view, + std::span suffix_data = {}) noexcept { + const std::size_t required = required_uart_size(field_id, view, suffix_data); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, field_id); + + const std::size_t uart_data_length = view.uart_data.size() + suffix_data.size(); + const bool use_extended_length = uart_data_length >= 4; + if (use_extended_length) { + auto header = UartHeaderExtended::Ref(cursor); + cursor += sizeof(UartHeaderExtended); + header.set(view.idle_delimited); + header.set(true); + header.set( + static_cast(uart_data_length)); + } else { + auto header = UartHeader::Ref(cursor); + cursor += sizeof(UartHeader); + header.set(view.idle_delimited); + header.set(false); + header.set(static_cast(uart_data_length)); + } + + if (!view.uart_data.empty()) { + std::memcpy(cursor, view.uart_data.data(), view.uart_data.size()); + cursor += view.uart_data.size(); + } + if (!suffix_data.empty()) { + std::memcpy(cursor, suffix_data.data(), suffix_data.size()); + cursor += suffix_data.size(); + } + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_gpio_digital_data(const data::GpioDigitalDataView& view) noexcept { + const auto payload_type = view.high ? GpioHeader::PayloadEnum::kDigitalWriteHigh + : GpioHeader::PayloadEnum::kDigitalWriteLow; + const std::size_t required = required_gpio_size(FieldId::kGpio, payload_type); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kGpio); + + auto header = GpioHeader::Ref(cursor); + cursor += sizeof(GpioHeader); + header.set(payload_type); + header.set(view.channel); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_gpio_digital_read_config(const data::GpioReadConfigView& view) noexcept { + LIBRMCS_VERIFY_LIKELY( + view.period_ms <= ((1U << 13) - 1U), SerializeResult::kInvalidArgument); + + const std::size_t required = + required_gpio_size(FieldId::kGpio, GpioHeader::PayloadEnum::kDigitalRead); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kGpio); + + auto header = GpioHeader::Ref(cursor); + cursor += sizeof(GpioHeader); + header.set(GpioHeader::PayloadEnum::kDigitalRead); + header.set(view.channel); + + auto payload = GpioReadConfigPayload::Ref(cursor); + cursor += sizeof(GpioReadConfigPayload); + payload.set(view.asap); + payload.set(view.rising_edge); + payload.set(view.falling_edge); + payload.set(view.period_ms); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_gpio_analog_data(const data::GpioAnalogDataView& view) noexcept { + const std::size_t required = + required_gpio_size(FieldId::kGpio, GpioHeader::PayloadEnum::kAnalogWrite); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kGpio); + + auto header = GpioHeader::Ref(cursor); + cursor += sizeof(GpioHeader); + header.set(GpioHeader::PayloadEnum::kAnalogWrite); + header.set(view.channel); + + auto payload = GpioAnalogPayload::Ref(cursor); + cursor += sizeof(GpioAnalogPayload); + payload.set(view.value); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_gpio_analog_read_config(const data::GpioReadConfigView& view) noexcept { + LIBRMCS_VERIFY_LIKELY( + view.period_ms <= ((1U << 13) - 1U), SerializeResult::kInvalidArgument); + LIBRMCS_VERIFY_LIKELY( + !view.falling_edge && !view.rising_edge, SerializeResult::kInvalidArgument); + + const std::size_t required = + required_gpio_size(FieldId::kGpio, GpioHeader::PayloadEnum::kAnalogRead); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kGpio); + + auto header = GpioHeader::Ref(cursor); + cursor += sizeof(GpioHeader); + header.set(GpioHeader::PayloadEnum::kAnalogRead); + header.set(view.channel); + + auto payload = GpioReadConfigPayload::Ref(cursor); + cursor += sizeof(GpioReadConfigPayload); + payload.set(view.asap); + payload.set(false); + payload.set(false); + payload.set(view.period_ms); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_gpio_digital_read_result(const data::GpioDigitalDataView& view) noexcept { + const auto payload_type = view.high ? GpioHeader::PayloadEnum::kDigitalReadResultHigh + : GpioHeader::PayloadEnum::kDigitalReadResultLow; + const std::size_t required = required_gpio_size(FieldId::kGpio, payload_type); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kGpio); + + auto header = GpioHeader::Ref(cursor); + cursor += sizeof(GpioHeader); + header.set(payload_type); + header.set(view.channel); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_gpio_analog_read_result(const data::GpioAnalogDataView& view) noexcept { + const std::size_t required = + required_gpio_size(FieldId::kGpio, GpioHeader::PayloadEnum::kAnalogReadResult); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kGpio); + + auto header = GpioHeader::Ref(cursor); + cursor += sizeof(GpioHeader); + header.set(GpioHeader::PayloadEnum::kAnalogReadResult); + header.set(view.channel); + + auto payload = GpioAnalogPayload::Ref(cursor); + cursor += sizeof(GpioAnalogPayload); + payload.set(view.value); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_imu_accelerometer(const data::AccelerometerDataView& view) noexcept { + const std::size_t required = required_imu_size(FieldId::kImu, ImuPayload::kAccelerometer); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kImu); + + auto header = ImuHeader::Ref(cursor); + cursor += sizeof(ImuHeader); + header.set(ImuHeader::PayloadEnum::kAccelerometer); + + auto payload = ImuAccelerometerPayload::Ref(cursor); + cursor += sizeof(ImuAccelerometerPayload); + payload.set(view.x); + payload.set(view.y); + payload.set(view.z); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + + SerializeResult write_imu_gyroscope(const data::GyroscopeDataView& view) noexcept { + const std::size_t required = required_imu_size(FieldId::kImu, ImuPayload::kGyroscope); + LIBRMCS_VERIFY_LIKELY(required, SerializeResult::kInvalidArgument); + + auto dst = buffer_.allocate(required); + LIBRMCS_VERIFY_LIKELY(!dst.empty(), SerializeResult::kBadAlloc); + utility::assert_debug(dst.size() == required); + std::byte* cursor = dst.data(); + + write_field_header(cursor, FieldId::kImu); + + auto header = ImuHeader::Ref(cursor); + cursor += sizeof(ImuHeader); + header.set(ImuHeader::PayloadEnum::kGyroscope); + + auto payload = ImuGyroscopePayload::Ref(cursor); + cursor += sizeof(ImuGyroscopePayload); + payload.set(view.x); + payload.set(view.y); + payload.set(view.z); + + utility::assert_debug(cursor == dst.data() + dst.size()); + return SerializeResult::kSuccess; + } + +private: + static constexpr bool use_extended_field_header(FieldId field_id) { + utility::assert_debug(field_id != FieldId::kExtend); + return static_cast(field_id) > 0xF; + } + + static constexpr std::size_t required_field_header_size(FieldId field_id) { + return use_extended_field_header(field_id) ? sizeof(FieldHeaderExtended) + : sizeof(FieldHeader); + } + + static void write_field_header(std::byte*& cursor, FieldId field_id) noexcept { + if (use_extended_field_header(field_id)) { + auto header = FieldHeaderExtended::Ref(cursor); + cursor += 1; + static_assert(sizeof(FieldHeaderExtended) == sizeof(FieldHeader) + 1); + header.set(FieldId::kExtend); + header.set(field_id); + } else { + auto header = FieldHeader::Ref(cursor); + header.set(field_id); + } + } + + static std::size_t required_can_size(FieldId field_id, const data::CanDataView& view) noexcept { + LIBRMCS_VERIFY_LIKELY(!view.is_fdcan, 0); // TODO: Support FDCAN when protocol ready + LIBRMCS_VERIFY_LIKELY(!view.is_remote_transmission || view.can_data.empty(), 0); + LIBRMCS_VERIFY_LIKELY(view.can_data.size() <= 8, 0); + if (view.is_extended_can_id) + LIBRMCS_VERIFY_LIKELY(view.can_id <= 0x1FFFFFFF, 0); + else + LIBRMCS_VERIFY_LIKELY(view.can_id <= 0x7FF, 0); + + const std::size_t field_header_bytes = required_field_header_size(field_id); + const std::size_t can_header_bytes = + view.is_extended_can_id ? sizeof(CanHeaderExtended) : sizeof(CanHeaderStandard); + const std::size_t total = + (field_header_bytes + can_header_bytes - 1) + view.can_data.size(); + utility::assert_debug(total <= kProtocolBufferSize); + + return total; + } + + static std::size_t required_uart_size( + FieldId field_id, const data::UartDataView& view, + std::span suffix_data) noexcept { + const std::size_t field_header_bytes = required_field_header_size(field_id); + + const std::size_t uart_data_length = view.uart_data.size() + suffix_data.size(); + LIBRMCS_VERIFY_LIKELY(uart_data_length <= kProtocolBufferSize, 0); + + const bool use_extended_length = uart_data_length >= 4; + const std::size_t uart_header_bytes = + use_extended_length ? sizeof(UartHeaderExtended) : sizeof(UartHeader); + + const std::size_t total = (field_header_bytes + uart_header_bytes - 1) + uart_data_length; + LIBRMCS_VERIFY_LIKELY(total <= kProtocolBufferSize, 0); + + return total; + } + + static std::size_t + required_gpio_size(FieldId field_id, GpioHeader::PayloadEnum payload) noexcept { + const std::size_t field_header_bytes = required_field_header_size(field_id); + const std::size_t gpio_header_bytes = sizeof(GpioHeader); + std::size_t payload_bytes = 0; + switch (payload) { + case GpioHeader::PayloadEnum::kDigitalWriteLow: + case GpioHeader::PayloadEnum::kDigitalWriteHigh: + case GpioHeader::PayloadEnum::kDigitalReadResultLow: + case GpioHeader::PayloadEnum::kDigitalReadResultHigh: payload_bytes = 0; break; + case GpioHeader::PayloadEnum::kDigitalRead: + case GpioHeader::PayloadEnum::kAnalogRead: + payload_bytes = sizeof(GpioReadConfigPayload); + break; + case GpioHeader::PayloadEnum::kAnalogWrite: + case GpioHeader::PayloadEnum::kAnalogReadResult: + payload_bytes = sizeof(GpioAnalogPayload); + break; + default: return 0; + } + + const std::size_t total = (field_header_bytes + gpio_header_bytes - 1) + payload_bytes; + utility::assert_debug(total <= kProtocolBufferSize); + + return total; + } + + enum class ImuPayload : std::uint8_t { kAccelerometer = 0, kGyroscope = 1 }; + + static std::size_t required_imu_size(FieldId field_id, ImuPayload payload) noexcept { + const std::size_t field_header_bytes = required_field_header_size(field_id); + const std::size_t imu_header_bytes = sizeof(ImuHeader); + const std::size_t payload_bytes = (payload == ImuPayload::kAccelerometer) + ? sizeof(ImuAccelerometerPayload) + : sizeof(ImuGyroscopePayload); + + const std::size_t total = (field_header_bytes + imu_header_bytes - 1) + payload_bytes; + utility::assert_debug(total <= kProtocolBufferSize); + + return total; + } + + SerializeBuffer& buffer_; +}; + +} // namespace librmcs::core::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/assert.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/assert.hpp new file mode 100644 index 0000000..6c92e80 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/assert.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#ifndef NDEBUG +# include +#endif + +namespace librmcs::core::utility { + +[[noreturn]] void assert_func(const std::source_location& location); + +[[noreturn]] constexpr void + assert_failed_always(const std::source_location& location = std::source_location::current()) { + assert_func(location); +} + +[[noreturn]] inline void + assert_failed_debug(const std::source_location& location = std::source_location::current()) { +#ifdef NDEBUG + (void)location; + std::unreachable(); +#else + assert_func(location); +#endif +} + +constexpr void assert_always( + bool condition, const std::source_location& location = std::source_location::current()) { + if (!condition) [[unlikely]] + assert_func(location); +} + +constexpr void assert_debug( + bool condition, const std::source_location& location = std::source_location::current()) { +#ifdef NDEBUG + [[assume(condition)]]; + (void)location; +#else + assert_always(condition, location); +#endif +} + +// Debug-only lazy assertion: The predicate is evaluated only in debug builds. +template +requires std::is_nothrow_invocable_r_v inline void assert_debug_lazy( + Condition&& condition, const std::source_location& location = std::source_location::current()) { +#ifdef NDEBUG + (void)condition; + (void)location; +#else + assert_always(static_cast(std::invoke(std::forward(condition))), location); +#endif +} + +} // namespace librmcs::core::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/bitfield.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/bitfield.hpp new file mode 100644 index 0000000..c285754 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/bitfield.hpp @@ -0,0 +1,273 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace librmcs::core::utility { + +struct BitfieldMemberTag {}; + +template +using DefaultValueTypeT = std::conditional_t< + (bit_width == 1), bool, + std::conditional_t< + (bit_width <= 8), std::uint8_t, + std::conditional_t< + (bit_width <= 16), std::uint16_t, + std::conditional_t< + (bit_width <= 32), std::uint32_t, + std::conditional_t<(bit_width <= 64), std::uint64_t, void>>>>>; + +/** + * @brief Describes a single field within a Bitfield buffer. + * + * If ValueT is a signed integral type and bit_width is smaller than sizeof(ValueT) * 8, + * Bitfield::get performs two's-complement sign extension after extraction. + */ +template > +requires( + std::is_trivial_v && sizeof(ValueT) <= sizeof(std::uint64_t) + && (std::is_integral_v || std::is_enum_v) && (sizeof(ValueT) * 8) >= bit_width) +struct BitfieldMember : BitfieldMemberTag { + static_assert(bit_width > 0 && bit_width <= 64); + + static constexpr std::size_t kIndex = index; + static constexpr std::size_t kBitWidth = bit_width; + + using ValueType = ValueT; +}; + +template +concept is_bitfield_member = std::derived_from; + +/** + * @brief Fixed-layout little-endian bitfield over a byte buffer. + * + * Bitfield provides compile-time defined bitfield access on top of a + * std::byte buffer, with a layout that is independent of CPU endianness + * and compiler-specific struct bitfield rules. + * + * Bit indexing follows a fixed little-endian bitstream convention: + * - bit 0 is the least significant bit of data[0] + * - bit 7 is the most significant bit of data[0] + * - bit 8 is the least significant bit of data[1] + * - and so on: bit N belongs to data[N / 8], at position (N % 8) + * + * This mapping is the same on all architectures, regardless of whether + * the target CPU is little-endian or big-endian. + * + * Fields are described by BitfieldMember types, which + * define the starting bit index and width. Access is performed via: + * - Bitfield::get(...) to read a field + * - Bitfield::set(...) to write a field + * + * The implementation: + * - operates only on std::byte and integer/enum types (bitwise extraction) + * - supports signed integral types via two's-complement sign extension + * - does not use or rely on C++ struct bitfields + * - enforces bounds and width constraints at compile time where possible + * - does not perform any implicit initialization or runtime bounds checks + * + * Notes on usage and safety: + * - This type is intended to be zero‑overhead. The `data` buffer is + * intentionally left uninitialized; the user is responsible for + * initializing it (e.g. `Bitfield<...> bf{};`) before reading. + * - Pointer‑based `get`/`set` overloads assume that the caller provides + * a buffer of at least `size_in_bytes` bytes. No runtime bounds checking + * is performed; out‑of‑bounds access is a caller ub. + * + * This type is intended for protocol headers, binary payloads, and other + * data formats that explicitly define a little-endian bit layout. + * + * Usage example: + * struct MyBitfield : Bitfield<4> { + * using Id = BitfieldMember<0, 4>; + * using Flag = BitfieldMember<4, 1>; + * using Length = BitfieldMember<5, 27>; + * } bf{}; + * bf.set(0xA); + * bf.set(true); + * bf.set(512); + * assert(bf.get() == 0xA); + * assert(bf.get() == true); + * assert(bf.get() == 512); + * + * @tparam size_in_bytes size of the underlying buffer in bytes + */ +template +struct Bitfield { +private: + template + static consteval bool check_member() { + constexpr std::size_t first_bit = Member::kIndex; + constexpr std::size_t bit_width = Member::kBitWidth; + constexpr std::size_t last_bit = first_bit + bit_width; + constexpr std::size_t first_byte = first_bit / 8; + constexpr std::size_t last_byte = (last_bit + 7) / 8; + constexpr std::size_t span_bytes = last_byte - first_byte; + + return bit_width > 0 // + && first_bit < kSizeInBits // + && bit_width <= kSizeInBits - first_bit // + && span_bytes > 0 // + && span_bytes <= sizeof(std::uint64_t); // + } + + template + using BestWordForSpan = std::conditional_t< + (span_bytes <= 1), std::uint8_t, + std::conditional_t< + (span_bytes <= 2), std::uint16_t, + std::conditional_t<(span_bytes <= 4), std::uint32_t, std::uint64_t>>>; + +public: + static_assert(size_in_bytes > 0); + static_assert(size_in_bytes <= std::numeric_limits::max() / 8); + + static constexpr std::size_t kSizeInBytes = size_in_bytes; + static constexpr std::size_t kSizeInBits = size_in_bytes * 8; + + template + requires(check_member()) + [[nodiscard]] static constexpr auto get(const std::byte* src) noexcept -> Member::ValueType { + return read_bits(src); + } + + template + requires(check_member()) + static constexpr void set(Member::ValueType value, std::byte* dst) noexcept { + write_bits(value, dst); + } + + template + requires(check_member()) + [[nodiscard]] constexpr auto get() const noexcept -> Member::ValueType { + return get(data); + } + + template + requires(check_member()) constexpr void set(Member::ValueType value) noexcept { + set(value, data); + } + + // Buffer is intentionally left uninitialized for zero-cost construction + std::byte data[kSizeInBytes]; + + struct Ref { + constexpr explicit Ref(std::byte* ptr) noexcept + : ptr_(ptr) {} + + template + requires(check_member()) + [[nodiscard]] constexpr auto get() const noexcept -> Member::ValueType { + return Bitfield::template get(ptr_); + } + + template + requires(check_member()) constexpr void set(Member::ValueType value) noexcept { + Bitfield::template set(value, ptr_); + } + + private: + std::byte* ptr_; + }; + + struct CRef { + constexpr explicit CRef(const std::byte* ptr) noexcept + : ptr_(ptr) {} + + template + requires(check_member()) + [[nodiscard]] constexpr auto get() const noexcept -> Member::ValueType { + return Bitfield::template get(ptr_); + } + + private: + const std::byte* ptr_; + }; + +private: + template + [[nodiscard]] static constexpr auto read_bits(const std::byte* src) noexcept + -> Member::ValueType { + + constexpr std::size_t first_bit = Member::kIndex; + constexpr std::size_t bit_width = Member::kBitWidth; + constexpr std::size_t last_bit = first_bit + bit_width; + constexpr std::size_t first_byte = first_bit / 8; + constexpr std::size_t last_byte = (last_bit + 7) / 8; + constexpr std::size_t span_bytes = last_byte - first_byte; + + using Word = BestWordForSpan; + + Word accum = 0; + + for (std::size_t i = 0; i < span_bytes; ++i) { + auto b = std::to_integer(src[first_byte + i]); + accum |= (Word(b) << (8 * i)); + } + + constexpr std::size_t inner_offset = first_bit - (first_byte * 8); + + constexpr Word full_mask = + (bit_width == sizeof(Word) * 8) ? ~Word(0) : ((Word(1) << bit_width) - 1); + + const Word value = (accum >> inner_offset) & full_mask; + + using ValueType = Member::ValueType; + if constexpr (std::is_integral_v && std::is_signed_v) { + // Use the "shift-left-then-shift-right" idiom for sign extension. + // This relies on C++20's mandatory arithmetic right shift and defined left-shift + // overflow. + static_assert(__cplusplus >= 202002L); + + using ComputeType = + std::conditional_t<(sizeof(Word) < sizeof(int)), int, std::make_signed_t>; + constexpr int shift_amount = (sizeof(ComputeType) * 8) - bit_width; + return static_cast( + (static_cast(value) << shift_amount) >> shift_amount); + } else { + return static_cast(value); + } + } + + template + static constexpr void write_bits(Member::ValueType value, std::byte* dst) noexcept { + + constexpr std::size_t first_bit = Member::kIndex; + constexpr std::size_t bit_width = Member::kBitWidth; + constexpr std::size_t last_bit = first_bit + bit_width; + constexpr std::size_t first_byte = first_bit / 8; + constexpr std::size_t last_byte = (last_bit + 7) / 8; + constexpr std::size_t span_bytes = last_byte - first_byte; + + using Word = BestWordForSpan; + + Word accum = 0; + + for (std::size_t i = 0; i < span_bytes; ++i) { + auto b = std::to_integer(dst[first_byte + i]); + accum |= (Word(b) << (8 * i)); + } + + constexpr std::size_t inner_offset = first_bit - (first_byte * 8); + + constexpr Word full_mask = + (bit_width == sizeof(Word) * 8) ? ~Word(0) : ((Word(1) << bit_width) - 1); + + accum &= ~(full_mask << inner_offset); + + const Word v = (Word(value) & full_mask) << inner_offset; + accum |= v; + + for (std::size_t i = 0; i < span_bytes; ++i) { + std::uint8_t b = static_cast((accum >> (8 * i)) & 0xFF); + dst[first_byte + i] = std::byte{b}; + } + } +}; + +} // namespace librmcs::core::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/immovable.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/immovable.hpp new file mode 100644 index 0000000..7fd9101 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/immovable.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace librmcs::core::utility { + +class Immovable { +public: + Immovable() = default; + Immovable(const Immovable&) = delete; + Immovable& operator=(const Immovable&) = delete; + Immovable(Immovable&&) = delete; + Immovable& operator=(Immovable&&) = delete; + ~Immovable() = default; +}; + +} // namespace librmcs::core::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/stack_allocator.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/stack_allocator.hpp new file mode 100644 index 0000000..ac7b094 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/stack_allocator.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include + +#include "core/src/utility/assert.hpp" + +namespace librmcs::core::utility { + +template +class BasicStackAllocator; + +template <> +class BasicStackAllocator { +public: + constexpr explicit BasicStackAllocator(std::span buffer) + : top_(buffer.data()) + , end_(buffer.data() + buffer.size()) {} + + BasicStackAllocator(const BasicStackAllocator&) = delete; + BasicStackAllocator& operator=(const BasicStackAllocator&) = delete; + BasicStackAllocator(BasicStackAllocator&&) = delete; + BasicStackAllocator& operator=(BasicStackAllocator&&) = delete; + ~BasicStackAllocator() = default; + + constexpr void* allocate(std::size_t n) noexcept { + constexpr std::size_t align = alignof(std::max_align_t); + n = (n + align - 1) & ~(align - 1); + + const std::size_t remaining = static_cast(end_ - top_); + if (remaining < n) [[unlikely]] + return nullptr; + + void* p = top_; + top_ += n; + return p; + } + + constexpr void deallocate(void* p, size_t n) noexcept { + (void)n; + top_ = static_cast(p); + } + +protected: + std::byte *top_, *end_; +}; + +template <> +class BasicStackAllocator : private BasicStackAllocator { + using Impl = BasicStackAllocator; + +public: + constexpr explicit BasicStackAllocator(std::span buffer) + : Impl(buffer) {} + + void* allocate(std::size_t n) noexcept { + assert_always(n > 0); + void* p = Impl::allocate(n); + if (!p) + return nullptr; + assert_always(reinterpret_cast(p) % alignof(std::max_align_t) == 0); + + assert_always(lifo_check_depth_ < kMaxAllocs); + lifo_check_stack_[lifo_check_depth_++] = p; + + return p; + } + + void deallocate(void* p, std::size_t n) noexcept { + assert_always(p); + assert_always(lifo_check_depth_ > 0); + assert_always(p == lifo_check_stack_[lifo_check_depth_ - 1]); + + constexpr std::size_t align = alignof(std::max_align_t); + n = (n + align - 1) & ~(align - 1); + assert_always(p == Impl::top_ - n); + + Impl::deallocate(p, n); + --lifo_check_depth_; + } + +private: + static constexpr std::size_t kMaxAllocs = 16; + void* lifo_check_stack_[kMaxAllocs]{}; + std::size_t lifo_check_depth_ = 0; +}; + +#ifdef NDEBUG +using StackAllocator = BasicStackAllocator; +#else +using StackAllocator = BasicStackAllocator; +#endif + +} // namespace librmcs::core::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/uncopyable.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/uncopyable.hpp new file mode 100644 index 0000000..ab45967 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/uncopyable.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace librmcs::core::utility { + +class Uncopyable { +public: + Uncopyable() = default; + Uncopyable(const Uncopyable&) = delete; + Uncopyable& operator=(const Uncopyable&) = delete; + Uncopyable(Uncopyable&&) = default; + Uncopyable& operator=(Uncopyable&&) = default; + ~Uncopyable() = default; +}; + +} // namespace librmcs::core::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/verify.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/verify.hpp new file mode 100644 index 0000000..7aecf41 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/core/src/utility/verify.hpp @@ -0,0 +1,19 @@ +#pragma once + +#define LIBRMCS_VERIFY(_cond, _ret) \ + do { \ + if (_cond) { \ + (void)0; \ + } else { \ + return _ret; \ + } \ + } while (false) + +#define LIBRMCS_VERIFY_LIKELY(_cond, _ret) \ + do { \ + if (_cond) [[likely]] { \ + (void)0; \ + } else { \ + return _ret; \ + } \ + } while (false) diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/CMakeLists.txt b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/CMakeLists.txt new file mode 100644 index 0000000..a1f1bc8 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/CMakeLists.txt @@ -0,0 +1,168 @@ +cmake_minimum_required(VERSION 3.28) +set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + +project(librmcs-sdk VERSION 3 LANGUAGES C CXX) + +if(NOT DEFINED LIBRMCS_PROJECT_VERSION OR LIBRMCS_PROJECT_VERSION STREQUAL "") + execute_process( + COMMAND ".scripts/generate_version" + WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/.." + OUTPUT_VARIABLE LIBRMCS_PROJECT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) +endif() +message(STATUS "Librmcs project version: ${LIBRMCS_PROJECT_VERSION}") + +# Set C++ standard to C++23 +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +# Set C standard to C11 +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) + +# Disable GNU extensions +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_C_EXTENSIONS OFF) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Add compiler options based on compiler +if(MSVC) + add_compile_options(/W4 /Zc:preprocessor) + add_compile_definitions(NOMINMAX _CRT_SECURE_NO_WARNINGS) +else() + # GCC/Clang + add_compile_options( + -g -Wall -Wextra -Wpedantic -fvisibility=hidden + $<$:-fvisibility-inlines-hidden> + ) +endif() +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + +# Get project sources +file(GLOB_RECURSE PROJECT_SOURCE CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/../core/src/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../core/src/*.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c +) + +option(BUILD_STATIC_LIBRMCS "Build static librmcs" ON) +if(BUILD_STATIC_LIBRMCS) + add_library( + ${PROJECT_NAME} STATIC + ${PROJECT_SOURCE} + ) + target_compile_definitions(${PROJECT_NAME} PUBLIC STATIC_LINKING_LIBRMCS) + message(STATUS "Building static librmcs") +else() + add_library( + ${PROJECT_NAME} SHARED + ${PROJECT_SOURCE} + ) + message(STATUS "Building shared librmcs") +endif() + +target_compile_definitions(${PROJECT_NAME} PRIVATE LIBRMCS_PROJECT_VERSION_STRING="${LIBRMCS_PROJECT_VERSION}") +target_compile_definitions(${PROJECT_NAME} PRIVATE BUILDING_LIBRMCS) + +target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../core/include) +target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../host/include) +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +# Force optimization in debug mode +if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE $<$:/O2>) +else() + target_compile_options(${PROJECT_NAME} PRIVATE $<$:-O3>) +endif() + +if(WIN32) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBUSB REQUIRED libusb-1.0) + target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE ${LIBUSB_INCLUDE_DIRS}) + target_link_directories(${PROJECT_NAME} PUBLIC "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib") + target_link_libraries(${PROJECT_NAME} PUBLIC ${LIBUSB_LIBRARIES}) +elseif(UNIX) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBUSB REQUIRED libusb-1.0) + target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE ${LIBUSB_INCLUDE_DIRS}) + target_link_libraries(${PROJECT_NAME} PUBLIC ${LIBUSB_LIBRARIES}) + find_package(Threads REQUIRED) + target_link_libraries(${PROJECT_NAME} PUBLIC Threads::Threads) +endif() + +# Enable LTO optimization +set_target_properties(${PROJECT_NAME} PROPERTIES + INTERPROCEDURAL_OPTIMIZATION TRUE +) + +install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) +install(DIRECTORY ../core/include/ DESTINATION include) +install(DIRECTORY include/ DESTINATION include) + +# include(CTest) +# if(BUILD_TESTING) +# include(FetchContent) +# set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# FetchContent_Declare( +# googletest +# URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +# DOWNLOAD_EXTRACT_TIMESTAMP TRUE +# ) +# FetchContent_MakeAvailable(googletest) + +# file(GLOB_RECURSE LIBRMCS_TEST_SOURCES CONFIGURE_DEPENDS +# ${PROJECT_SOURCE_DIR}/tests/*.cpp +# ) + +# add_executable(librmcs_tests +# ${LIBRMCS_TEST_SOURCES} +# ) +# target_link_libraries(librmcs_tests PRIVATE gtest_main ${PROJECT_NAME}) + +# add_test(NAME librmcs_tests COMMAND librmcs_tests) +# endif() + +if(UNIX AND NOT APPLE) + if(NOT DEFINED LIBRMCS_DEBIAN_VERSION OR LIBRMCS_DEBIAN_VERSION STREQUAL "") + execute_process( + COMMAND ".scripts/generate_version" "--format" "debian" + WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/.." + OUTPUT_VARIABLE LIBRMCS_DEBIAN_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) + endif() + message(STATUS "Librmcs Debian package version: ${LIBRMCS_DEBIAN_VERSION}") + + set(CPACK_GENERATOR "DEB") + + set(ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) + if(ARCHITECTURE STREQUAL "x86_64") + set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") + elseif(ARCHITECTURE STREQUAL "aarch64") + set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64") + else() + message(FATAL_ERROR "Unsupported architecture: ${ARCHITECTURE}.") + endif() + + set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}") + set(CPACK_DEBIAN_PACKAGE_VERSION "${LIBRMCS_DEBIAN_VERSION}") + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Zihan Qin ") + set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "A portable library implements the core functionality of RMCS") + set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/Alliance-Algorithm/librmcs") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libusb-1.0-0-dev") + set(CPACK_DEBIAN_PACKAGE_SECTION "libs") + set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") + + string(TOLOWER "${CMAKE_BUILD_TYPE}" PACKAGE_BUILD_MODE) + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${LIBRMCS_PROJECT_VERSION}-${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}-${PACKAGE_BUILD_MODE}") + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE") + + include(CPack) +endif() diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/CMakePresets.json b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/CMakePresets.json new file mode 100644 index 0000000..25d2ef1 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/CMakePresets.json @@ -0,0 +1,25 @@ +{ + "version": 4, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build" + }, + { + "name": "linux-debug", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "linux-release", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ] +} diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/agent/c_board.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/agent/c_board.hpp new file mode 100644 index 0000000..1a8912a --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/agent/c_board.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include + +#include +#include + +namespace librmcs::agent { + +class CBoard : private data::DataCallback { +public: + explicit CBoard(std::string_view serial_filter = {}) + : handler_(0xA11C, 0xD401, serial_filter, *this) {} + + CBoard(const CBoard&) = delete; + CBoard& operator=(const CBoard&) = delete; + CBoard(CBoard&&) = delete; + CBoard& operator=(CBoard&&) = delete; + ~CBoard() override = default; + + class PacketBuilder { + friend class CBoard; + + public: + PacketBuilder& can1_transmit(const librmcs::data::CanDataView& data) { + if (!builder_.write_can(data::DataId::kCan1, data)) [[unlikely]] + throw std::invalid_argument{"CAN1 transmission failed: Invalid CAN data"}; + return *this; + } + PacketBuilder& can2_transmit(const librmcs::data::CanDataView& data) { + if (!builder_.write_can(data::DataId::kCan2, data)) [[unlikely]] + throw std::invalid_argument{"CAN2 transmission failed: Invalid CAN data"}; + return *this; + } + + PacketBuilder& dbus_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUartDbus, data)) [[unlikely]] + throw std::invalid_argument{"DBUS transmission failed: Invalid UART data"}; + return *this; + } + PacketBuilder& uart1_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUart1, data)) [[unlikely]] + throw std::invalid_argument{"UART1 transmission failed: Invalid UART data"}; + return *this; + } + PacketBuilder& uart2_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUart2, data)) [[unlikely]] + throw std::invalid_argument{"UART2 transmission failed: Invalid UART data"}; + return *this; + } + + PacketBuilder& gpio_digital_write(const librmcs::data::GpioDigitalDataView& data) { + if (data.channel < 1 || data.channel > 7 || !builder_.write_gpio_digital_data(data)) + [[unlikely]] + throw std::invalid_argument{"GPIO digital transmission failed: Invalid GPIO data"}; + return *this; + } + PacketBuilder& gpio_digital_read(const librmcs::data::GpioReadConfigView& data) { + if (data.channel < 1 || data.channel > 7 + || (data.channel == 6 && (data.rising_edge || data.falling_edge)) + || !builder_.write_gpio_digital_read_config(data)) [[unlikely]] + throw std::invalid_argument{ + "GPIO digital read configuration transmission failed: Invalid GPIO data"}; + return *this; + } + PacketBuilder& gpio_analog_write(const librmcs::data::GpioAnalogDataView& data) { + if (data.channel < 1 || data.channel > 7 || !builder_.write_gpio_analog_data(data)) + [[unlikely]] + throw std::invalid_argument{"GPIO analog transmission failed: Invalid GPIO data"}; + return *this; + } + + private: + explicit PacketBuilder(host::protocol::Handler& handler) noexcept + : builder_(handler.start_transmit()) {} + + host::protocol::Handler::PacketBuilder builder_; + }; + PacketBuilder start_transmit() noexcept { return PacketBuilder{handler_}; } + +private: + bool can_receive_callback(data::DataId id, const data::CanDataView& data) final { + switch (id) { + case data::DataId::kCan1: can1_receive_callback(data); return true; + case data::DataId::kCan2: can2_receive_callback(data); return true; + default: return false; + } + } + + virtual void can1_receive_callback(const librmcs::data::CanDataView& data) { (void)data; } + virtual void can2_receive_callback(const librmcs::data::CanDataView& data) { (void)data; } + + bool uart_receive_callback(data::DataId id, const data::UartDataView& data) final { + switch (id) { + case data::DataId::kUartDbus: dbus_receive_callback(data); return true; + case data::DataId::kUart1: uart1_receive_callback(data); return true; + case data::DataId::kUart2: uart2_receive_callback(data); return true; + default: return false; + } + } + + virtual void dbus_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + virtual void uart1_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + virtual void uart2_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + + void + gpio_digital_read_result_callback(const librmcs::data::GpioDigitalDataView& data) override { + (void)data; + } + void gpio_analog_read_result_callback(const librmcs::data::GpioAnalogDataView& data) override { + (void)data; + } + + void accelerometer_receive_callback(const librmcs::data::AccelerometerDataView& data) override { + (void)data; + } + void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { + (void)data; + } + + host::protocol::Handler handler_; +}; + +} // namespace librmcs::agent diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/agent/rmcs_board.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/agent/rmcs_board.hpp new file mode 100644 index 0000000..8a270e9 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/agent/rmcs_board.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include +#include + +#include +#include + +namespace librmcs::agent { + +class RmcsBoard : private data::DataCallback { +public: + explicit RmcsBoard(std::string_view serial_filter = {}) + : handler_(0xA11C, 0xAF01, serial_filter, *this) {} + + RmcsBoard(const RmcsBoard&) = delete; + RmcsBoard& operator=(const RmcsBoard&) = delete; + RmcsBoard(RmcsBoard&&) = delete; + RmcsBoard& operator=(RmcsBoard&&) = delete; + ~RmcsBoard() override = default; + + class PacketBuilder { + friend class RmcsBoard; + + public: + PacketBuilder& can0_transmit(const librmcs::data::CanDataView& data) { + if (!builder_.write_can(data::DataId::kCan0, data)) [[unlikely]] + throw std::invalid_argument{"CAN0 transmission failed: Invalid CAN data"}; + return *this; + } + PacketBuilder& can1_transmit(const librmcs::data::CanDataView& data) { + if (!builder_.write_can(data::DataId::kCan1, data)) [[unlikely]] + throw std::invalid_argument{"CAN1 transmission failed: Invalid CAN data"}; + return *this; + } + PacketBuilder& can2_transmit(const librmcs::data::CanDataView& data) { + if (!builder_.write_can(data::DataId::kCan2, data)) [[unlikely]] + throw std::invalid_argument{"CAN2 transmission failed: Invalid CAN data"}; + return *this; + } + PacketBuilder& can3_transmit(const librmcs::data::CanDataView& data) { + if (!builder_.write_can(data::DataId::kCan3, data)) [[unlikely]] + throw std::invalid_argument{"CAN3 transmission failed: Invalid CAN data"}; + return *this; + } + + PacketBuilder& dbus_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUartDbus, data)) [[unlikely]] + throw std::invalid_argument{"DBUS transmission failed: Invalid UART data"}; + return *this; + } + PacketBuilder& uart0_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUart0, data)) [[unlikely]] + throw std::invalid_argument{"UART0 transmission failed: Invalid UART data"}; + return *this; + } + PacketBuilder& uart1_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUart1, data)) [[unlikely]] + throw std::invalid_argument{"UART1 transmission failed: Invalid UART data"}; + return *this; + } + PacketBuilder& uart2_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUart2, data)) [[unlikely]] + throw std::invalid_argument{"UART2 transmission failed: Invalid UART data"}; + return *this; + } + PacketBuilder& uart3_transmit(const librmcs::data::UartDataView& data) { + if (!builder_.write_uart(data::DataId::kUart3, data)) [[unlikely]] + throw std::invalid_argument{"UART3 transmission failed: Invalid UART data"}; + return *this; + } + + private: + explicit PacketBuilder(host::protocol::Handler& handler) noexcept + : builder_(handler.start_transmit()) {} + + host::protocol::Handler::PacketBuilder builder_; + }; + PacketBuilder start_transmit() noexcept { return PacketBuilder{handler_}; } + +private: + bool can_receive_callback(data::DataId id, const data::CanDataView& data) final { + switch (id) { + case data::DataId::kCan0: can0_receive_callback(data); return true; + case data::DataId::kCan1: can1_receive_callback(data); return true; + case data::DataId::kCan2: can2_receive_callback(data); return true; + case data::DataId::kCan3: can3_receive_callback(data); return true; + default: return false; + } + } + + virtual void can0_receive_callback(const librmcs::data::CanDataView& data) { (void)data; } + virtual void can1_receive_callback(const librmcs::data::CanDataView& data) { (void)data; } + virtual void can2_receive_callback(const librmcs::data::CanDataView& data) { (void)data; } + virtual void can3_receive_callback(const librmcs::data::CanDataView& data) { (void)data; } + + bool uart_receive_callback(data::DataId id, const data::UartDataView& data) final { + switch (id) { + case data::DataId::kUartDbus: dbus_receive_callback(data); return true; + case data::DataId::kUart0: uart0_receive_callback(data); return true; + case data::DataId::kUart1: uart1_receive_callback(data); return true; + case data::DataId::kUart2: uart2_receive_callback(data); return true; + case data::DataId::kUart3: uart3_receive_callback(data); return true; + default: return false; + } + } + + virtual void dbus_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + virtual void uart0_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + virtual void uart1_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + virtual void uart2_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + virtual void uart3_receive_callback(const librmcs::data::UartDataView& data) { (void)data; } + + void + gpio_digital_read_result_callback(const librmcs::data::GpioDigitalDataView& data) override { + (void)data; + } + void gpio_analog_read_result_callback(const librmcs::data::GpioAnalogDataView& data) override { + (void)data; + } + + void accelerometer_receive_callback(const librmcs::data::AccelerometerDataView& data) override { + (void)data; + } + void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { + (void)data; + } + + host::protocol::Handler handler_; +}; + +} // namespace librmcs::agent diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/export.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/export.hpp new file mode 100644 index 0000000..12b4431 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/export.hpp @@ -0,0 +1,19 @@ +#pragma once + +#if defined(_MSC_VER) +# ifdef BUILDING_LIBRMCS +# define LIBRMCS_API __declspec(dllexport) +# elif defined(STATIC_LINKING_LIBRMCS) +# define LIBRMCS_API +# else +# define LIBRMCS_API __declspec(dllimport) +# endif +#elif defined(__GNUC__) || defined(__clang__) +# ifdef BUILDING_LIBRMCS +# define LIBRMCS_API __attribute__((visibility("default"))) +# else +# define LIBRMCS_API +# endif +#else +# define LIBRMCS_API +#endif diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/protocol/handler.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/protocol/handler.hpp new file mode 100644 index 0000000..b40059d --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/include/librmcs/protocol/handler.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include +#include + +namespace librmcs::host::protocol { + +class LIBRMCS_API Handler { +public: + class LIBRMCS_API PacketBuilder { + public: + PacketBuilder(const PacketBuilder&) = delete; + PacketBuilder& operator=(const PacketBuilder&) = delete; + PacketBuilder(PacketBuilder&&) = delete; + PacketBuilder& operator=(PacketBuilder&&) = delete; + + ~PacketBuilder() noexcept; + + bool write_can(data::DataId field_id, const data::CanDataView& view) noexcept; + + bool write_uart(data::DataId field_id, const data::UartDataView& view) noexcept; + + bool write_gpio_digital_data(const data::GpioDigitalDataView& view) noexcept; + + bool write_gpio_digital_read_config(const data::GpioReadConfigView& view) noexcept; + + bool write_gpio_analog_data(const data::GpioAnalogDataView& view) noexcept; + + bool write_imu_accelerometer(const data::AccelerometerDataView& view) noexcept; + + bool write_imu_gyroscope(const data::GyroscopeDataView& view) noexcept; + + private: + friend class Handler; + + explicit PacketBuilder(void* transport) noexcept; + + alignas(std::uintptr_t) std::uint8_t storage_[6 * sizeof(std::uintptr_t)]; + }; + + Handler( + uint16_t usb_vid, int32_t usb_pid, std::string_view serial_filter, + data::DataCallback& callback); + + Handler(const Handler&) = delete; + Handler& operator=(const Handler&) = delete; + Handler(Handler&& other) noexcept; + Handler& operator=(Handler&& other) noexcept; + + ~Handler() noexcept; + + PacketBuilder start_transmit() noexcept; + +private: + class Impl; + Impl* impl_ = nullptr; +}; + +} // namespace librmcs::host::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/logging/logging.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/logging/logging.hpp new file mode 100644 index 0000000..c497d17 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/logging/logging.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "core/src/utility/assert.hpp" + +namespace librmcs::host::logging { + +enum class Level : std::uint8_t { + kTrace = 0, + kDebug = 1, + kInfo = 2, + kWarn = 3, + kErr = 4, + kCritical = 5, + kOff = 6, +}; + +#ifndef LIBRMCS_LOGGING_LEVEL +# define LIBRMCS_LOGGING_LEVEL kInfo +#endif + +class Logger { +public: + static constexpr Level kLoggingLevel = Level::LIBRMCS_LOGGING_LEVEL; + + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + Logger(Logger&&) = delete; + Logger& operator=(Logger&&) = delete; + ~Logger() = default; + +public: // Singleton + static Logger& get_instance() noexcept { + static Logger logger{}; + return logger; + } + +public: // Logging + static constexpr bool should_log(Level level) { return level >= kLoggingLevel; } + +public: // Logging.Formatted + template + void trace(std::format_string fmt, Args&&... args) { + log_internal(Level::kTrace, fmt, std::forward(args)...); + } + + template + void debug(std::format_string fmt, Args&&... args) { + log_internal(Level::kDebug, fmt, std::forward(args)...); + } + + template + void info(std::format_string fmt, Args&&... args) { + log_internal(Level::kInfo, fmt, std::forward(args)...); + } + + template + void warn(std::format_string fmt, Args&&... args) { + log_internal(Level::kWarn, fmt, std::forward(args)...); + } + + template + void error(std::format_string fmt, Args&&... args) { + log_internal(Level::kErr, fmt, std::forward(args)...); + } + + template + void critical(std::format_string fmt, Args&&... args) { + log_internal(Level::kCritical, fmt, std::forward(args)...); + } + + template + void log(Level level, std::format_string fmt, Args&&... args) { + log_internal(level, fmt, std::forward(args)...); + } + +public: // Logging.Raw + template + void trace(const T& msg) { + log_internal(Level::kTrace, msg); + } + + template + void debug(const T& msg) { + log_internal(Level::kDebug, msg); + } + + template + void info(const T& msg) { + log_internal(Level::kInfo, msg); + } + + template + void warn(const T& msg) { + log_internal(Level::kWarn, msg); + } + + template + void error(const T& msg) { + log_internal(Level::kErr, msg); + } + + template + void critical(const T& msg) { + log_internal(Level::kCritical, msg); + } + + template + void log(Level level, const T& msg) { + log_internal(level, msg); + } + +private: + constexpr Logger() noexcept = default; + + template + void log_internal(Level level, std::format_string fmt, Args&&... args) { + if (!should_log(level)) + return; + + print_prefix(level); + std::println(std::cerr, fmt, std::forward(args)...); + } + + template + void log_internal(Level level, const T& msg) { + if (!should_log(level)) + return; + + print_prefix(level); + std::cerr << msg << '\n'; + } + + static void print_prefix(Level level) { + std::string_view level_text = [level]() constexpr -> std::string_view { + if (level == Level::kTrace) + return "trace"; + if (level == Level::kDebug) + return "debug"; + if (level == Level::kInfo) + return "info"; + if (level == Level::kWarn) + return "warn"; + if (level == Level::kErr) + return "error"; + if (level == Level::kCritical) + return "critical"; + core::utility::assert_failed_debug(); + }(); + std::print(std::cerr, "[librmcs] [{}] ", level_text); + } +}; + +inline Logger& get_logger() { return Logger::get_instance(); } + +} // namespace librmcs::host::logging diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/protocol/handler.cpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/protocol/handler.cpp new file mode 100644 index 0000000..8960fc2 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/protocol/handler.cpp @@ -0,0 +1,228 @@ +#include "librmcs/protocol/handler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/src/protocol/deserializer.hpp" +#include "core/src/protocol/protocol.hpp" +#include "core/src/protocol/serializer.hpp" +#include "core/src/utility/assert.hpp" +#include "host/src/logging/logging.hpp" +#include "host/src/protocol/stream_buffer.hpp" +#include "host/src/transport/transport.hpp" +#include "librmcs/data/datas.hpp" + +namespace librmcs::host::protocol { + +class Handler::Impl : public core::protocol::DeserializeCallback { +public: + explicit Impl(std::unique_ptr transport, data::DataCallback& callback) + : transport_(std::move(transport)) + , callback_(callback) + , deserializer_(*this) { + transport_->receive([this](std::span buffer) { + // Operating system automatically assembles the packet + deserializer_.feed(buffer); + deserializer_.finish_transfer(); + }); + } + + PacketBuilder start_transmit() { return PacketBuilder{transport_.get()}; } + + void can_deserialized_callback( + core::protocol::FieldId id, const data::CanDataView& data) override { + if (!callback_.can_receive_callback(id, data)) + logging::get_logger().error("Unexpected can field id: ", static_cast(id)); + } + + void uart_deserialized_callback( + core::protocol::FieldId id, const data::UartDataView& data) override { + if (!callback_.uart_receive_callback(id, data)) + logging::get_logger().error("Unexpected uart field id: ", static_cast(id)); + } + + void gpio_digital_data_deserialized_callback(const data::GpioDigitalDataView& data) override { + callback_.gpio_digital_read_result_callback(data); + } + + void gpio_analog_data_deserialized_callback(const data::GpioAnalogDataView& data) override { + callback_.gpio_analog_read_result_callback(data); + } + + void gpio_digital_read_config_deserialized_callback( + const data::GpioReadConfigView& data) override { + (void)data; + logging::get_logger().error("Unexpected gpio digital read config field in uplink"); + } + + void gpio_analog_read_config_deserialized_callback( + const data::GpioReadConfigView& data) override { + (void)data; + logging::get_logger().error("Unexpected gpio analog read config field in uplink"); + } + + void accelerometer_deserialized_callback(const data::AccelerometerDataView& data) override { + callback_.accelerometer_receive_callback(data); + } + + void gyroscope_deserialized_callback(const data::GyroscopeDataView& data) override { + callback_.gyroscope_receive_callback(data); + } + + void error_callback() override { + logging::get_logger().error("Deserializer encountered an error while parsing input"); + } + +private: + std::unique_ptr transport_; + data::DataCallback& callback_; + core::protocol::Deserializer deserializer_; +}; + +namespace { + +struct PacketBuilderImpl { + explicit PacketBuilderImpl(transport::Transport& transport) noexcept + : buffer_(transport) + , serializer_(buffer_) {} + + PacketBuilderImpl(PacketBuilderImpl&& other) noexcept + : buffer_(std::move(other.buffer_)) + , serializer_(buffer_) {} + + PacketBuilderImpl& operator=(PacketBuilderImpl&&) = delete; + PacketBuilderImpl(const PacketBuilderImpl&) = delete; + PacketBuilderImpl& operator=(const PacketBuilderImpl&) = delete; + ~PacketBuilderImpl() = default; + + // `write_*` returns `true` if args are valid; it never reports transport/resource issues. + // - `kInvalidArgument` => `false` (user error) + // - `kBadAlloc` => logged and ignored (`true`) (internal/transient) + [[nodiscard]] bool write_can(data::DataId field_id, const data::CanDataView& view) noexcept { + return process_result(serializer_.write_can(field_id, view)); + } + + [[nodiscard]] bool write_uart(data::DataId field_id, const data::UartDataView& view) noexcept { + return process_result(serializer_.write_uart(field_id, view)); + } + + [[nodiscard]] bool write_gpio_digital_data(const data::GpioDigitalDataView& view) noexcept { + return process_result(serializer_.write_gpio_digital_data(view)); + } + + [[nodiscard]] bool + write_gpio_digital_read_config(const data::GpioReadConfigView& view) noexcept { + return process_result(serializer_.write_gpio_digital_read_config(view)); + } + + [[nodiscard]] bool write_gpio_analog_data(const data::GpioAnalogDataView& view) noexcept { + return process_result(serializer_.write_gpio_analog_data(view)); + } + + [[nodiscard]] bool write_imu_accelerometer(const data::AccelerometerDataView& view) noexcept { + return process_result(serializer_.write_imu_accelerometer(view)); + } + + [[nodiscard]] bool write_imu_gyroscope(const data::GyroscopeDataView& view) noexcept { + return process_result(serializer_.write_imu_gyroscope(view)); + } + +private: + static bool process_result(core::protocol::Serializer::SerializeResult result) { + using core::protocol::Serializer; + if (result == Serializer::SerializeResult::kSuccess) [[likely]] + return true; + if (result == Serializer::SerializeResult::kBadAlloc) { + logging::get_logger().error("Transmit buffer unavailable (acquire failed)"); + return true; + } + if (result == Serializer::SerializeResult::kInvalidArgument) { + return false; + } + core::utility::assert_failed_debug(); + } + + StreamBuffer buffer_; + core::protocol::Serializer serializer_; +}; + +} // namespace + +Handler::PacketBuilder::PacketBuilder(void* transport_ptr) noexcept { + static_assert(sizeof(PacketBuilderImpl) <= sizeof(storage_)); + static_assert(alignof(PacketBuilderImpl) <= alignof(std::uintptr_t)); + + auto& transport_ref = *static_cast(transport_ptr); + std::construct_at(reinterpret_cast(storage_), transport_ref); +} + +Handler::PacketBuilder::~PacketBuilder() noexcept { + std::destroy_at(std::launder(reinterpret_cast(storage_))); +} + +bool Handler::PacketBuilder::write_can( + data::DataId field_id, const data::CanDataView& view) noexcept { + return std::launder(reinterpret_cast(storage_))->write_can(field_id, view); +} + +bool Handler::PacketBuilder::write_uart( + data::DataId field_id, const data::UartDataView& view) noexcept { + return std::launder(reinterpret_cast(storage_))->write_uart(field_id, view); +} + +bool Handler::PacketBuilder::write_gpio_digital_data( + const data::GpioDigitalDataView& view) noexcept { + return std::launder(reinterpret_cast(storage_)) + ->write_gpio_digital_data(view); +} + +bool Handler::PacketBuilder::write_gpio_digital_read_config( + const data::GpioReadConfigView& view) noexcept { + return std::launder(reinterpret_cast(storage_)) + ->write_gpio_digital_read_config(view); +} + +bool Handler::PacketBuilder::write_gpio_analog_data(const data::GpioAnalogDataView& view) noexcept { + return std::launder(reinterpret_cast(storage_)) + ->write_gpio_analog_data(view); +} + +bool Handler::PacketBuilder::write_imu_accelerometer( + const data::AccelerometerDataView& view) noexcept { + return std::launder(reinterpret_cast(storage_)) + ->write_imu_accelerometer(view); +} + +bool Handler::PacketBuilder::write_imu_gyroscope(const data::GyroscopeDataView& view) noexcept { + return std::launder(reinterpret_cast(storage_))->write_imu_gyroscope(view); +} + +Handler::Handler( + uint16_t usb_vid, int32_t usb_pid, std::string_view serial_filter, data::DataCallback& callback) + : impl_(new Impl(transport::create_usb_transport(usb_vid, usb_pid, serial_filter), callback)) {} + +Handler::Handler(Handler&& other) noexcept + : impl_(std::exchange(other.impl_, nullptr)) {} + +Handler& Handler::operator=(Handler&& other) noexcept { + if (this == &other) + return *this; + delete impl_; + impl_ = std::exchange(other.impl_, nullptr); + return *this; +} + +Handler::~Handler() noexcept { delete impl_; } + +Handler::PacketBuilder Handler::start_transmit() noexcept { + core::utility::assert_debug(impl_); + return impl_->start_transmit(); +} + +} // namespace librmcs::host::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/protocol/stream_buffer.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/protocol/stream_buffer.hpp new file mode 100644 index 0000000..80d8630 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/protocol/stream_buffer.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "core/src/protocol/constant.hpp" +#include "core/src/protocol/serializer.hpp" +#include "core/src/utility/assert.hpp" +#include "host/src/transport/transport.hpp" + +namespace librmcs::host::protocol { + +/** + * @brief A buffered stream writer for efficient sequential data transmission. + * + * StreamBuffer manages a buffer pool from a Transport and provides a + * streaming interface for writing data. It automatically handles buffer + * allocation, switching, and transmission, allowing users to focus on + * writing data without managing buffer lifecycle. + * + * @section Usage Pattern + * 1. Allocate memory via allocate() or allocate_up_to() + * 2. Write data to the returned span + * 3. Repeat steps 1-2 as needed + * 4. Destruction automatically transmits any pending data + * + * @section Buffer Management + * - Buffers are acquired lazily on first allocation + * - When current buffer is full, it's automatically transmitted and replaced + * - Destruction commits any pending buffer with written data + * - All buffer lifecycle is managed internally - no manual cleanup needed + * + * @section Constraints + * - Single allocation size must not exceed kProtocolBufferSize + * - Move-only + * - Not thread-safe - external synchronization required for concurrent access + * + * @section Example + * @code + * StreamBuffer writer(transport); + * + * // Allocate exactly 32 bytes + * auto span1 = writer.allocate(32); + * if (!span1.empty()) { + * // Write data to span1... + * } + * + * // Allocate between 16-128 bytes (gets as much as possible) + * auto span2 = writer.allocate_up_to(16, 128); + * if (!span2.empty()) { + * // Write data to span2... + * } + * + * // Destructor automatically transmits all written data + * @endcode + */ +class StreamBuffer : public core::protocol::SerializeBuffer { +public: + /** + * @brief Constructs a StreamBuffer bound to the specified transport. + * + * @param transport The transport to use for buffer acquisition and transmission. + * Must outlive this StreamBuffer instance. + */ + explicit StreamBuffer(transport::Transport& transport) noexcept + : transport_(transport) {} + + StreamBuffer(StreamBuffer&& other) noexcept + : transport_(other.transport_) + , buffer_(std::move(other.buffer_)) + , current_(other.current_) + , end_(other.end_) { + other.current_ = nullptr; + other.end_ = nullptr; + } + StreamBuffer& operator=(StreamBuffer&&) = delete; + StreamBuffer(const StreamBuffer&) = delete; + StreamBuffer& operator=(const StreamBuffer&) = delete; + + /** + * @brief Destructor. Automatically transmits any pending buffered data. + * + * If a buffer contains written data, it will be transmitted before + * destruction completes. This ensures no data loss on scope exit. + */ + ~StreamBuffer() override { + if (buffer_) + finalize_buffer(); + } + + /** + * @brief Allocates a contiguous memory region of exactly the specified size. + * + * Returns a writable span that remains valid until the next call to + * allocate(), allocate_up_to(), or destruction of this StreamBuffer. + * + * If the current buffer has insufficient space, it will be automatically + * transmitted and a new buffer will be acquired. + * + * @param size Number of bytes to allocate. + * Must be in range (0, kProtocolBufferSize]. + * + * @return A span of exactly 'size' bytes on success, or an empty span + * if buffer acquisition fails (e.g., resource exhaustion) + * + * @pre size must be greater than 0 + * @pre size must not exceed kProtocolBufferSize + * + * @par Example + * @code + * auto span = writer.allocate(64); + * if (!span.empty()) { + * std::memcpy(span.data(), my_data, 64); + * } + * @endcode + */ + std::span allocate(std::size_t size) noexcept override { + core::utility::assert_debug( + 0 < size && size <= core::protocol::kProtocolBufferSize && current_ <= end_); + + if (!buffer_) { + if (!init_buffer()) + return {}; + } else if (std::cmp_less(end_ - current_, size)) { + finalize_buffer(); + if (!init_buffer()) + return {}; + } + + auto* begin = current_; + current_ += size; + return {begin, size}; + } + + /** + * @brief Allocates a memory region with flexible size between min and max. + * + * Attempts to allocate as much memory as possible within the specified + * range. This is useful when you can work with variable amounts of data + * (e.g., bulk operations where more is better but a minimum is required). + * + * Returns the largest available span up to max_size, as long as it's at + * least min_size. The actual size depends on remaining buffer space. + * + * If the current buffer has less than min_size available, it will be + * automatically transmitted and a new buffer will be acquired. + * + * @param min_size Minimum acceptable allocation size. + * Must be in range (0, kProtocolBufferSize]. + * @param max_size Maximum desired allocation size. Must be >= min_size. + * + * @return A span of size in range [min_size, max_size] on success, or an + * empty span if buffer acquisition fails (e.g., resource exhaustion) + * + * @pre min_size must be greater than 0 + * @pre min_size must not exceed kProtocolBufferSize + * @pre max_size must be >= min_size + * + * @par Example + * @code + * // Try to copy up to 256 bytes, but need at least 64 + * auto span = writer.allocate_up_to(64, 256); + * if (!span.empty()) { + * size_t copied = copy_data(span.data(), span.size()); + * // span.size() might be 64, 128, 256, or anywhere in between + * } + * @endcode + */ + std::span allocate_up_to(std::size_t min_size, std::size_t max_size) noexcept { + core::utility::assert_debug( + 0 < min_size && min_size <= core::protocol::kProtocolBufferSize && min_size <= max_size + && current_ <= end_); + + if (!buffer_) { + if (!init_buffer()) + return {}; + } else if (std::cmp_less(end_ - current_, min_size)) { + finalize_buffer(); + if (!init_buffer()) + return {}; + } + + auto* begin = current_; + auto size = std::min(static_cast(end_ - current_), max_size); + current_ += size; + return {begin, size}; + } + +private: + bool init_buffer() noexcept { + core::utility::assert_debug(!buffer_ && !current_ && !end_); + + buffer_ = transport_.acquire_transmit_buffer(); + if (!buffer_) + return false; + + auto data = buffer_->data(); + current_ = data.data(); + end_ = current_ + data.size(); + core::utility::assert_debug(data.size() == core::protocol::kProtocolBufferSize); + + return true; + } + + void finalize_buffer() noexcept { + core::utility::assert_debug(buffer_ && current_ && end_ && current_ <= end_); + + const std::byte* begin = buffer_->data().data(); + const std::size_t payload_size = current_ - begin; + core::utility::assert_debug(payload_size > 0); + + transport_.transmit(std::move(buffer_), payload_size); + buffer_.reset(); + current_ = end_ = nullptr; + } + + transport::Transport& transport_; + + std::unique_ptr buffer_ = nullptr; + std::byte *current_ = nullptr, *end_ = nullptr; +}; + +} // namespace librmcs::host::protocol diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/transport.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/transport.hpp new file mode 100644 index 0000000..117f84d --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/transport.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "core/src/protocol/constant.hpp" + +namespace librmcs::host::transport { + +/** + * @brief Buffer interface for transport operations. + * + * Buffers are acquired from Transport and must be returned to the same + * transport instance through either transmit() or release_transmit_buffer(). + * + * @section Ownership Rules: + * - Buffers must only be destroyed by the transport that created them + * - Destroying a buffer externally results in undefined behavior + * - Passing a buffer to a different transport instance is undefined behavior + * + * @section Memory Guarantees: + * - data() returns a memory region of exactly kProtocolBufferSize bytes + * - data() always returns the same memory region for a given buffer instance + */ +class TransportBuffer { +public: + using BufferSpanType = std::span; + + TransportBuffer() = default; + TransportBuffer(const TransportBuffer&) = delete; + TransportBuffer& operator=(const TransportBuffer&) = delete; + TransportBuffer(TransportBuffer&&) = delete; + TransportBuffer& operator=(TransportBuffer&&) = delete; + virtual ~TransportBuffer() noexcept = default; + + /** + * @brief Returns a mutable view of the buffer's memory region. + * + * The returned span is guaranteed to be exactly kProtocolBufferSize bytes and remains + * valid for the lifetime of this buffer object. Multiple calls to data() + * must return a span pointing to the same underlying memory. + */ + virtual BufferSpanType data() const noexcept = 0; +}; + +/** + * @brief Transport interface for bidirectional data transmission. + * + * @section Buffer Lifecycle: + * - Acquire buffers via acquire_transmit_buffer() + * - Fill buffer with data using TransportBuffer::data() + * - Either transmit the buffer via transmit() or return it via release_transmit_buffer() + * - Never destroy buffers externally - let the transport manage their lifecycle + * + * @section Receive Semantics: + * - receive() may only be called once during the transport's lifetime + * - Once started, reception continues until the transport is destroyed + * - The callback will be invoked for each received data packet + * + * @section Implementation Requirements: + * - Implementations should detect and log errors when buffers are destroyed externally + * - Buffer ownership violations should be treated as programming errors + */ +class Transport { +public: + Transport() = default; + virtual ~Transport() noexcept = default; + Transport(const Transport&) = delete; + Transport& operator=(const Transport&) = delete; + Transport(Transport&&) = delete; + Transport& operator=(Transport&&) = delete; + + /** + * @brief Acquires a buffer for transmission. + * + * The returned buffer is owned by the caller and must be passed back to + * this transport via either transmit() or release_transmit_buffer(). + * + * @return A buffer with exactly kProtocolBufferSize bytes of writable memory, or nullptr + * if buffer acquisition fails (e.g., resource exhaustion) + */ + virtual std::unique_ptr acquire_transmit_buffer() noexcept = 0; + + /** + * @brief Transmits data from the provided buffer. + * + * Takes ownership of the buffer and queues it for transmission. The buffer + * must have been acquired from this transport instance via acquire_transmit_buffer(). + * + * @param buffer Buffer containing the data to transmit (ownership transferred) + * @param payload_size Number of bytes to transmit from the buffer (must not exceed buffer size) + * + * @section Preconditions: + * - buffer must be non-null and acquired from this transport + * - payload_size must be <= buffer capacity + */ + virtual void transmit(std::unique_ptr buffer, size_t payload_size) = 0; + + /** + * @brief Returns an unused buffer back to the transport. + * + * Use this to release a buffer that was acquired but will not be transmitted. + * Takes ownership of the buffer and returns it to the transport's buffer pool. + * + * @param buffer Buffer to release (ownership transferred) + * + * @section Preconditions: + * - buffer must be non-null and acquired from this transport + * - buffer must not have been previously transmitted or released + */ + virtual void release_transmit_buffer(std::unique_ptr buffer) = 0; + + /** + * @brief Starts receiving data with the provided callback. + * + * This function may only be called once during the transport's lifetime. + * Once called, the transport will invoke the callback for each received + * data packet until the transport is destroyed. + * + * @param callback Function invoked for each received data packet. + * The span is valid only for the duration of the callback. + * + * @section Preconditions: + * - Must be called at most once per transport instance + * - callback must be a valid callable object + * + * @section Thread Safety: + * The callback will be invoked from at most one thread at any given time. + * Concurrent invocations are guaranteed not to occur. However, the callback + * may be invoked from any thread, and different invocations may use different + * threads. Callers are responsible for thread synchronization if state is + * shared with other threads. + */ + virtual void receive(std::function)> callback) = 0; +}; + +std::unique_ptr + create_usb_transport(uint16_t usb_vid, int32_t usb_pid, std::string_view serial_filter); + +} // namespace librmcs::host::transport diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/device_scanner.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/device_scanner.hpp new file mode 100644 index 0000000..b202a6c --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/device_scanner.hpp @@ -0,0 +1,422 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "core/src/utility/assert.hpp" +#include "host/src/transport/usb/helper.hpp" +#include "host/src/utility/final_action.hpp" + +namespace librmcs::host::transport::usb { + +class DeviceScanner { +public: + static libusb_device_handle* select_device( + libusb_context* context, uint16_t vendor_id, int32_t product_id, + std::string_view serial_filter) { + libusb_device** device_list = nullptr; + const ssize_t device_count = libusb_get_device_list(context, &device_list); + if (device_count < 0) { + const int error_code = static_cast(device_count); + throw std::runtime_error( + std::format( + "Failed to get USB device list: {} ({})", error_code, + helper::libusb_errname(error_code))); + } + + const utility::FinalAction free_device_list{ + [&device_list]() noexcept { libusb_free_device_list(device_list, 1); }}; + + auto infos = scan_devices(device_list, device_count, vendor_id, product_id, serial_filter); + + std::vector devices_opened; + for (const auto& info : infos) { + if (const auto* matched = std::get_if(&info.result)) + devices_opened.push_back(matched->handle); + } + + if (devices_opened.size() != 1) { + for (auto& device : devices_opened) + libusb_close(device); + + const bool multiple_compatible_devices = devices_opened.size() > 1; + const auto report = generate_device_discovery_report( + infos, vendor_id, product_id, serial_filter, multiple_compatible_devices); + throw std::runtime_error( + std::format( + "{}\n\n{}", + multiple_compatible_devices ? "Multiple compatible devices found." + : "No compatible device found.", + report)); + } + + return devices_opened[0]; + } + +private: + struct DeviceInfo { + libusb_device* device = nullptr; + libusb_device_descriptor descriptor = {}; + std::string serial_number; + + struct Matching {}; + + struct DescriptorReadFailed { + int libusb_error_number = 0; + }; + struct DescriptorEmpty {}; + + struct VendorMismatch {}; + struct ProductMismatch {}; + struct OpenFailed { + int libusb_error_number = 0; + }; + + struct MissingSerialNumber {}; + struct SerialReadFailed { + int libusb_error_number = 0; + }; + struct SerialFilterMismatch {}; + + struct MissingProductString {}; + struct ProductStringReadFailed { + int libusb_error_number; + }; + struct ProtocolVersionMismatch { + std::string product_string; + }; + + struct Matched { + libusb_device_handle* handle = nullptr; + }; + + using MatchResult = std::variant< + Matching, DescriptorReadFailed, DescriptorEmpty, VendorMismatch, MissingSerialNumber, + ProductMismatch, OpenFailed, SerialReadFailed, SerialFilterMismatch, + MissingProductString, ProductStringReadFailed, ProtocolVersionMismatch, Matched>; + MatchResult result; + }; + + struct DiscoveryTroubleshooting { + bool access_denied = false; + bool device_busy = false; + bool pid_mismatch = false; + bool serial_mismatch = false; + bool firmware_mismatch = false; + }; + + static std::vector scan_devices( + libusb_device** device_list, ssize_t device_count, uint16_t vendor_id, int32_t product_id, + std::string_view serial_filter) { + std::vector infos; + infos.reserve(static_cast(device_count)); + + for (ssize_t i = 0; i < device_count; i++) { + auto& info = infos.emplace_back(); + info.device = device_list[i]; + + if (const int ret = libusb_get_device_descriptor(device_list[i], &info.descriptor); + ret != 0) { + info.result = DeviceInfo::DescriptorReadFailed{ret}; + continue; + } + if (info.descriptor.bLength == 0) { + info.result = DeviceInfo::DescriptorEmpty{}; + continue; + } + + if (info.descriptor.idVendor != vendor_id) { + info.result = DeviceInfo::VendorMismatch{}; + continue; + } + if (product_id >= 0 && !std::cmp_equal(info.descriptor.idProduct, product_id)) { + info.result = DeviceInfo::ProductMismatch{}; + continue; + } + + libusb_device_handle* handle = nullptr; + if (const int ret = libusb_open(device_list[i], &handle); ret != 0) { + info.result = DeviceInfo::OpenFailed{ret}; + continue; + } + utility::FinalAction close_device{[&handle]() noexcept { libusb_close(handle); }}; + + if (!read_and_match_serial_number(handle, serial_filter, info)) + continue; + + if (!match_product_version(handle, info)) + continue; + + info.result = DeviceInfo::Matched{handle}; + close_device.disable(); + } + + return infos; + } + + static bool read_and_match_serial_number( + libusb_device_handle* handle, const std::string_view& serial_filter, DeviceInfo& info) { + if (info.descriptor.iSerialNumber == 0) { + info.result = DeviceInfo::MissingSerialNumber{}; + return false; + } + + char serial_buf[256]; + const int n = libusb_get_string_descriptor_ascii( + handle, info.descriptor.iSerialNumber, reinterpret_cast(serial_buf), + sizeof(serial_buf)); + if (n < 0) { + info.result = DeviceInfo::SerialReadFailed{n}; + return false; + } + + info.serial_number = {serial_buf, static_cast(n)}; + + if (!serial_filter.empty() + && !match_filter(serial_filter, std::string_view{info.serial_number})) { + info.result = DeviceInfo::SerialFilterMismatch{}; + return false; + } + return true; + } + + static constexpr bool match_filter(std::string_view filter, std::string_view target) { + const auto *filter_it = filter.cbegin(), *filter_end = filter.cend(); + const auto *target_it = target.cbegin(), *target_end = target.cend(); + + const auto to_upper = [](char c) constexpr { + if (c >= 'a' && c <= 'z') + return static_cast(c - 'a' + 'A'); + return c; + }; + + while (filter_it != filter_end && target_it != target_end) { + if (*filter_it == '-') { + ++filter_it; + continue; + } + if (*target_it == '-') { + ++target_it; + continue; + } + + if (to_upper(*filter_it) != to_upper(*target_it)) + return false; + + ++filter_it; + ++target_it; + } + + while (filter_it != filter_end && *filter_it == '-') { + ++filter_it; + } + + return filter_it == filter_end; + } + + static bool match_product_version(libusb_device_handle* handle, DeviceInfo& info) { + if (info.descriptor.iProduct == 0) { + info.result = DeviceInfo::MissingProductString{}; + return false; + } + + char product_buf[256]; + const int n = libusb_get_string_descriptor_ascii( + handle, info.descriptor.iProduct, reinterpret_cast(product_buf), + sizeof(product_buf)); + if (n < 0) { + info.result = DeviceInfo::ProductStringReadFailed{n}; + return false; + } + + const std::string_view product_string{product_buf, static_cast(n)}; + if (product_string != "RMCS Agent v" LIBRMCS_PROJECT_VERSION_STRING) { + info.result = DeviceInfo::ProtocolVersionMismatch{std::string{product_string}}; + return false; + } + + return true; + } + + static std::string generate_device_discovery_report( + std::span infos, uint16_t vendor_id, int32_t product_id, + std::string_view serial_filter, bool multiple_compatible_devices) { + const std::string pid_text = product_id >= 0 ? std::format("0x{:04x}", product_id) : "Any"; + const std::string serial_filter_text = + serial_filter.empty() ? "Any" : std::string{serial_filter}; + + std::string report = std::format( + "Target Specs:\n" + "- VID: 0x{:04x}\n" + "- PID: {}\n" + "- Serial Filter: {}\n" + "- Firmware: v{}\n\n" + "Discovered Devices:\n", + vendor_id, pid_text, serial_filter_text, LIBRMCS_PROJECT_VERSION_STRING); + + DiscoveryTroubleshooting troubleshooting = {}; + std::size_t reported_device_count = 0; + std::size_t matched_count = 0; + + for (const auto& info : infos) { + if (std::holds_alternative(info.result)) + continue; + + report.append(std::format("- {}\n", format_device_summary(info))); + report.append( + std::format( + " -> {}\n", + describe_device_match_result(info, matched_count, troubleshooting))); + reported_device_count++; + } + + if (reported_device_count == 0) + report.append("- No relevant devices discovered.\n"); + + append_troubleshooting_section(report, troubleshooting, multiple_compatible_devices); + return report; + } + + static std::string format_device_summary(const DeviceInfo& info) { + core::utility::assert_debug(info.device); + std::string summary = std::format( + "Bus {:03} Dev {:03}", static_cast(libusb_get_bus_number(info.device)), + static_cast(libusb_get_device_address(info.device))); + + const bool has_descriptor = + !std::holds_alternative(info.result) + && !std::holds_alternative(info.result); + if (!has_descriptor) + return summary; + + summary.append( + std::format(" (ID {:04x}:{:04x}", info.descriptor.idVendor, info.descriptor.idProduct)); + if (!info.serial_number.empty()) + summary.append(std::format(", SN {}", info.serial_number)); + summary.push_back(')'); + + return summary; + } + + static std::string describe_device_match_result( + const DeviceInfo& info, std::size_t& matched_count, + DiscoveryTroubleshooting& troubleshooting) { + if (std::holds_alternative(info.result)) { + core::utility::assert_failed_debug(); + return "Ignored: Internal matching state error"; + } + if (const auto* descriptor_read_failed = + std::get_if(&info.result)) { + return std::format( + "Ignored: USB descriptor unreadable ({} / {})", + descriptor_read_failed->libusb_error_number, + helper::libusb_errname(descriptor_read_failed->libusb_error_number)); + } + if (std::holds_alternative(info.result)) + return "Ignored: USB descriptor empty"; + if (std::holds_alternative(info.result)) + return "Ignored: VID mismatch"; + if (std::holds_alternative(info.result)) { + troubleshooting.pid_mismatch = true; + return "Ignored: PID mismatch"; + } + if (const auto* open_failed = std::get_if(&info.result)) { + if (open_failed->libusb_error_number == LIBUSB_ERROR_ACCESS) { + troubleshooting.access_denied = true; + return std::format( + "Ignored: Access denied ({} / {})", open_failed->libusb_error_number, + helper::libusb_errname(open_failed->libusb_error_number)); + } + if (open_failed->libusb_error_number == LIBUSB_ERROR_BUSY) { + troubleshooting.device_busy = true; + return std::format( + "Ignored: Device busy ({} / {})", open_failed->libusb_error_number, + helper::libusb_errname(open_failed->libusb_error_number)); + } + + return std::format( + "Ignored: Open failed ({} / {})", open_failed->libusb_error_number, + helper::libusb_errname(open_failed->libusb_error_number)); + } + if (std::holds_alternative(info.result)) + return "Ignored: Missing serial number"; + if (const auto* serial_read_failed = + std::get_if(&info.result)) { + return std::format( + "Ignored: Serial number read failed ({} / {})", + serial_read_failed->libusb_error_number, + helper::libusb_errname(serial_read_failed->libusb_error_number)); + } + if (std::holds_alternative(info.result)) { + troubleshooting.serial_mismatch = true; + return "Ignored: Serial mismatch"; + } + if (std::holds_alternative(info.result)) + return "Ignored: Missing product string"; + if (const auto* product_read_failed = + std::get_if(&info.result)) { + return std::format( + "Ignored: Product string read failed ({} / {})", + product_read_failed->libusb_error_number, + helper::libusb_errname(product_read_failed->libusb_error_number)); + } + if (const auto* protocol_mismatch = + std::get_if(&info.result)) { + troubleshooting.firmware_mismatch = true; + return std::format( + "Ignored: Firmware mismatch (Found: {})", protocol_mismatch->product_string); + } + if (std::holds_alternative(info.result)) { + matched_count++; + return std::format("Matched #{}", matched_count); + } + core::utility::assert_failed_debug(); + } + + static void append_troubleshooting_section( + std::string& report, const DiscoveryTroubleshooting& troubleshooting, + bool multiple_compatible_devices) { + report.append("\nTroubleshooting:\n"); + + bool has_any_hints = false; + const auto append_hint = [&report, &has_any_hints](std::string_view hint) { + has_any_hints = true; + report.append(std::format("- {}\n", hint)); + }; + + if (troubleshooting.access_denied) + append_hint("Access denied: Check USB permissions (e.g., udev rules)."); + if (troubleshooting.device_busy) + append_hint("Device busy: Close other processes using the device."); + if (troubleshooting.pid_mismatch) + append_hint("PID mismatch: Check board type selection."); + if (troubleshooting.serial_mismatch) + append_hint("Serial mismatch: Verify target SN or loosen the serial filter."); + if (troubleshooting.firmware_mismatch) { + append_hint( + std::format( + "Firmware mismatch: Flash device firmware to v{}.", + LIBRMCS_PROJECT_VERSION_STRING)); + } + if (multiple_compatible_devices) { + append_hint( + "Multiple matches: Provide a stricter serial filter to target a specific device."); + } + + if (!has_any_hints) + report.append("- No additional hints.\n"); + } +}; + +} // namespace librmcs::host::transport::usb diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/helper.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/helper.hpp new file mode 100644 index 0000000..838534b --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/helper.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace librmcs::host::transport::usb::helper { + +constexpr const char* libusb_errname(int number) { + switch (number) { + case LIBUSB_ERROR_IO: return "ERROR_IO"; + case LIBUSB_ERROR_INVALID_PARAM: return "ERROR_INVALID_PARAM"; + case LIBUSB_ERROR_ACCESS: return "ERROR_ACCESS"; + case LIBUSB_ERROR_NO_DEVICE: return "ERROR_NO_DEVICE"; + case LIBUSB_ERROR_NOT_FOUND: return "ERROR_NOT_FOUND"; + case LIBUSB_ERROR_BUSY: return "ERROR_BUSY"; + case LIBUSB_ERROR_TIMEOUT: return "ERROR_TIMEOUT"; + case LIBUSB_ERROR_OVERFLOW: return "ERROR_OVERFLOW"; + case LIBUSB_ERROR_PIPE: return "ERROR_PIPE"; + case LIBUSB_ERROR_INTERRUPTED: return "ERROR_INTERRUPTED"; + case LIBUSB_ERROR_NO_MEM: return "ERROR_NO_MEM"; + case LIBUSB_ERROR_NOT_SUPPORTED: return "ERROR_NOT_SUPPORTED"; + case LIBUSB_ERROR_OTHER: return "ERROR_OTHER"; + default: return "UNKNOWN"; + } +} + +} // namespace librmcs::host::transport::usb::helper diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/usb.cpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/usb.cpp new file mode 100644 index 0000000..54954fe --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/transport/usb/usb.cpp @@ -0,0 +1,360 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "core/src/protocol/constant.hpp" +#include "core/src/utility/assert.hpp" +#include "host/src/logging/logging.hpp" +#include "host/src/transport/transport.hpp" +#include "host/src/transport/usb/device_scanner.hpp" +#include "host/src/transport/usb/helper.hpp" +#include "host/src/utility/final_action.hpp" +#include "host/src/utility/ring_buffer.hpp" + +namespace librmcs::host::transport { +namespace usb { + +class Usb : public Transport { +public: + explicit Usb(uint16_t usb_vid, int32_t usb_pid, std::string_view serial_filter) + : logger_(logging::get_logger()) + , free_transmit_transfers_(kTransmitTransferCount) { + usb_init(usb_vid, usb_pid, serial_filter); + utility::FinalAction rollback_on_failure{[this]() noexcept { + destroy_free_transmit_transfers(); + libusb_release_interface(libusb_device_handle_, kTargetInterface); + libusb_close(libusb_device_handle_); + libusb_exit(libusb_context_); + }}; + + init_transmit_transfers(); + event_thread_ = std::thread{[this]() { handle_events(); }}; + + rollback_on_failure.disable(); + } + + Usb(const Usb&) = delete; + Usb& operator=(const Usb&) = delete; + Usb(Usb&&) = delete; + Usb& operator=(Usb&&) = delete; + + ~Usb() override { + { + const std::scoped_lock guard{transmit_transfer_push_mutex_}; + stop_handling_events_.store(true, std::memory_order::relaxed); + } + destroy_free_transmit_transfers(); + + libusb_release_interface(libusb_device_handle_, kTargetInterface); + + // libusb_close() reliably cancels all pending transfers and invokes their callbacks, + // avoiding race conditions present in other cancellation methods + libusb_close(libusb_device_handle_); + + if (event_thread_.joinable()) + event_thread_.join(); + + libusb_exit(libusb_context_); + } + + std::unique_ptr acquire_transmit_buffer() noexcept override { + TransferWrapper* transfer = nullptr; + { + const std::scoped_lock guard{transmit_transfer_pop_mutex_}; + free_transmit_transfers_.pop_front( + [&transfer](TransferWrapper* value) noexcept { transfer = value; }); + } + if (!transfer) + return nullptr; + + return std::unique_ptr{transfer}; + } + + void transmit(std::unique_ptr buffer, size_t size) override { + core::utility::assert_debug(static_cast(buffer)); + + if (size > core::protocol::kProtocolBufferSize) + throw std::invalid_argument("Transmit size exceeds maximum transfer length"); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) + auto& transfer = static_cast(buffer.get())->transfer_; + transfer->length = static_cast(size); + + int ret = libusb_submit_transfer(transfer); + if (ret != 0) [[unlikely]] { + throw std::runtime_error( + std::format( + "Failed to submit transmit transfer: {} ({})", ret, + helper::libusb_errname(ret))); + } + + // If success: Ownership is transferred to libusb + std::ignore = buffer.release(); + } + + void release_transmit_buffer(std::unique_ptr buffer) override { + core::utility::assert_debug(static_cast(buffer)); + + const std::scoped_lock guard{transmit_transfer_push_mutex_}; + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) + auto* wrapper = static_cast(buffer.release()); + free_transmit_transfers_.emplace_back(wrapper); + } + + void receive(std::function)> callback) override { + if (!callback) + throw std::invalid_argument{"Callback function cannot be null"}; + if (receive_callback_) + throw std::logic_error{"Receive function can only be called once"}; + + receive_callback_ = std::move(callback); + init_receive_transfers(); + } + +private: + class TransferWrapper : public TransportBuffer { + friend class Usb; + + public: + explicit TransferWrapper(Usb& self) + : self_(self) + , transfer_(self_.create_libusb_transfer()) {} + + TransferWrapper(const TransferWrapper&) = delete; + TransferWrapper& operator=(const TransferWrapper&) = delete; + TransferWrapper(TransferWrapper&&) = delete; + TransferWrapper& operator=(TransferWrapper&&) = delete; + + ~TransferWrapper() override { + if (transfer_) { + logging::get_logger().error( + "USB TransferBuffer {} was destroyed externally - this is undefined behavior. " + "Buffers must be returned via transmit() or " + "release_transmit_buffer(). ", + static_cast(this)); + destroy(); + } + } + + BufferSpanType data() const noexcept override { + return BufferSpanType{ + reinterpret_cast(transfer_->buffer), + core::protocol::kProtocolBufferSize}; + } + + void destroy() noexcept { + self_.destroy_libusb_transfer(transfer_); + transfer_ = nullptr; + } + + private: + Usb& self_; + + libusb_transfer* transfer_; + }; + + void usb_init(uint16_t vendor_id, int32_t product_id, std::string_view serial_filter) { + if (const int ret = libusb_init(&libusb_context_); ret != 0) [[unlikely]] { + throw std::runtime_error( + std::format( + "Failed to initialize libusb: {} ({})", ret, helper::libusb_errname(ret))); + } + utility::FinalAction exit_libusb{[this]() noexcept { libusb_exit(libusb_context_); }}; + + libusb_device_handle_ = + DeviceScanner::select_device(libusb_context_, vendor_id, product_id, serial_filter); + utility::FinalAction close_device_handle{ + [this]() noexcept { libusb_close(libusb_device_handle_); }}; + + if (const int ret = libusb_claim_interface(libusb_device_handle_, kTargetInterface); + ret != 0) [[unlikely]] { + throw std::runtime_error( + std::format( + "Failed to claim interface {}: {} ({})", kTargetInterface, ret, + helper::libusb_errname(ret))); + } + + // Libusb successfully initialized + close_device_handle.disable(); + exit_libusb.disable(); + } + + void init_transmit_transfers() { + TransferWrapper* transmit_transfers[kTransmitTransferCount] = {}; + try { + for (auto& wrapper : transmit_transfers) { + wrapper = new TransferWrapper{*this}; + auto* transfer = wrapper->transfer_; + + libusb_fill_bulk_transfer( + transfer, libusb_device_handle_, kOutEndpoint, + new unsigned char[core::protocol::kProtocolBufferSize], 0, + [](libusb_transfer* transfer) { + auto* wrapper = static_cast(transfer->user_data); + wrapper->self_.usb_transmit_complete_callback(wrapper); + }, + wrapper, 0); + transfer->flags = libusb_transfer_flags::LIBUSB_TRANSFER_FREE_BUFFER; + } + } catch (...) { + for (auto& wrapper : transmit_transfers) { + if (wrapper) { + wrapper->destroy(); + delete wrapper; + } + } + throw; + } + + auto* iter = transmit_transfers; + free_transmit_transfers_.push_back_n( + [&iter]() noexcept { return *iter++; }, kTransmitTransferCount); + } + + void handle_events() { + while (active_transfers_.load(std::memory_order::relaxed)) { + libusb_handle_events(libusb_context_); + } + } + + void init_receive_transfers() { + for (size_t i = 0; i < kReceiveTransferCount; i++) { + auto* transfer = create_libusb_transfer(); + + libusb_fill_bulk_transfer( + transfer, libusb_device_handle_, kInEndpoint, + new unsigned char[core::protocol::kProtocolBufferSize], + static_cast(core::protocol::kProtocolBufferSize), + [](libusb_transfer* transfer) { + static_cast(transfer->user_data)->usb_receive_complete_callback(transfer); + }, + this, 0); + transfer->flags = libusb_transfer_flags::LIBUSB_TRANSFER_FREE_BUFFER; + + int ret = libusb_submit_transfer(transfer); + if (ret != 0) [[unlikely]] { + destroy_libusb_transfer(transfer); + throw std::runtime_error( + std::format( + "Failed to submit receive transfer: {} ({})", ret, + helper::libusb_errname(ret))); + } + } + } + + void usb_transmit_complete_callback(TransferWrapper* wrapper) { + // Share mutex with teardown so destructor can block callbacks before draining the queue + const std::scoped_lock guard{transmit_transfer_push_mutex_}; + + if (stop_handling_events_.load(std::memory_order::relaxed)) [[unlikely]] { + wrapper->destroy(); + delete wrapper; + return; + } + + free_transmit_transfers_.emplace_back(wrapper); + } + + void usb_receive_complete_callback(libusb_transfer* transfer) { + if (stop_handling_events_.load(std::memory_order::relaxed)) [[unlikely]] { + destroy_libusb_transfer(transfer); + return; + } + + const auto now = std::chrono::steady_clock::now(); + const bool should_drop = now > last_rx_callback_timepoint_ + std::chrono::seconds{1}; + last_rx_callback_timepoint_ = now; + + if (!should_drop && transfer->actual_length > 0) { + const auto* first = reinterpret_cast(transfer->buffer); + const auto size = static_cast(transfer->actual_length); + receive_callback_({first, size}); + } + + int ret = libusb_submit_transfer(transfer); + if (ret != 0) [[unlikely]] { + if (ret == LIBUSB_ERROR_NO_DEVICE) + logger_.error( + "Failed to re-submit receive transfer: Device disconnected. " + "Terminating..."); + else + logger_.error( + "Failed to re-submit receive transfer: {} ({}). Terminating...", ret, + helper::libusb_errname(ret)); + destroy_libusb_transfer(transfer); + + // TODO: Replace abrupt termination with a flag and exception-based error handling + std::terminate(); + } + } + + void destroy_free_transmit_transfers() noexcept { + free_transmit_transfers_.pop_front_n([](TransferWrapper* wrapper) noexcept { + wrapper->destroy(); + delete wrapper; + }); + } + + libusb_transfer* create_libusb_transfer() { + auto* transfer = libusb_alloc_transfer(0); + if (!transfer) + throw std::bad_alloc{}; + active_transfers_.fetch_add(1, std::memory_order::relaxed); + return transfer; + } + + void destroy_libusb_transfer(libusb_transfer* transfer) noexcept { + libusb_free_transfer(transfer); + active_transfers_.fetch_sub(1, std::memory_order::relaxed); + } + + static constexpr int kTargetInterface = 0x00; + + static constexpr unsigned char kOutEndpoint = 0x01; + static constexpr unsigned char kInEndpoint = 0x81; + + static constexpr size_t kTransmitTransferCount = 64; + static constexpr size_t kReceiveTransferCount = 4; + + logging::Logger& logger_; + + libusb_context* libusb_context_ = nullptr; + libusb_device_handle* libusb_device_handle_ = nullptr; + + std::thread event_thread_; + + std::atomic active_transfers_ = 0; + std::atomic stop_handling_events_ = false; + + utility::RingBuffer free_transmit_transfers_; + std::mutex transmit_transfer_pop_mutex_, transmit_transfer_push_mutex_; + + std::function)> receive_callback_; + std::chrono::steady_clock::time_point last_rx_callback_timepoint_ = + std::chrono::steady_clock::time_point::min(); +}; + +} // namespace usb + +std::unique_ptr + create_usb_transport(uint16_t usb_vid, int32_t usb_pid, std::string_view serial_filter) { + return std::make_unique(usb_vid, usb_pid, serial_filter); +} + +} // namespace librmcs::host::transport diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/assert.cpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/assert.cpp new file mode 100644 index 0000000..51479f7 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/assert.cpp @@ -0,0 +1,17 @@ +#include "core/src/utility/assert.hpp" + +#include +#include +#include +#include + +namespace librmcs::core::utility { + +[[noreturn]] void assert_func(const std::source_location& location) { + std::println( + std::cerr, "Assertion failed at {}:{} in function {}", location.file_name(), + location.line(), location.function_name()); + std::terminate(); +} + +} // namespace librmcs::core::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/cross_os.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/cross_os.hpp new file mode 100644 index 0000000..5f465f7 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/cross_os.hpp @@ -0,0 +1,35 @@ +#pragma once + +namespace librmcs::host::utility { + +#ifdef _MSC_VER +# define PACKED_STRUCT(...) __pragma(pack(push, 1)) struct __VA_ARGS__ __pragma(pack(pop)) +#elif defined(__GNUC__) +# define PACKED_STRUCT(...) struct __attribute__((packed)) __VA_ARGS__ +#endif + +constexpr static bool is_linux() { +#ifdef __linux__ + return true; +#else + return false; +#endif +} + +constexpr static bool is_windows() { +#if defined(_WIN32) || defined(WIN32) + return true; +#else + return false; +#endif +} + +#if defined(_MSC_VER) +# define ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) || defined(__clang__) +# define ALWAYS_INLINE __attribute__((always_inline)) inline +#else +# define ALWAYS_INLINE inline +#endif + +} // namespace librmcs::host::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/final_action.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/final_action.hpp new file mode 100644 index 0000000..e3f079c --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/final_action.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace librmcs::host::utility { + +template +requires requires(Functor& action) { + { action() } noexcept; +} struct FinalAction { + constexpr explicit FinalAction(Functor action) + : action_{std::move(action)} {} + + constexpr FinalAction(const FinalAction&) = delete; + constexpr FinalAction& operator=(const FinalAction&) = delete; + constexpr FinalAction(FinalAction&&) = delete; + constexpr FinalAction& operator=(FinalAction&&) = delete; + + ~FinalAction() noexcept { + if (enabled_) { + action_(); + } + } + + void disable() { enabled_ = false; } + +private: + bool enabled_{true}; + Functor action_; +}; + +} // namespace librmcs::host::utility diff --git a/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/ring_buffer.hpp b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/ring_buffer.hpp new file mode 100644 index 0000000..9a8cb39 --- /dev/null +++ b/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b/host/src/utility/ring_buffer.hpp @@ -0,0 +1,277 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace librmcs::host::utility { + +// Lock-free Single-Producer/Single-Consumer (SPSC) ring buffer +// Inspired by Linux kfifo. +template +class RingBuffer { +public: + /*! + * @brief Construct an SPSC ring buffer + * @param size Minimum capacity requested. Actual capacity is rounded up + * to the next power of two and clamped to at least 2. + * @note This data structure is single-producer/single-consumer. Only one + * thread may push, and only one thread may pop, at a time. + */ + explicit RingBuffer(size_t size) { + if (size <= 2) + size = 2; + else + size = round_up_to_next_power_of_2(size); + mask_ = size - 1; + storage_ = new Storage[size]; + } + + RingBuffer(const RingBuffer&) = delete; + RingBuffer& operator=(const RingBuffer&) = delete; + RingBuffer(RingBuffer&&) = delete; + RingBuffer& operator=(RingBuffer&&) = delete; + + /*! + * @brief Destructor + * Destroys all elements remaining in the buffer and frees storage. + */ + ~RingBuffer() { + clear(); + delete[] storage_; + } + + /*! + * @brief Capacity of the ring buffer + * @return Total number of slots (power of two) + */ + size_t max_size() const { return mask_ + 1; } + + /*! + * @brief Number of elements currently readable + * @return Count of elements available to the consumer + * @note Uses acquire on producer index and relaxed on consumer index to + * ensure visibility of constructed elements to the consumer. + */ + size_t readable() const { + const auto in = in_.load(std::memory_order::acquire); + const auto out = out_.load(std::memory_order::relaxed); + return in - out; + } + + /*! + * @brief Number of free slots for producer + * @return Count of slots available to write + * @note Uses relaxed on producer index and acquire on consumer index to + * avoid overrun while allowing the producer to run without contention. + */ + size_t writable() const { + const auto in = in_.load(std::memory_order::relaxed); + const auto out = out_.load(std::memory_order::acquire); + return max_size() - (in - out); + } + + /*! + * @brief Peek the first element (consumer side) + * @return Pointer to the first element, or nullptr if empty + * @warning Do not call from producer thread. The pointer remains valid + * until the element is popped or overwritten. + */ + T* peek_front() { + const auto out = out_.load(std::memory_order::relaxed); + + if (out == in_.load(std::memory_order::acquire)) + return nullptr; + + return std::launder(reinterpret_cast(storage_[out & mask_].data)); + } + + /*! + * @brief Peek the last produced element (consumer side) + * @return Pointer to the last element, or nullptr if empty + * @warning Do not call from producer thread. The pointer remains valid + * until the element is popped or overwritten. + */ + T* peek_back() { + const auto in = in_.load(std::memory_order::acquire); + + if (in == out_.load(std::memory_order::relaxed)) + return nullptr; + + return std::launder(reinterpret_cast(storage_[(in - 1) & mask_].data)); + } + + /*! + * @brief Batch-construct elements at the tail (producer) + * @tparam F Functor with signature `void(std::byte* storage)` that constructs + * a `T` in-place via placement-new. + * @param count Maximum number of elements to construct (defaults to as many as fit) + * @return Number of elements actually constructed + * @note Producer-only. Publishes with release semantics. + */ + template + requires requires(F& f, std::byte* storage) { + { f(storage) } noexcept; + } + size_t emplace_back_n(F construct_functor, size_t count = std::numeric_limits::max()) { + const auto in = in_.load(std::memory_order::relaxed); + const auto out = out_.load(std::memory_order::acquire); + + const auto writable = max_size() - (in - out); + + if (count > writable) + count = writable; + if (!count) + return 0; + + const auto offset = in & mask_; + const auto slice = std::min(count, max_size() - offset); + + for (size_t i = 0; i < slice; i++) + construct_functor(storage_[offset + i].data); + for (size_t i = 0; i < count - slice; i++) + construct_functor(storage_[i].data); + + in_.store(in + count, std::memory_order::release); + + return count; + } + + /*! + * @brief Construct one element in-place at the tail (producer) + * @return true if pushed, false if buffer is full + */ + template + bool emplace_back(Args&&... args) { + return emplace_back_n( + [&](std::byte* storage) noexcept(noexcept(T{std::forward(args)...})) { + new (storage) T{std::forward(args)...}; + }, + 1); + } + + /*! + * @brief Batch-push using a generator (producer) + * @tparam F Functor returning a `T` to be stored + * @param count Maximum number to generate/push + * @return Number of elements actually pushed + */ + template + requires requires(F& f) { + { f() } noexcept; + { T{f()} } noexcept; + } size_t push_back_n(F generator, size_t count = std::numeric_limits::max()) { + return emplace_back_n( + [&](std::byte* storage) noexcept(noexcept(T{generator()})) { + new (storage) T{generator()}; + }, + count); + } + + /*! + * @brief Push a copy of value (producer) + * @return true if pushed, false if buffer is full + */ + bool push_back(const T& value) { + return emplace_back_n( + [&](std::byte* storage) noexcept(noexcept(T{value})) { new (storage) T{value}; }, 1); + } + /*! + * @brief Push by moving value (producer) + * @return true if pushed, false if buffer is full + */ + bool push_back(T&& value) { + return emplace_back_n( + [&](std::byte* storage) noexcept(noexcept(T{std::move(value)})) { + new (storage) T{std::move(value)}; + }, + 1); + } + + /*! + * @brief Batch-pop elements from the head (consumer) + * @tparam F Functor with signature `void(T)` receiving moved-out elements + * @param count Maximum number of elements to pop (defaults to all available) + * @return Number of elements actually popped + * @note Consumer-only. Consumes with release on `out_` and destroys elements. + */ + template + requires requires(F& f, T& t) { + { f(std::move(t)) } noexcept; + } size_t pop_front_n(F callback_functor, size_t count = std::numeric_limits::max()) { + const auto in = in_.load(std::memory_order::acquire); + const auto out = out_.load(std::memory_order::relaxed); + + const auto readable = in - out; + count = std::min(count, readable); + if (!count) + return 0; + + const auto offset = out & mask_; + const auto slice = std::min(count, max_size() - offset); + + auto process = [&callback_functor](std::byte* storage) { + auto& element = *std::launder(reinterpret_cast(storage)); + callback_functor(std::move(element)); + std::destroy_at(&element); + }; + for (size_t i = 0; i < slice; i++) + process(storage_[offset + i].data); + for (size_t i = 0; i < count - slice; i++) + process(storage_[i].data); + + out_.store(out + count, std::memory_order::release); + + return count; + } + + /*! + * @brief Pop one element (consumer) + * @return true if an element was popped, false if empty + */ + template + requires requires(F& f, T& t) { + { f(std::move(t)) } noexcept; + } bool pop_front(F&& callback_functor) { + return pop_front_n(std::forward(callback_functor), 1); + } + + /*! + * @brief Clear the buffer by consuming all elements + * @return Number of elements that were erased + */ + size_t clear() { + return pop_front_n([](const T&) noexcept {}); + } + +private: + /*! + * @brief Round up to next power of two + * @note Assumes n > 0. Handles 32/64-bit size_t. + */ + constexpr static size_t round_up_to_next_power_of_2(size_t n) { + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + if constexpr (sizeof(size_t) > 4) + n |= n >> 32; + n++; + return n; + } + + size_t mask_; + struct Storage { + alignas(T) std::byte data[sizeof(T)]; + }* storage_; + + std::atomic in_{0}, out_{0}; +}; + +} // namespace librmcs::host::utility