From 088d8ef75cc454bc8833492534cbc45fa95caaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ertu=C4=9Frul=20Top=C3=A7u?= Date: Mon, 3 Aug 2026 11:43:42 +0300 Subject: [PATCH] Initial GXFP5130 userspace prototype --- .gitignore | 13 + LICENSES/GPL-2.0 | 357 ++++++++++++++++++++ LICENSES/LGPL-2.1 | 509 ++++++++++++++++++++++++++++ Makefile | 43 +++ README.md | 99 ++++++ docs/protocol.md | 267 +++++++++++++++ examples/fdt-monitor.c | 99 ++++++ examples/sensor-probe.c | 161 +++++++++ examples/session-regression.c | 212 ++++++++++++ include/gxfp/config.h | 48 +++ include/gxfp/device.h | 32 ++ include/gxfp/fdt.h | 34 ++ include/gxfp/goodix_ec_uapi.h | 37 ++ include/gxfp/protocol.h | 29 ++ include/gxfp/request.h | 28 ++ include/gxfp/sensor.h | 92 +++++ include/gxfp/session.h | 58 ++++ include/gxfp/tls.h | 36 ++ libfprint-driver/README.md | 26 ++ libfprint-driver/gxfp5130.c | 404 ++++++++++++++++++++++ src/config.c | 262 ++++++++++++++ src/device.c | 166 +++++++++ src/fdt.c | 155 +++++++++ src/protocol.c | 100 ++++++ src/request.c | 149 ++++++++ src/sensor.c | 618 ++++++++++++++++++++++++++++++++++ src/session.c | 481 ++++++++++++++++++++++++++ src/tls.c | 356 ++++++++++++++++++++ tools/analyze_capture.c | 225 +++++++++++++ tools/capture_to_pgm.c | 222 ++++++++++++ 30 files changed, 5318 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSES/GPL-2.0 create mode 100644 LICENSES/LGPL-2.1 create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/protocol.md create mode 100644 examples/fdt-monitor.c create mode 100644 examples/sensor-probe.c create mode 100644 examples/session-regression.c create mode 100644 include/gxfp/config.h create mode 100644 include/gxfp/device.h create mode 100644 include/gxfp/fdt.h create mode 100644 include/gxfp/goodix_ec_uapi.h create mode 100644 include/gxfp/protocol.h create mode 100644 include/gxfp/request.h create mode 100644 include/gxfp/sensor.h create mode 100644 include/gxfp/session.h create mode 100644 include/gxfp/tls.h create mode 100644 libfprint-driver/README.md create mode 100644 libfprint-driver/gxfp5130.c create mode 100644 src/config.c create mode 100644 src/device.c create mode 100644 src/fdt.c create mode 100644 src/protocol.c create mode 100644 src/request.c create mode 100644 src/sensor.c create mode 100644 src/session.c create mode 100644 src/tls.c create mode 100644 tools/analyze_capture.c create mode 100644 tools/capture_to_pgm.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..706ec6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.a +*.o +*.bin +*.pgm +*.variant +*.bak +*.bak[0-9]* + +/analyze-capture +/capture-to-pgm +/fdt-monitor +/sensor-probe +/session-regression diff --git a/LICENSES/GPL-2.0 b/LICENSES/GPL-2.0 new file mode 100644 index 0000000..ea8e93d --- /dev/null +++ b/LICENSES/GPL-2.0 @@ -0,0 +1,357 @@ +Valid-License-Identifier: GPL-2.0 +Valid-License-Identifier: GPL-2.0-only +Valid-License-Identifier: GPL-2.0+ +Valid-License-Identifier: GPL-2.0-or-later +SPDX-URL: https://spdx.org/licenses/GPL-2.0.html +Usage-Guide: + To use this license in source code, put one of the following SPDX + tag/value pairs into a comment according to the placement + guidelines in the licensing rules documentation. + For 'GNU General Public License (GPL) version 2 only' use: + SPDX-License-Identifier: GPL-2.0 + or + SPDX-License-Identifier: GPL-2.0-only + For 'GNU General Public License (GPL) version 2 or any later version' use: + SPDX-License-Identifier: GPL-2.0+ + or + SPDX-License-Identifier: GPL-2.0-or-later +License-Text: + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 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 licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey 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 2 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 is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This 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 Library General +Public License instead of this License. diff --git a/LICENSES/LGPL-2.1 b/LICENSES/LGPL-2.1 new file mode 100644 index 0000000..4d1d06a --- /dev/null +++ b/LICENSES/LGPL-2.1 @@ -0,0 +1,509 @@ +Valid-License-Identifier: LGPL-2.1 +Valid-License-Identifier: LGPL-2.1-only +Valid-License-Identifier: LGPL-2.1+ +Valid-License-Identifier: LGPL-2.1-or-later +SPDX-URL: https://spdx.org/licenses/LGPL-2.1.html +Usage-Guide: + To use this license in source code, put one of the following SPDX + tag/value pairs into a comment according to the placement + guidelines in the licensing rules documentation. + For 'GNU Lesser General Public License (LGPL) version 2.1 only' use: + SPDX-License-Identifier: LGPL-2.1 + or: + SPDX-License-Identifier: LGPL-2.1-only + For 'GNU Lesser General Public License (LGPL) version 2.1 or any later + version' use: + SPDX-License-Identifier: LGPL-2.1+ + or: + SPDX-License-Identifier: LGPL-2.1-or-later +License-Text: + +GNU LESSER GENERAL PUBLIC LICENSE +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as +the successor of the GNU Library Public License, version 2, hence the +version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public Licenses are +intended to guarantee your freedom to share and change free software--to +make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software +Foundation and other authors who decide to use it. You can use it too, but +we suggest you first think carefully about whether this license or the +ordinary General Public License is the better strategy to use in any +particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, 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 this +service if you wish); that you receive source code or can get it if you +want it; that you can change the software and use pieces of it in new free +programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for you if +you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You +must make sure that they, too, receive or can get the source code. If you +link other code with the library, you must provide complete object files to +the recipients, so that they can relink them with the library after making +changes to the library and recompiling it. And you must show them these +terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any +free program. We wish to make sure that a company cannot effectively +restrict the users of a free program by obtaining a restrictive license +from a patent holder. Therefore, we insist that any patent license obtained +for a version of the library must be consistent with the full freedom of +use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public +License, applies to certain designated libraries, and is quite different +from the ordinary General Public License. We use this license for certain +libraries in order to permit linking those libraries into non-free +programs. + +When a program is linked with a library, whether statically or using a +shared library, the combination of the two is legally speaking a combined +work, a derivative of the original library. The ordinary General Public +License therefore permits such linking only if the entire combination fits +its criteria of freedom. The Lesser General Public License permits more lax +criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does +Less to protect the user's freedom than the ordinary General Public +License. It also provides other free software developers Less of an +advantage over competing non-free programs. These disadvantages are the +reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + +For example, on rare occasions, there may be a special need to encourage +the widest possible use of a certain library, so that it becomes a de-facto +standard. To achieve this, non-free programs must be allowed to use the +library. A more frequent case is that a free library does the same job as +widely used non-free libraries. In this case, there is little to gain by +limiting the free library to free software only, so we use the Lesser +General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free +software. For example, permission to use the GNU C Library in non-free +programs enables many more people to use the whole GNU operating system, as +well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a +modified version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code +derived from the library, whereas the latter must be combined with the +library in order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program + which contains a notice placed by the copyright holder or other + authorized party saying it may be distributed under the terms of this + Lesser General Public License (also called "this License"). Each + licensee is addressed as "you". + + A "library" means a collection of software functions and/or data + prepared so as to be conveniently linked with application programs + (which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work which + has been distributed under these terms. A "work based on the Library" + means either the Library or any derivative work under copyright law: + that is to say, a work containing the Library or a portion of it, either + verbatim or with modifications and/or translated straightforwardly into + another language. (Hereinafter, translation is included without + limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for making + modifications to it. For a library, complete source code means all the + source code for all modules it contains, plus any associated interface + definition files, plus the scripts used to control compilation and + installation of the library. + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + a program using the Library is not restricted, and output from such a + program is covered only if its contents constitute a work based on the + Library (independent of the use of the Library in a tool for writing + it). Whether that is true depends on what the Library does and what the + program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete + source code as you receive it, in any medium, provided that you + conspicuously and appropriately publish on each copy an appropriate + copyright notice and disclaimer of warranty; keep intact all the notices + that refer to this License and to the absence of any warranty; and + distribute a copy of this License along with the Library. + + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, + thus forming a work based on the Library, and copy and distribute such + modifications or work under the terms of Section 1 above, provided that + you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating + that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to + all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table + of data to be supplied by an application program that uses the + facility, other than as an argument passed when the facility is + invoked, then you must make a good faith effort to ensure that, in + the event an application does not supply such function or table, the + facility still operates, and performs whatever part of its purpose + remains meaningful. + + (For example, a function in a library to compute square roots has a + purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must be + optional: if the application does not supply it, the square root + function must still compute square roots.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Library, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Library, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Library. + + In addition, mere aggregation of another work not based on the Library + with the Library (or with a work based on the Library) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public + License instead of this License to a given copy of the Library. To do + this, you must alter all the notices that refer to this License, so that + they refer to the ordinary GNU General Public License, version 2, + instead of to this License. (If a newer version than version 2 of the + ordinary GNU General Public License has appeared, then you can specify + that version instead if you wish.) Do not make any other change in these + notices. + + Once this change is made in a given copy, it is irreversible for that + copy, so the ordinary GNU General Public License applies to all + subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of the + Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of + it, under Section 2) in object code or executable form under the terms + of Sections 1 and 2 above provided that you accompany it with the + complete corresponding machine-readable source code, which must be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange. + + If distribution of object code is made by offering access to copy from a + designated place, then offering equivalent access to copy the source + code from the same place satisfies the requirement to distribute the + source code, even though third parties are not compelled to copy the + source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but + is designed to work with the Library by being compiled or linked with + it, is called a "work that uses the Library". Such a work, in isolation, + is not a derivative work of the Library, and therefore falls outside the + scope of this License. + + However, linking a "work that uses the Library" with the Library creates + an executable that is a derivative of the Library (because it contains + portions of the Library), rather than a "work that uses the + library". The executable is therefore covered by this License. Section 6 + states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file + that is part of the Library, the object code for the work may be a + derivative work of the Library even though the source code is + not. Whether this is true is especially significant if the work can be + linked without the Library, or if the work is itself a library. The + threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data structure + layouts and accessors, and small macros and small inline functions (ten + lines or less in length), then the use of the object file is + unrestricted, regardless of whether it is legally a derivative + work. (Executables containing this object code plus portions of the + Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may + distribute the object code for the work under the terms of Section + 6. Any executables containing that work also fall under Section 6, + whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a + "work that uses the Library" with the Library to produce a work + containing portions of the Library, and distribute that work under terms + of your choice, provided that the terms permit modification of the work + for the customer's own use and reverse engineering for debugging such + modifications. + + You must give prominent notice with each copy of the work that the + Library is used in it and that the Library and its use are covered by + this License. You must supply a copy of this License. If the work during + execution displays copyright notices, you must include the copyright + notice for the Library among them, as well as a reference directing the + user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable + source code for the Library including whatever changes were used in + the work (which must be distributed under Sections 1 and 2 above); + and, if the work is an executable linked with the Library, with the + complete machine-readable "work that uses the Library", as object + code and/or source code, so that the user can modify the Library and + then relink to produce a modified executable containing the modified + Library. (It is understood that the user who changes the contents of + definitions files in the Library will not necessarily be able to + recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a copy + of the library already present on the user's computer system, rather + than copying library functions into the executable, and (2) will + operate properly with a modified version of the library, if the user + installs one, as long as the modified version is interface-compatible + with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three + years, to give the same user the materials specified in Subsection + 6a, above, for a charge no more than the cost of performing this + distribution. + + d) If distribution of the work is made by offering access to copy from a + designated place, offer equivalent access to copy the above specified + materials from the same place. + + e) Verify that the user has already received a copy of these materials + or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the Library" + must include any data and utility programs needed for reproducing the + executable from it. However, as a special exception, the materials to be + distributed need not include anything that is normally distributed (in + either source or binary form) with the major components (compiler, + kernel, and so on) of the operating system on which the executable runs, + unless that component itself accompanies the executable. + + It may happen that this requirement contradicts the license restrictions + of other proprietary libraries that do not normally accompany the + operating system. Such a contradiction means you cannot use both them + and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library + side-by-side in a single library together with other library facilities + not covered by this License, and distribute such a combined library, + provided that the separate distribution of the work based on the Library + and of the other library facilities is otherwise permitted, and provided + that you do these two things: + + a) Accompany the combined library with a copy of the same work based on + the Library, uncombined with any other library facilities. This must + be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part + of it is a work based on the Library, and explaining where to find + the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the + Library except as expressly provided under this License. Any attempt + otherwise to copy, modify, sublicense, link with, or distribute the + Library is void, and will automatically terminate your rights under this + License. However, parties who have received copies, or rights, from you + under this License will not have their licenses terminated so long as + such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed + it. However, nothing else grants you permission to modify or distribute + the Library or its derivative works. These actions are prohibited by law + if you do not accept this License. Therefore, by modifying or + distributing the Library (or any work based on the Library), you + indicate your acceptance of this License to do so, and all its terms and + conditions for copying, distributing or modifying the Library or works + based on it. + +10. Each time you redistribute the Library (or any work based on the + Library), the recipient automatically receives a license from the + original licensor to copy, distribute, link with or modify the Library + subject to these terms and conditions. You may not impose any further + restrictions on the recipients' exercise of the rights granted + herein. You are not responsible for enforcing compliance by third + parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + 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 + distribute so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you + may not distribute the Library at all. For example, if a patent license + would not permit royalty-free redistribution of the Library by all + those who receive copies directly or indirectly through you, then the + only way you could satisfy both it and this License would be to refrain + entirely from distribution of the Library. + + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply, and the section as a whole is intended to apply in other + circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system which is implemented + by public license practices. Many people have made generous + contributions to the wide range of software distributed through that + system in reliance on consistent application of that system; it is up + to the author/donor to decide if he or she is willing to distribute + software through any other system and a licensee cannot impose that + choice. + + This section is intended to make thoroughly clear what is believed to + be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain + countries either by patents or by copyrighted interfaces, the original + copyright holder who places the Library under this License may add an + explicit geographical distribution limitation excluding those + countries, so that distribution is permitted only in or among countries + not thus excluded. In such case, this License incorporates the + limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of + the Lesser 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 Library + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and + conditions either of that version or of any later version published by + the Free Software Foundation. If the Library does not specify a license + version number, you may choose any version ever published by the Free + Software Foundation. + +14. If you wish to incorporate parts of the Library into other free + programs whose distribution conditions are incompatible with these, + write to the author to ask for permission. For software which is + copyrighted by the Free Software Foundation, write to the Free Software + Foundation; we sometimes make exceptions for this. Our decision will be + guided by the two goals of preserving the free status of all + derivatives of our free software and of promoting the sharing and reuse + of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE LIBRARY "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 LIBRARY IS WITH + YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE LIBRARY 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 LIBRARY + (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or (at +your option) any later version. + +This library 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 Lesser General Public License +for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this library; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..19f898b --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +CC ?= gcc +AR ?= ar + +MBEDTLS_CFLAGS := $(shell pkg-config --cflags mbedtls 2>/dev/null) +MBEDTLS_LIBS := $(shell pkg-config --libs mbedtls 2>/dev/null) -lmbedx509 -lmbedcrypto + +CPPFLAGS += -Iinclude $(MBEDTLS_CFLAGS) +CFLAGS ?= -O2 -Wall -Wextra -Wpedantic -std=c11 -D_POSIX_C_SOURCE=200809L + +LIB_OBJECTS = src/device.o src/protocol.o src/request.o src/fdt.o src/sensor.o src/config.o src/tls.o src/session.o +LIBRARY = libgxfp.a + +all: $(LIBRARY) fdt-monitor sensor-probe session-regression analyze-capture capture-to-pgm + +$(LIBRARY): $(LIB_OBJECTS) + $(AR) rcs $@ $^ + +fdt-monitor: examples/fdt-monitor.o $(LIBRARY) + $(CC) $(CFLAGS) -o $@ examples/fdt-monitor.o $(LIBRARY) $(MBEDTLS_LIBS) + +sensor-probe: examples/sensor-probe.o $(LIBRARY) + $(CC) $(CFLAGS) -o $@ examples/sensor-probe.o $(LIBRARY) $(MBEDTLS_LIBS) + +session-regression: examples/session-regression.o $(LIBRARY) + $(CC) $(CFLAGS) -o $@ examples/session-regression.o $(LIBRARY) $(MBEDTLS_LIBS) -pthread + +analyze-capture: tools/analyze_capture.o + $(CC) $(CFLAGS) -o $@ $^ + +capture-to-pgm: tools/capture_to_pgm.o + $(CC) $(CFLAGS) -o $@ $^ + +src/%.o: src/%.c + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +examples/%.o: examples/%.c + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +tools/%.o: tools/%.c + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(LIB_OBJECTS) examples/*.o tools/*.o $(LIBRARY) fdt-monitor sensor-probe session-regression analyze-capture capture-to-pgm diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd94d59 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Goodix GXFP5130 userspace prototype + +Experimental Linux userspace prototype for the Goodix GXFP5130 fingerprint +sensor transported through the `/dev/gxfp` EC mailbox ABI. + +The low-level initialization, TLS session, finger detection and image capture +paths work on the tested hardware. The project is **not yet fully integrated +into libfprint**, and enrollment/verification is not production-ready. + +The implementation has been tested on one GXFP5130 device with chip ID +`0x2504` and the ChicagoHU profile. + +## Hardware-validated functionality + +- mailbox record I/O through `/dev/gxfp` +- Goodix request and response framing +- chip-ID and 64-byte OTP reads +- OTP CRC validation and DAC calibration extraction +- OTP-derived 224-byte ChicagoHU configuration generation +- TLS 1.2 PSK session establishment with the sensor MCU +- finger-down and finger-up detection +- repeated image capture in one TLS session +- 7680-byte packed 12-bit image decoding into 5120 samples +- CRC-32/MPEG-2 image validation +- 64x80 12-bit image output and deterministic 8-bit conversion +- timeout, cancellation and session recovery regression paths + +An experimental libfprint image-device skeleton is included under +`libfprint-driver/`. It can capture images through libfprint, but the final +enroll/verify pipeline and matching policy remain work in progress. + +## Requirements + +- Linux with the Goodix EC mailbox transport and `/dev/gxfp` +- a C11 compiler and GNU Make +- pkg-config +- mbedTLS development libraries + +The corresponding kernel transport patch is archived at: + + + +## Build and test + +```sh +make +sudo ./sensor-probe /dev/gxfp 10 +``` + +Enable protocol/TLS debug output only when needed: + +```sh +sudo env GXFP_DEBUG=1 ./sensor-probe /dev/gxfp 1 +``` + +Run cancellation, timeout-recovery and normal-cycle regression tests: + +```sh +sudo ./session-regression /dev/gxfp +``` + +## Layout + +```text +include/gxfp/ protocol and session headers +src/ transport, configuration, TLS, capture and session code +examples/ hardware probes and session regression program +tools/ offline capture-analysis utilities +libfprint-driver/ experimental, incomplete libfprint integration +docs/ verified protocol documentation +``` + +See [docs/protocol.md](docs/protocol.md) for the protocol and capture format. + +## Security and compatibility + +The TLS material in `src/tls.c` was recovered for interoperability with the +tested hardware and forms part of this experimental protocol implementation. +It is not a user password or repository credential. Do not assume that the +protocol, configuration, TLS material or image geometry applies to another +Goodix model. + +The prototype exposes raw biometric images. Captures, PGM files and enrollment +templates are ignored by Git and should not be shared without the subject's +consent. + +## Reverse-engineering notes + +Protocol and configuration behavior was reconstructed primarily from the +Windows implementation and verified against live hardware traces. Existing +community GXFP5130 experiments were used for comparison. The +`capture-to-pgm` utility is an offline diagnostic tool and is not used by the +capture/session implementation. + +## License + +The userspace library, examples and tools are GPL-2.0-only. The experimental +libfprint integration file carries an LGPL-2.1-or-later SPDX identifier. Full +license texts are available under `LICENSES/`. diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000..0946a17 --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,267 @@ +# GXFP5130 protocol notes + +This document describes the protocol implemented by this repository for the +tested Goodix GXFP5130, chip ID `0x2504`, using the ChicagoHU configuration +profile. It replaces the earlier point-in-time PDF report with documentation +that can evolve together with the source. + +The labels below have precise meanings: + +- **Hardware verified:** repeatedly observed on the physical sensor. +- **Windows-derived:** recovered from the Windows implementation and then used + to guide the implementation. +- **Unknown:** retained as opaque data without assigning semantics. + +## 1. Layering + +```text +application / libfprint prototype + | + v +GXFP session, configuration, TLS and image parser + | + v +/dev/gxfp record-oriented userspace ABI + | + v +kernel EC mailbox transport + | + v +Goodix GXFP5130 +``` + +The kernel driver transports opaque mailbox records, manages MMIO/GPIO/IRQ and +owns the userspace ABI. Sensor commands, configuration policy, TLS and image +processing remain in userspace. + +## 2. `/dev/gxfp` records + +The UAPI structures are mirrored in `include/gxfp/goodix_ec_uapi.h`. + +Userspace writes a `goodix_ec_tx_header` followed by exactly `payload_len` +bytes. A read returns one complete `goodix_ec_record_header` followed by its +payload; records are never split across reads. + +Observed normalized MP types: + +| MP type | Purpose | Status | +| --- | --- | --- | +| `0x0a` | Normal Goodix commands, replies, ACKs and FDT | Hardware verified | +| `0x0b` | TLS records exchanged with the sensor MCU | Hardware verified | +| `0xb0` | Host-to-sensor TLS transmit flag | Hardware verified | + +The kernel may receive a raw normal MP value represented as `0xa0` at the +mailbox level. The userspace ABI exposes its normalized type as `0x0a`. + +## 3. Normal Goodix frame + +Normal command traffic uses this byte layout: + +```text +offset size field +0 1 command +1 2 declared length, little-endian +3 n payload +3+n 1 Goodix checksum +``` + +The declared length covers the payload and checksum. The checksum is selected +so the eight-bit sum of the command, both length bytes, payload and checksum is +zero. + +Responses may use the direction bit in the command byte. The request layer +matches the normalized command and skips unrelated asynchronous records while +continuing to wait for the expected response. + +## 4. Identification and OTP + +Register `0x0000` returns the chip identification. The tested response is: + +```text +command: 0x82 +payload: a2 04 25 00 +chip ID: 0x2504 +``` + +The sensor exposes a 64-byte OTP block. The implementation validates the CP, +FT and MT sections independently using the recovered eight-bit CRC algorithm. +The tested sensor produces valid values for all three sections. + +The OTP is also used to select and patch the DAC calibration values rather +than applying a single machine-wide constant. A captured example yielded: + +```text +raw DAC values: 0x00ba 0x00bc 0x00ba 0x00ba +0x0220: 0x0ba8 +0x0236: 0x00bc +0x0238: 0x00ba +0x023a: 0x00ba +``` + +These exact values are device calibration data, not universal constants. + +## 5. ChicagoHU configuration + +For chip ID `0x2504`, userspace constructs a 224-byte ChicagoHU configuration, +patches its OTP-derived fields and recomputes the 16-bit configuration +checksum. The tested device used: + +```text +t-code: 0x0100 +FDT delta: 0x1d +FDT offset: 0x00 +size: 224 bytes +``` + +The exact checksum depends on the patched device calibration values. The +configuration is downloaded only after reset and is acknowledged by the +sensor. + +## 6. TLS transport + +The host userspace implementation acts as a TLS server and the sensor MCU acts +as a TLS client. + +| Parameter | Value | +| --- | --- | +| Protocol | TLS 1.2 | +| Verified cipher suite | `TLS-PSK-WITH-AES-256-GCM-SHA384` (`0x00a9`) | +| PSK identity | `Client_identity` | +| MCU-to-host MP type | `0x0b` | +| Host-to-MCU MP flag | `0xb0` | + +A hardware-verified handshake contains ClientHello, ClientKeyExchange, +ChangeCipherSpec and Finished. Capture data subsequently arrives as TLS +application data and is decrypted through `mbedtls_ssl_read()`. + +Normal `0x0a` ACKs can be interleaved with TLS traffic. The TLS receive path +must ignore non-TLS MP records, while the normal request path must not consume +TLS records intended for mbedTLS. + +## 7. Finger detection + +The verified FDT commands are implemented in `src/fdt.c`: + +| Command | Function | +| --- | --- | +| `0x36` | Set/arm FDT mode | +| `0x32` | Finger-down path | +| `0x34` | Finger-up path | +| `0xda` | FDT status/event report | + +A typical finger-down sequence reports: + +```text +cmd=0x36 status=0x0100 -> ready +cmd=0x32 status=0x0002 -> finger down +``` + +Capture must begin while the finger remains present. After a successful image, +userspace arms finger-up, waits for removal and returns the session to ACTIVE. + +## 8. Image capture + +The hardware-verified capture frame is sent through the normal MP path: + +```text +20 03 00 01 00 86 +``` + +Decoded: + +| Bytes | Meaning | +| --- | --- | +| `20` | Image-capture command | +| `03 00` | Payload plus checksum length | +| `01 00` | Capture payload | +| `86` | Goodix checksum | + +The command ACK arrives as a normal `0x0a` record. The image does not arrive +there; it arrives encrypted as TLS application data over MP `0x0b`. + +After TLS stream reassembly, one capture has this verified layout: + +```text +offset size field +0 1 command (0x20) +1 2 declared length (7690, little-endian) +3 5 opaque image header +8 7680 packed 12-bit pixels +7688 4 CRC-32/MPEG-2, Goodix byte order +7692 1 opaque trailer/status +total 7693 bytes +``` + +The Windows-derived parser relationship is consistent with this layout: + +```c +image_data = frame_payload + 5; +image_data_length = frame_payload_length - 6; +``` + +The CRC covers the 7680 packed-pixel bytes. It does not cover the five-byte +opaque header or the final trailer byte. Stored and calculated CRC values have +matched across repeated live captures. + +## 9. Packed 12-bit pixels + +Six packed bytes decode to four 12-bit samples. A complete image contains: + +```text +7680 packed bytes +5120 samples +64 x 80 pixels +``` + +The session API exposes the decoded samples as `uint16_t`. Conversion to an +8-bit `FpImage` is intentionally separate from protocol parsing so that image +orientation, normalization and matching policy can evolve without changing +the validated capture layer. + +## 10. Session state sequence + +```text +OPEN + -> activate (identification, OTP, config, TLS) +ACTIVE + -> arm finger down +WAITING_FINGER_DOWN + -> finger detected +FINGER_PRESENT + -> capture +CAPTURING + -> CRC-valid image +FINGER_PRESENT + -> arm/wait finger up +WAITING_FINGER_UP + -> finger removed +ACTIVE +``` + +Timeout and cancellation paths deactivate and reinitialize the hardware rather +than leaving FDT armed across the next process invocation. The regression tool +tests cancellation during finger-down wait, finger-up timeout recovery and a +normal capture after both recoveries. + +## 11. Known unknowns + +The following values are deliberately kept opaque: + +- semantics of the five-byte image header; it is consistently all zero on the + tested profile +- meaning of trailer value `0x88` +- whether command `0x21` selects a separate image or calibration mode +- orientation/inversion policy appropriate for final libfprint matching +- fixed-pattern-noise and dark-frame correction policy +- applicability of this profile and TLS material to another chip ID + +No resynchronization heuristic, guessed checksum or alternate capture command +is used in the validated session path. + +## 12. Current project boundary + +The transport, sensor setup, TLS, FDT, capture, CRC and image decoding layers +are hardware-validated prototypes. The libfprint driver remains experimental: +final enroll/verify behavior, multi-capture policy, image enhancement and +matcher quality still require validation before production use. + diff --git a/examples/fdt-monitor.c b/examples/fdt-monitor.c new file mode 100644 index 0000000..fa1fd96 --- /dev/null +++ b/examples/fdt-monitor.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/device.h" +#include "gxfp/fdt.h" +#include "gxfp/protocol.h" + +#include +#include +#include +#include +#include + +static const char *event_name(enum gxfp_fdt_event event) +{ + switch (event) { + case GXFP_FDT_EVENT_READY: + return "ready"; + case GXFP_FDT_EVENT_FINGER_DOWN: + return "finger-down"; + case GXFP_FDT_EVENT_FINGER_UP: + return "finger-up"; + case GXFP_FDT_EVENT_REVERSE: + return "reverse"; + default: + return "none"; + } +} + +int main(int argc, char **argv) +{ + const char *path = argc > 1 ? argv[1] : "/dev/gxfp"; + struct gxfp_device device = { .fd = -1 }; + struct gxfp_fdt fdt; + int ret; + + ret = gxfp_device_open(&device, path); + if (ret) { + fprintf(stderr, "open failed: %s\n", strerror(-ret)); + return EXIT_FAILURE; + } + + ret = gxfp_device_flush_rx(&device); + if (ret) + fprintf(stderr, "warning: flush failed: %s\n", strerror(-ret)); + + gxfp_fdt_init(&fdt); + + ret = gxfp_fdt_arm_down(&device); + if (ret) { + fprintf(stderr, "FDT arm failed: %s\n", strerror(-ret)); + gxfp_device_close(&device); + return EXIT_FAILURE; + } + + printf("FDT monitor active. Touch and release the sensor.\n"); + + for (;;) { + struct gxfp_rx_record record; + struct gxfp_frame_view frame; + enum gxfp_fdt_event event; + uint16_t status = 0; + + ret = gxfp_device_receive(&device, &record, -1); + if (ret == -EINTR) + break; + if (ret) { + fprintf(stderr, "receive failed: %s\n", strerror(-ret)); + break; + } + + ret = gxfp_frame_parse(record.payload, record.payload_len, &frame); + if (ret) { + fprintf(stderr, "bad frame: %s\n", strerror(-ret)); + gxfp_rx_record_release(&record); + continue; + } + + if (frame.command == GXFP_GOODIX_ACK) { + gxfp_rx_record_release(&record); + continue; + } + + event = gxfp_fdt_decode(&frame, &status); + + printf("cmd=0x%02x status=0x%04x event=%s\n", + frame.command, status, event_name(event)); + + ret = gxfp_fdt_handle_event(&fdt, &device, event); + gxfp_rx_record_release(&record); + + if (ret) { + fprintf(stderr, "FDT transition failed: %s\n", + strerror(-ret)); + break; + } + } + + gxfp_device_close(&device); + return EXIT_SUCCESS; +} diff --git a/examples/sensor-probe.c b/examples/sensor-probe.c new file mode 100644 index 0000000..21b1956 --- /dev/null +++ b/examples/sensor-probe.c @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/session.h" + +#include +#include +#include +#include +#include + +static const char *error_string(int error) +{ + if (error >= 0) + return "non-negative API error"; + + return strerror(-error); +} + +static int print_image_stats(unsigned int index, + const struct gxfp_image12 *image) +{ + size_t count; + uint16_t minimum = UINT16_MAX; + uint16_t maximum = 0; + uint64_t sum = 0; + size_t i; + + if (!image || !image->pixels || !image->width || !image->height) { + fprintf(stderr, "invalid image returned by capture\n"); + return -EINVAL; + } + if (image->width > SIZE_MAX / image->height) { + fprintf(stderr, "image dimensions overflow\n"); + return -EOVERFLOW; + } + count = image->width * image->height; + + for (i = 0; i < count; i++) { + uint16_t value = image->pixels[i]; + if (value < minimum) + minimum = value; + if (value > maximum) + maximum = value; + sum += value; + } + printf("capture_index: %u\n", index); + printf("image: %zux%zu, samples=%zu\n", + image->width, image->height, count); + printf("CRC: stored=0x%08x calculated=0x%08x valid=yes\n", + image->crc_stored, image->crc_calculated); + printf("header: %02x %02x %02x %02x %02x\n", + image->header[0], image->header[1], image->header[2], + image->header[3], image->header[4]); + printf("frame_trailer: 0x%02x\n", image->trailer); + printf("pixel_min=%u pixel_max=%u pixel_mean=%.2f\n", + minimum, maximum, (double)sum / count); + return 0; +} + +int main(int argc, char **argv) +{ + const char *path = argc > 1 ? argv[1] : "/dev/gxfp"; + unsigned int capture_count = 1u; + struct gxfp_session *session = NULL; + const char *stage = "open"; + unsigned int completed = 0; + unsigned int index; + int active = 0; + int ret; + + if (argc > 3) { + fprintf(stderr, "usage: %s [device] [capture-count]\n", argv[0]); + return EXIT_FAILURE; + } + + if (argc > 2) { + char *end = NULL; + unsigned long value; + + errno = 0; + value = strtoul(argv[2], &end, 10); + if (errno == ERANGE || end == argv[2] || *end != '\0' || + value == 0 || value > 100) { + fprintf(stderr, "capture count must be in range 1..100\n"); + return EXIT_FAILURE; + } + capture_count = (unsigned int)value; + } + + ret = gxfp_session_open(&session, path); + if (ret) { + fprintf(stderr, "session open failed: %s (%d)\n", + error_string(ret), ret); + return EXIT_FAILURE; + } + stage = "activate"; + ret = gxfp_session_activate(session, 10000); + if (ret) { + fprintf(stderr, "session activate failed: %s (%d)\n", + error_string(ret), ret); + goto out; + } + active = 1; + printf("Session active; TLS established.\n"); + + for (index = 1; index <= capture_count; index++) { + struct gxfp_image12 image = { 0 }; + + printf("\n=== Capture %u/%u ===\n", index, capture_count); + stage = "arm-finger-down"; + ret = gxfp_session_arm_finger_down(session); + if (ret) + goto capture_error; + printf("FDT down armed. Put finger on sensor...\n"); + stage = "wait-finger-down"; + ret = gxfp_session_wait_finger_down(session, 10000); + if (ret) + goto capture_error; + stage = "capture"; + ret = gxfp_session_capture(session, &image, 20000); + if (ret) + goto capture_error; + stage = "print-image-stats"; + ret = print_image_stats(index, &image); + if (ret) + goto capture_error; + gxfp_image12_clear(&image); + + stage = "arm-finger-up"; + ret = gxfp_session_arm_finger_up(session); + if (ret) + goto capture_error; + printf("Remove your finger...\n"); + stage = "wait-finger-up"; + ret = gxfp_session_wait_finger_up(session, 10000); + if (ret) + goto capture_error; + completed++; + continue; + +capture_error: + gxfp_image12_clear(&image); + fprintf(stderr, + "capture %u failed during %s, state=%d: %s (%d)\n", + index, stage, + (int)gxfp_session_get_state(session), + error_string(ret), ret); + break; + } + +out: + printf("\ncompleted captures: %u/%u\n", completed, capture_count); + if (session) { + if (active) { + int deactivate_ret = gxfp_session_deactivate(session); + if (!ret && deactivate_ret) + ret = deactivate_ret; + } + gxfp_session_close(session); + } + return ret ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/examples/session-regression.c b/examples/session-regression.c new file mode 100644 index 0000000..3fea5fc --- /dev/null +++ b/examples/session-regression.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/session.h" + +#include +#include +#include +#include +#include +#include + +struct wait_context { + struct gxfp_session *session; + int result; +}; + +static const char *error_string(int error) +{ + return error < 0 ? strerror(-error) : "non-negative API error"; +} + +static void sleep_ms(long milliseconds) +{ + struct timespec delay = { + .tv_sec = milliseconds / 1000, + .tv_nsec = (milliseconds % 1000) * 1000000L, + }; + + while (nanosleep(&delay, &delay) < 0 && errno == EINTR) + ; +} + +static void *wait_down_thread(void *opaque) +{ + struct wait_context *context = opaque; + + context->result = gxfp_session_wait_finger_down(context->session, + 30000); + return NULL; +} + +static int activate(struct gxfp_session *session) +{ + int ret = gxfp_session_activate(session, 10000); + + if (ret) + fprintf(stderr, "activate failed: %s (%d), state=%d\n", + error_string(ret), ret, + (int)gxfp_session_get_state(session)); + return ret; +} + +static int recover(struct gxfp_session *session) +{ + int ret; + + ret = gxfp_session_deactivate(session); + if (ret) { + fprintf(stderr, "deactivate/reset failed: %s (%d)\n", + error_string(ret), ret); + return ret; + } + if (gxfp_session_get_state(session) != GXFP_SESSION_OPEN) { + fprintf(stderr, "deactivate did not reach OPEN\n"); + return -EPROTO; + } + return activate(session); +} + +static int test_cancel_wait_down(struct gxfp_session *session) +{ + struct wait_context context = { .session = session, .result = 0 }; + pthread_t thread; + int pthread_ret; + int ret; + + puts("\n[1/3] cross-thread cancel during wait-finger-down"); + ret = gxfp_session_arm_finger_down(session); + if (ret) + return ret; + pthread_ret = pthread_create(&thread, NULL, wait_down_thread, &context); + if (pthread_ret) + return -pthread_ret; + sleep_ms(500); + gxfp_session_cancel(session); + pthread_ret = pthread_join(thread, NULL); + if (pthread_ret) + return -pthread_ret; + if (context.result != -ECANCELED) { + fprintf(stderr, "expected -ECANCELED, got %d\n", + context.result); + return -EPROTO; + } + if (gxfp_session_get_state(session) != GXFP_SESSION_ERROR) { + fprintf(stderr, "cancel did not leave session in ERROR\n"); + return -EPROTO; + } + puts("PASS: waiter returned -ECANCELED; recovering hardware"); + return recover(session); +} + +static int capture_while_present(struct gxfp_session *session, + struct gxfp_image12 *image) +{ + int ret; + + ret = gxfp_session_arm_finger_down(session); + if (ret) + return ret; + puts("Put finger on sensor and keep it there..."); + ret = gxfp_session_wait_finger_down(session, 10000); + if (ret) + return ret; + ret = gxfp_session_capture(session, image, 20000); + if (!ret) + printf("capture CRC: stored=0x%08x calculated=0x%08x\n", + image->crc_stored, image->crc_calculated); + return ret; +} + +static int test_finger_up_timeout(struct gxfp_session *session) +{ + struct gxfp_image12 image = { 0 }; + int ret; + + puts("\n[2/3] wait-finger-up timeout and hardware recovery"); + ret = capture_while_present(session, &image); + if (ret) + goto out; + ret = gxfp_session_arm_finger_up(session); + if (ret) + goto out; + puts("KEEP finger pressed for 3 seconds (timeout is intentional)..."); + ret = gxfp_session_wait_finger_up(session, 3000); + if (ret != -ETIMEDOUT) { + fprintf(stderr, "expected -ETIMEDOUT, got %d\n", ret); + ret = -EPROTO; + goto out; + } + if (gxfp_session_get_state(session) != GXFP_SESSION_ERROR) { + fprintf(stderr, "timeout did not leave session in ERROR\n"); + ret = -EPROTO; + goto out; + } + puts("PASS: finger-up timed out; remove finger now"); + sleep_ms(1000); + ret = recover(session); +out: + gxfp_image12_clear(&image); + return ret; +} + +static int test_normal_cycle(struct gxfp_session *session) +{ + struct gxfp_image12 image = { 0 }; + int ret; + + puts("\n[3/3] normal capture after both recoveries"); + ret = capture_while_present(session, &image); + if (ret) + goto out; + ret = gxfp_session_arm_finger_up(session); + if (ret) + goto out; + puts("Remove finger..."); + ret = gxfp_session_wait_finger_up(session, 10000); + if (!ret && gxfp_session_get_state(session) != GXFP_SESSION_ACTIVE) + ret = -EPROTO; + if (!ret) + puts("PASS: normal cycle completed in ACTIVE state"); +out: + gxfp_image12_clear(&image); + return ret; +} + +int main(int argc, char **argv) +{ + const char *path = argc == 2 ? argv[1] : "/dev/gxfp"; + struct gxfp_session *session = NULL; + int ret; + + if (argc > 2) { + fprintf(stderr, "usage: %s [device]\n", argv[0]); + return EXIT_FAILURE; + } + ret = gxfp_session_open(&session, path); + if (ret) + goto out; + ret = activate(session); + if (ret) + goto out; + ret = test_cancel_wait_down(session); + if (ret) + goto out; + ret = test_finger_up_timeout(session); + if (ret) + goto out; + ret = test_normal_cycle(session); +out: + if (ret) + fprintf(stderr, "regression failed: %s (%d), state=%d\n", + error_string(ret), ret, + session ? (int)gxfp_session_get_state(session) : -1); + if (session) { + int cleanup_ret = gxfp_session_deactivate(session); + if (!ret && cleanup_ret) + ret = cleanup_ret; + gxfp_session_close(session); + } + if (!ret) + puts("\nALL SESSION REGRESSIONS PASSED"); + return ret ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/include/gxfp/config.h b/include/gxfp/config.h new file mode 100644 index 0000000..77ae543 --- /dev/null +++ b/include/gxfp/config.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_CONFIG_H +#define GXFP_CONFIG_H + +#include +#include + +#define GXFP_CONFIG_SIZE 0xE0u +#define GXFP_CONFIG_SECTION_COUNT 8u + +enum gxfp_config_write_mode { + GXFP_CONFIG_WRITE_WORD = 0, + GXFP_CONFIG_WRITE_LOW_BYTE = 1, + GXFP_CONFIG_WRITE_HIGH_BYTE = 2, +}; + +struct gxfp_dac_info; + +struct gxfp_chicagohu_config_info { + uint8_t tcode_diff; + uint16_t tcode; + uint8_t fdt_delta; + uint8_t fdt_offset; + uint16_t checksum; +}; + +uint16_t gxfp_config_checksum(const uint8_t config[GXFP_CONFIG_SIZE]); + +int gxfp_config_update_checksum(uint8_t config[GXFP_CONFIG_SIZE]); + +int gxfp_config_modify_register(uint8_t config[GXFP_CONFIG_SIZE], + uint16_t reg, + uint16_t value, + unsigned int section, + enum gxfp_config_write_mode mode, + uint16_t *old_value); + +int gxfp_config_patch_dac(uint8_t config[GXFP_CONFIG_SIZE], + const uint16_t register_values[4]); + +int gxfp_config_build_chicagohu_2504( + uint8_t config[GXFP_CONFIG_SIZE], + const void *otp, + size_t otp_len, + const struct gxfp_dac_info *dac, + struct gxfp_chicagohu_config_info *info); + +#endif diff --git a/include/gxfp/device.h b/include/gxfp/device.h new file mode 100644 index 0000000..ce807cc --- /dev/null +++ b/include/gxfp/device.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_DEVICE_H +#define GXFP_DEVICE_H + +#include +#include + +struct gxfp_device { + int fd; +}; + +struct gxfp_rx_record { + uint32_t mp_type; + uint64_t timestamp_ns; + uint8_t *payload; + size_t payload_len; +}; + +int gxfp_device_open(struct gxfp_device *device, const char *path); +void gxfp_device_close(struct gxfp_device *device); +int gxfp_device_flush_rx(struct gxfp_device *device); + +int gxfp_device_send_mp(struct gxfp_device *device, uint8_t mp_flags, + const void *payload, size_t payload_len); + +int gxfp_device_receive(struct gxfp_device *device, + struct gxfp_rx_record *record, + int timeout_ms); + +void gxfp_rx_record_release(struct gxfp_rx_record *record); + +#endif diff --git a/include/gxfp/fdt.h b/include/gxfp/fdt.h new file mode 100644 index 0000000..e855974 --- /dev/null +++ b/include/gxfp/fdt.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_FDT_H +#define GXFP_FDT_H + +#include +#include + +struct gxfp_device; +struct gxfp_frame_view; + +enum gxfp_fdt_event { + GXFP_FDT_EVENT_NONE = 0, + GXFP_FDT_EVENT_READY, + GXFP_FDT_EVENT_FINGER_DOWN, + GXFP_FDT_EVENT_FINGER_UP, + GXFP_FDT_EVENT_REVERSE, +}; + +struct gxfp_fdt { + bool waiting_for_up; +}; + +void gxfp_fdt_init(struct gxfp_fdt *fdt); +int gxfp_fdt_arm_down(struct gxfp_device *device); +int gxfp_fdt_arm_up(struct gxfp_device *device); + +enum gxfp_fdt_event +gxfp_fdt_decode(const struct gxfp_frame_view *frame, uint16_t *status); + +int gxfp_fdt_handle_event(struct gxfp_fdt *fdt, + struct gxfp_device *device, + enum gxfp_fdt_event event); + +#endif diff --git a/include/gxfp/goodix_ec_uapi.h b/include/gxfp/goodix_ec_uapi.h new file mode 100644 index 0000000..ff8d0d5 --- /dev/null +++ b/include/gxfp/goodix_ec_uapi.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +#ifndef _UAPI_GOODIX_EC_H_ +#define _UAPI_GOODIX_EC_H_ + +#include +#include + +#define GOODIX_EC_UAPI_MAGIC 'G' +#define GOODIX_EC_UAPI_TX_MAX 500u +#define GOODIX_EC_UAPI_RX_MAX (128u * 1024u) + +/* + * read(2) returns one record: + * struct goodix_ec_record_header + * followed by len bytes of MP payload (normally one Goodix frame). + */ +struct goodix_ec_record_header { + __u32 len; + __u32 mp_type; + __u64 timestamp_ns; +}; + +/* + * write(2) accepts: + * struct goodix_ec_tx_header + * followed by payload_len bytes used as the MP payload. + */ +struct goodix_ec_tx_header { + __u8 mp_flags; + __u8 reserved; + __u16 payload_len; + __u32 flags; +}; + +#define GOODIX_EC_IOCTL_FLUSH_RX _IO(GOODIX_EC_UAPI_MAGIC, 0x11) + +#endif /* _UAPI_GOODIX_EC_H_ */ diff --git a/include/gxfp/protocol.h b/include/gxfp/protocol.h new file mode 100644 index 0000000..3b79722 --- /dev/null +++ b/include/gxfp/protocol.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_PROTOCOL_H +#define GXFP_PROTOCOL_H + +#include +#include + +#define GXFP_MP_COMMAND 0xA0 +#define GXFP_MP_RX_COMMAND 0x0A +#define GXFP_GOODIX_CHECKSUM_TARGET 0xAA +#define GXFP_GOODIX_ACK 0xB0 +#define GXFP_CMD_IMAGE_CAPTURE 0x20 + +struct gxfp_frame_view { + uint8_t command; + const uint8_t *payload; + size_t payload_len; + uint8_t checksum; +}; + +int gxfp_frame_build(uint8_t command, + const void *payload, size_t payload_len, + uint8_t *output, size_t output_capacity, + size_t *output_len); + +int gxfp_frame_parse(const void *frame, size_t frame_len, + struct gxfp_frame_view *view); + +#endif diff --git a/include/gxfp/request.h b/include/gxfp/request.h new file mode 100644 index 0000000..7dbf91c --- /dev/null +++ b/include/gxfp/request.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_REQUEST_H +#define GXFP_REQUEST_H + +#include +#include + +struct gxfp_device; + +struct gxfp_response { + uint8_t command; + uint8_t *payload; + size_t payload_len; + uint64_t timestamp_ns; +}; + +int gxfp_request(struct gxfp_device *device, + uint8_t command, + const void *payload, + size_t payload_len, + uint8_t expected_command, + unsigned int max_frames, + int timeout_ms, + struct gxfp_response *response); + +void gxfp_response_release(struct gxfp_response *response); + +#endif diff --git a/include/gxfp/sensor.h b/include/gxfp/sensor.h new file mode 100644 index 0000000..26b8be2 --- /dev/null +++ b/include/gxfp/sensor.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_SENSOR_H +#define GXFP_SENSOR_H + +#include +#include + +struct gxfp_device; + +struct gxfp_otp_crc_info { + int cp_valid; + int ft_valid; + int mt_valid; + uint8_t cp_calculated; + uint8_t ft_calculated; + uint8_t mt_calculated; + uint8_t cp_stored; + uint8_t ft_stored; + uint8_t mt_stored; +}; + +struct gxfp_dac_info { + uint16_t raw[4]; + uint16_t register_value[4]; + unsigned int match_count; + int used_ft; + int used_mt; + int used_fallback; + struct gxfp_otp_crc_info crc; +}; + +struct gxfp_mcu_state { + uint8_t version; + uint8_t flags; + int pov_image_valid; + int tls_connected; + int tls_used; + int locked; + uint8_t reserved[18]; +}; + +int gxfp_sensor_read_version(struct gxfp_device *device, + char *version, size_t version_capacity); + +int gxfp_sensor_query_mcu_state(struct gxfp_device *device, + struct gxfp_mcu_state *state); + +int gxfp_sensor_recover(struct gxfp_device *device, int unstick_tls); + +int gxfp_sensor_reset(struct gxfp_device *device); + +int gxfp_sensor_start_tls(struct gxfp_device *device); + +int gxfp_sensor_download_config(struct gxfp_device *device, + const void *config, + size_t config_len); + +int gxfp_sensor_reset_and_download_config( + struct gxfp_device *device, + const void *config, + size_t config_len); + +int gxfp_sensor_capture_oneframe(struct gxfp_device *device, + void *output, + size_t output_capacity, + size_t *output_len); + +int gxfp_sensor_read_register(struct gxfp_device *device, + uint16_t address, + void *output, + uint16_t output_len); + +int gxfp_sensor_read_chip_id(struct gxfp_device *device, + uint16_t *chip_id); + +int gxfp_sensor_read_otp(struct gxfp_device *device, + void *output, + size_t output_capacity, + size_t *output_len); + +int gxfp_sensor_check_chicagohu_otp_crc( + const void *otp, + size_t otp_len, + struct gxfp_otp_crc_info *info); + +int gxfp_sensor_parse_dac(const void *otp, + size_t otp_len, + int force_ft, + int force_mt, + struct gxfp_dac_info *info); + +#endif diff --git a/include/gxfp/session.h b/include/gxfp/session.h new file mode 100644 index 0000000..4176765 --- /dev/null +++ b/include/gxfp/session.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_SESSION_H +#define GXFP_SESSION_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct gxfp_session; + +enum gxfp_session_state { + GXFP_SESSION_OPEN = 0, + GXFP_SESSION_ACTIVE, + GXFP_SESSION_WAITING_FINGER_DOWN, + GXFP_SESSION_FINGER_PRESENT, + GXFP_SESSION_CAPTURING, + GXFP_SESSION_WAITING_FINGER_UP, + GXFP_SESSION_ERROR, +}; + +struct gxfp_image12 { + uint16_t *pixels; + size_t width; + size_t height; + uint8_t header[5]; + uint8_t trailer; + uint32_t crc_stored; + uint32_t crc_calculated; +}; + +int gxfp_session_open(struct gxfp_session **out, const char *device_path); +int gxfp_session_activate(struct gxfp_session *session, int timeout_ms); +int gxfp_session_deactivate(struct gxfp_session *session); + +int gxfp_session_arm_finger_down(struct gxfp_session *session); +int gxfp_session_wait_finger_down(struct gxfp_session *session, int timeout_ms); +int gxfp_session_capture(struct gxfp_session *session, + struct gxfp_image12 *image, int timeout_ms); +int gxfp_session_arm_finger_up(struct gxfp_session *session); +int gxfp_session_wait_finger_up(struct gxfp_session *session, int timeout_ms); + +int gxfp_image12_to_u8(const struct gxfp_image12 *source, + uint8_t *destination, size_t destination_size); +void gxfp_image12_clear(struct gxfp_image12 *image); + +void gxfp_session_cancel(struct gxfp_session *session); +enum gxfp_session_state +gxfp_session_get_state(const struct gxfp_session *session); +void gxfp_session_close(struct gxfp_session *session); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/gxfp/tls.h b/include/gxfp/tls.h new file mode 100644 index 0000000..4ccbc0e --- /dev/null +++ b/include/gxfp/tls.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef GXFP_TLS_H +#define GXFP_TLS_H + +#include +#include + +#include +#include +#include + +#include "gxfp/device.h" + +struct gxfp_tls { + struct gxfp_device *device; + mbedtls_ssl_context ssl; + mbedtls_ssl_config config; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_context entropy; + unsigned char *rx_buffer; + size_t rx_length; + size_t rx_offset; + int initialized; +}; + +int gxfp_tls_init(struct gxfp_tls *tls, struct gxfp_device *device); +int gxfp_tls_handshake(struct gxfp_tls *tls); +int gxfp_tls_handshake_timeout(struct gxfp_tls *tls, int timeout_ms); +int gxfp_tls_write_all(struct gxfp_tls *tls, const void *data, size_t len); +int gxfp_tls_read(struct gxfp_tls *tls, void *data, size_t capacity, + size_t *data_len); +int gxfp_tls_read_timeout(struct gxfp_tls *tls, void *data, size_t capacity, + size_t *data_len, int timeout_ms); +void gxfp_tls_close(struct gxfp_tls *tls); + +#endif diff --git a/libfprint-driver/README.md b/libfprint-driver/README.md new file mode 100644 index 0000000..3a82dcc --- /dev/null +++ b/libfprint-driver/README.md @@ -0,0 +1,26 @@ +# libfprint GXFP5130 integration skeleton + +This driver connects libfprint's `FpImageDevice` state machine to the +hardware-tested `gxfp_session` API. Blocking sensor operations run in a GLib +worker thread; all libfprint callbacks run in the main context. + +Current discovery is intentionally explicit because the sensor is an ACPI +platform/character device, while upstream libfprint has no generic ACPI char +device discovery type. Build the driver as an optional virtual-type driver and +start libfprint/fprintd with: + +```sh +FP_GXFP5130=/dev/gxfp +``` + +The integration must add `gxfp5130` to `drivers_info` and +`libfprint_drivers_sources` in upstream Meson, include this file, add the GXFP +headers, and link the PIC-built `libgxfp.a` plus mbedTLS libraries. + +The first image path uses the verified 64x80 sample order and the deterministic +`gxfp_image12_to_u8()` conversion. No transpose, flip, inversion, percentile +clipping, or unverified `0x21` command is applied. + +Before upstreaming, replace the environment-backed discovery with a dedicated +udev/ACPI character-device discovery mechanism and install permissions for +`/dev/gxfp`. diff --git a/libfprint-driver/gxfp5130.c b/libfprint-driver/gxfp5130.c new file mode 100644 index 0000000..6de15d5 --- /dev/null +++ b/libfprint-driver/gxfp5130.c @@ -0,0 +1,404 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#define FP_COMPONENT "gxfp5130" + +#include "drivers_api.h" +#include "gxfp/session.h" + +#include +#include +#include +#include +#include + +#define GXFP_DEVICE_PATH_DEFAULT "/dev/gxfp" +#define GXFP_ACTIVATE_TIMEOUT_MS 10000 +#define GXFP_FINGER_TIMEOUT_MS INT_MAX +#define GXFP_CAPTURE_TIMEOUT_MS 20000 +#define GXFP_IMAGE_WIDTH 64 +#define GXFP_IMAGE_HEIGHT 80 +#define GXFP_IMAGE_PPMM (500.0 / 25.4) + +typedef enum { + GXFP_JOB_OPEN, + GXFP_JOB_ACTIVATE, + GXFP_JOB_WAIT_FINGER_DOWN, + GXFP_JOB_CAPTURE, + GXFP_JOB_WAIT_FINGER_UP, + GXFP_JOB_DEACTIVATE, + GXFP_JOB_CLOSE, +} GxfpJob; + +typedef struct { + GxfpJob job; + gint result; + guint8 *image_data; + gsize image_size; +} GxfpJobResult; + +struct _FpiDeviceGxfp5130 +{ + FpImageDevice parent; + struct gxfp_session *session; + gchar *device_path; + gchar *debug_image_dir; + guint64 capture_sequence; + gboolean worker_running; + gboolean deactivating; +}; + +G_DECLARE_FINAL_TYPE (FpiDeviceGxfp5130, fpi_device_gxfp5130, + FPI, DEVICE_GXFP5130, FpImageDevice) +G_DEFINE_TYPE (FpiDeviceGxfp5130, fpi_device_gxfp5130, FP_TYPE_IMAGE_DEVICE) +static void +gxfp_save_debug_image (FpiDeviceGxfp5130 *self, + const guint8 *data, + gsize size) +{ + g_autofree gchar *filename = NULL; + g_autofree gchar *contents = NULL; + g_autoptr(GError) error = NULL; + gsize header_size; + + if (!self->debug_image_dir || !self->debug_image_dir[0]) + return; + + if (size != GXFP_IMAGE_WIDTH * GXFP_IMAGE_HEIGHT) + { + fp_warn ("Refusing to save debug image with invalid size %" G_GSIZE_FORMAT, + size); + return; + } + + if (g_mkdir_with_parents (self->debug_image_dir, 0700) < 0) + { + fp_warn ("Failed to create debug image directory %s: %s", + self->debug_image_dir, g_strerror (errno)); + return; + } + + self->capture_sequence++; + filename = g_strdup_printf ("%s/capture-%u-%06" G_GUINT64_FORMAT ".pgm", + self->debug_image_dir, + (guint) getpid (), + self->capture_sequence); + contents = g_malloc (32 + size); + header_size = g_snprintf (contents, 32, "P5\n%d %d\n255\n", + GXFP_IMAGE_WIDTH, GXFP_IMAGE_HEIGHT); + memcpy (contents + header_size, data, size); + + if (!g_file_set_contents (filename, contents, header_size + size, &error)) + fp_warn ("Failed to save debug image %s: %s", filename, error->message); + else + fp_dbg ("Saved raw debug image to %s", filename); +} + +static void gxfp_start_job (FpiDeviceGxfp5130 *self, GxfpJob job); + +static GError * +gxfp_error_new (gint result) +{ + gint error_number = result < 0 ? -result : EIO; + + return g_error_new (G_IO_ERROR, g_io_error_from_errno (error_number), + "GXFP session failed: %s (%d)", + g_strerror (error_number), result); +} + +static void +gxfp_job_result_free (GxfpJobResult *result) +{ + if (!result) + return; + g_free (result->image_data); + g_free (result); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GxfpJobResult, gxfp_job_result_free) + +static void +gxfp_job_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (source_object); + GxfpJob job = GPOINTER_TO_INT (task_data); + GxfpJobResult *result = g_new0 (GxfpJobResult, 1); + + (void) cancellable; + result->job = job; + + switch (job) + { + case GXFP_JOB_OPEN: + result->result = gxfp_session_open (&self->session, self->device_path); + break; + + case GXFP_JOB_ACTIVATE: + result->result = gxfp_session_activate (self->session, + GXFP_ACTIVATE_TIMEOUT_MS); + break; + + case GXFP_JOB_WAIT_FINGER_DOWN: + result->result = gxfp_session_arm_finger_down (self->session); + if (!result->result) + result->result = gxfp_session_wait_finger_down ( + self->session, GXFP_FINGER_TIMEOUT_MS); + break; + + case GXFP_JOB_CAPTURE: + { + struct gxfp_image12 image = { 0 }; + + result->result = gxfp_session_capture (self->session, &image, + GXFP_CAPTURE_TIMEOUT_MS); + if (!result->result) + { + result->image_size = image.width * image.height; + result->image_data = g_malloc (result->image_size); + result->result = gxfp_image12_to_u8 (&image, + result->image_data, + result->image_size); + } + gxfp_image12_clear (&image); + } + break; + + case GXFP_JOB_WAIT_FINGER_UP: + result->result = gxfp_session_arm_finger_up (self->session); + if (!result->result) + result->result = gxfp_session_wait_finger_up ( + self->session, GXFP_FINGER_TIMEOUT_MS); + break; + + case GXFP_JOB_DEACTIVATE: + result->result = gxfp_session_deactivate (self->session); + break; + + case GXFP_JOB_CLOSE: + gxfp_session_close (self->session); + self->session = NULL; + result->result = 0; + break; + + default: + result->result = -EINVAL; + break; + } + + g_task_return_pointer (task, result, (GDestroyNotify) gxfp_job_result_free); +} + +static void +gxfp_finish_deactivate (FpiDeviceGxfp5130 *self, gint result) +{ + self->deactivating = FALSE; + fpi_image_device_deactivate_complete ( + FP_IMAGE_DEVICE (self), result ? gxfp_error_new (result) : NULL); +} + +static void +gxfp_job_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (source_object); + g_autoptr(GxfpJobResult) result = g_task_propagate_pointer (G_TASK (res), NULL); + FpImageDevice *image_device = FP_IMAGE_DEVICE (self); + + (void) user_data; + self->worker_running = FALSE; + + if (self->deactivating && result->job != GXFP_JOB_DEACTIVATE) + { + gxfp_start_job (self, GXFP_JOB_DEACTIVATE); + return; + } + + if (result->result) + { + GError *error = gxfp_error_new (result->result); + + switch (result->job) + { + case GXFP_JOB_OPEN: + fpi_image_device_open_complete (image_device, error); + break; + case GXFP_JOB_ACTIVATE: + fpi_image_device_activate_complete (image_device, error); + break; + case GXFP_JOB_DEACTIVATE: + gxfp_finish_deactivate (self, result->result); + g_error_free (error); + break; + case GXFP_JOB_CLOSE: + fpi_image_device_close_complete (image_device, error); + break; + case GXFP_JOB_WAIT_FINGER_DOWN: + case GXFP_JOB_CAPTURE: + case GXFP_JOB_WAIT_FINGER_UP: + default: + fpi_image_device_session_error (image_device, error); + break; + } + return; + } + + switch (result->job) + { + case GXFP_JOB_OPEN: + fpi_image_device_open_complete (image_device, NULL); + break; + + case GXFP_JOB_ACTIVATE: + fpi_image_device_activate_complete (image_device, NULL); + break; + + case GXFP_JOB_WAIT_FINGER_DOWN: + fpi_image_device_report_finger_status (image_device, TRUE); + break; + + case GXFP_JOB_CAPTURE: + { + FpImage *image; + + g_assert (result->image_size == GXFP_IMAGE_WIDTH * GXFP_IMAGE_HEIGHT); + image = fp_image_new (GXFP_IMAGE_WIDTH, GXFP_IMAGE_HEIGHT); + memcpy (image->data, result->image_data, result->image_size); + image->ppmm = GXFP_IMAGE_PPMM; + image->flags = FPI_IMAGE_NONE; + gxfp_save_debug_image (self, result->image_data, result->image_size); + fpi_image_device_image_captured (image_device, image); + } + break; + + case GXFP_JOB_WAIT_FINGER_UP: + fpi_image_device_report_finger_status (image_device, FALSE); + break; + + case GXFP_JOB_DEACTIVATE: + gxfp_finish_deactivate (self, 0); + break; + + case GXFP_JOB_CLOSE: + g_clear_pointer (&self->device_path, g_free); + g_clear_pointer (&self->debug_image_dir, g_free); + fpi_image_device_close_complete (image_device, NULL); + break; + } +} + +static void +gxfp_start_job (FpiDeviceGxfp5130 *self, GxfpJob job) +{ + g_autoptr(GTask) task = NULL; + + g_assert (!self->worker_running); + self->worker_running = TRUE; + task = g_task_new (self, NULL, gxfp_job_done, NULL); + g_task_set_task_data (task, GINT_TO_POINTER (job), NULL); + g_task_run_in_thread (task, gxfp_job_thread); +} + +static void +gxfp_dev_open (FpImageDevice *device) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device); + const gchar *path = fpi_device_get_virtual_env (FP_DEVICE (device)); + + self->device_path = g_strdup (path && path[0] ? path + : GXFP_DEVICE_PATH_DEFAULT); + self->debug_image_dir = g_strdup (g_getenv ("GXFP_DEBUG_IMAGE_DIR")); + self->capture_sequence = 0; + gxfp_start_job (self, GXFP_JOB_OPEN); +} + +static void +gxfp_dev_close (FpImageDevice *device) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device); + + g_assert (!self->worker_running); + gxfp_start_job (self, GXFP_JOB_CLOSE); +} + +static void +gxfp_dev_activate (FpImageDevice *device) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device); + + self->deactivating = FALSE; + gxfp_start_job (self, GXFP_JOB_ACTIVATE); +} + +static void +gxfp_dev_deactivate (FpImageDevice *device) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device); + + self->deactivating = TRUE; + if (self->worker_running) + { + gxfp_session_cancel (self->session); + return; + } + gxfp_start_job (self, GXFP_JOB_DEACTIVATE); +} + +static void +gxfp_dev_change_state (FpImageDevice *device, FpiImageDeviceState state) +{ + FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device); + + if (self->deactivating || self->worker_running) + return; + + switch (state) + { + case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON: + gxfp_start_job (self, GXFP_JOB_WAIT_FINGER_DOWN); + break; + case FPI_IMAGE_DEVICE_STATE_CAPTURE: + gxfp_start_job (self, GXFP_JOB_CAPTURE); + break; + case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF: + gxfp_start_job (self, GXFP_JOB_WAIT_FINGER_UP); + break; + case FPI_IMAGE_DEVICE_STATE_INACTIVE: + case FPI_IMAGE_DEVICE_STATE_ACTIVATING: + case FPI_IMAGE_DEVICE_STATE_DEACTIVATING: + case FPI_IMAGE_DEVICE_STATE_IDLE: + default: + break; + } +} + +static const FpIdEntry id_table[] = { + { .virtual_envvar = "FP_GXFP5130" }, + { .virtual_envvar = NULL }, +}; + +static void +fpi_device_gxfp5130_init (FpiDeviceGxfp5130 *self) +{ + self->session = NULL; +} + +static void +fpi_device_gxfp5130_class_init (FpiDeviceGxfp5130Class *klass) +{ + FpDeviceClass *device_class = FP_DEVICE_CLASS (klass); + FpImageDeviceClass *image_class = FP_IMAGE_DEVICE_CLASS (klass); + + device_class->id = FP_COMPONENT; + device_class->full_name = "Goodix GXFP5130 (ChicagoHU 0x2504)"; + device_class->type = FP_DEVICE_TYPE_VIRTUAL; + device_class->id_table = id_table; + device_class->scan_type = FP_SCAN_TYPE_PRESS; + + image_class->img_open = gxfp_dev_open; + image_class->img_close = gxfp_dev_close; + image_class->activate = gxfp_dev_activate; + image_class->deactivate = gxfp_dev_deactivate; + image_class->change_state = gxfp_dev_change_state; + image_class->img_width = GXFP_IMAGE_WIDTH; + image_class->img_height = GXFP_IMAGE_HEIGHT; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..4b0122f --- /dev/null +++ b/src/config.c @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/config.h" +#include "gxfp/sensor.h" + +#include +#include +#include +#include + +#define GXFP_CONFIG_CHECKSUM_WORDS 0x6fu +#define GXFP_CONFIG_CHECKSUM_OFFSET 0xdeu + +#define GXFP_OTP_FDT_OFFSET 0x1bu +#define GXFP_OTP_TCODE_A_OFFSET 0x2au +#define GXFP_OTP_TCODE_INV_OFFSET 0x2bu +#define GXFP_OTP_TCODE_B_OFFSET 0x2du +#define GXFP_CHICAGOHU_OTP_MIN_SIZE 0x36u + +static const uint8_t chicagohu_2504_base_config[GXFP_CONFIG_SIZE] = { + 0x70, 0x11, 0x74, 0x85, 0x00, 0x85, 0x2c, 0xb1, 0x18, 0xc9, 0x14, 0xdd, + 0x00, 0xdd, 0x00, 0xdd, 0x00, 0xba, 0x00, 0x01, 0x80, 0xca, 0x00, 0x04, + 0x00, 0x84, 0x00, 0x15, 0xb3, 0x86, 0x00, 0x00, 0xc4, 0x88, 0x00, 0x00, + 0xba, 0x8a, 0x00, 0x00, 0xb2, 0x8c, 0x00, 0x00, 0xaa, 0x8e, 0x00, 0x00, + 0xc1, 0x90, 0x00, 0xbb, 0xbb, 0x92, 0x00, 0xb1, 0xb1, 0x94, 0x00, 0x00, + 0xa8, 0x96, 0x00, 0x00, 0xb6, 0x98, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, + 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, + 0x00, 0xd8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x01, 0x05, 0xd0, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x00, 0x72, 0x00, 0x78, 0x56, 0x74, 0x00, 0x34, + 0x12, 0x20, 0x00, 0x10, 0x40, 0x5c, 0x00, 0x80, 0x01, 0x20, 0x02, 0x08, + 0x08, 0x36, 0x02, 0x80, 0x00, 0x38, 0x02, 0x80, 0x00, 0x3a, 0x02, 0x80, + 0x00, 0x2a, 0x01, 0x82, 0x03, 0x22, 0x00, 0x01, 0x20, 0x24, 0x00, 0x14, + 0x00, 0x80, 0x00, 0x01, 0x00, 0x5c, 0x00, 0x00, 0x01, 0x56, 0x00, 0x04, + 0x20, 0x58, 0x00, 0x03, 0x02, 0x32, 0x00, 0x0c, 0x02, 0x66, 0x00, 0x03, + 0x00, 0x7c, 0x00, 0x00, 0x58, 0x82, 0x00, 0x80, 0x15, 0x2a, 0x01, 0x08, + 0x00, 0x54, 0x00, 0x10, 0x01, 0x62, 0x00, 0x04, 0x03, 0x64, 0x00, 0x19, + 0x00, 0x66, 0x00, 0x03, 0x00, 0x7c, 0x00, 0x00, 0x58, 0x2a, 0x01, 0x08, + 0x00, 0x52, 0x00, 0x08, 0x00, 0x54, 0x00, 0x00, 0x01, 0x66, 0x00, 0x03, + 0x00, 0x7c, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, +}; + +static uint16_t get_le16(const uint8_t *data) +{ + return (uint16_t)data[0] | ((uint16_t)data[1] << 8); +} + +static void put_le16(uint8_t *data, uint16_t value) +{ + data[0] = (uint8_t)(value & 0xff); + data[1] = (uint8_t)(value >> 8); +} + +uint16_t gxfp_config_checksum(const uint8_t config[GXFP_CONFIG_SIZE]) +{ + uint16_t sum = 0xa5a5u; + size_t i; + + for (i = 0; i < GXFP_CONFIG_CHECKSUM_WORDS; i++) + sum = (uint16_t)(sum + get_le16(config + i * 2u)); + + return (uint16_t)(0u - sum); +} + +int gxfp_config_update_checksum(uint8_t config[GXFP_CONFIG_SIZE]) +{ + uint16_t checksum; + + if (!config) + return -EINVAL; + + checksum = gxfp_config_checksum(config); + put_le16(config + GXFP_CONFIG_CHECKSUM_OFFSET, checksum); + return 0; +} + +int gxfp_config_modify_register(uint8_t config[GXFP_CONFIG_SIZE], + uint16_t reg, + uint16_t value, + unsigned int section, + enum gxfp_config_write_mode mode, + uint16_t *old_value) +{ + size_t start; + size_t length; + size_t offset; + + if (!config || section >= GXFP_CONFIG_SECTION_COUNT) + return -EINVAL; + + start = config[1u + section * 2u]; + length = config[2u + section * 2u]; + + if (start >= GXFP_CONFIG_CHECKSUM_OFFSET || + length > GXFP_CONFIG_CHECKSUM_OFFSET - start || + (length % 4u) != 0u) + return -EBADMSG; + + for (offset = 0; offset < length; offset += 4u) { + uint8_t *entry = config + start + offset; + uint16_t entry_reg = get_le16(entry); + uint16_t current; + + if (entry_reg != reg) + continue; + + current = get_le16(entry + 2u); + if (old_value) + *old_value = current; + + switch (mode) { + case GXFP_CONFIG_WRITE_WORD: + put_le16(entry + 2u, value); + break; + case GXFP_CONFIG_WRITE_LOW_BYTE: + entry[2] = (uint8_t)value; + break; + case GXFP_CONFIG_WRITE_HIGH_BYTE: + entry[3] = (uint8_t)(value >> 8); + break; + default: + return -EINVAL; + } + + return gxfp_config_update_checksum(config); + } + + return -ENOENT; +} + +int gxfp_config_patch_dac(uint8_t config[GXFP_CONFIG_SIZE], + const uint16_t register_values[4]) +{ + static const uint16_t registers[4] = { + 0x0220, 0x0236, 0x0238, 0x023a + }; + unsigned int i; + int ret; + + if (!config || !register_values) + return -EINVAL; + + for (i = 0; i < 4; i++) { + ret = gxfp_config_modify_register(config, + registers[i], + register_values[i], + 0, + GXFP_CONFIG_WRITE_WORD, + NULL); + if (ret) + return ret; + } + + return 0; +} + +static int parse_tcode_diff(const uint8_t *otp, uint8_t *tcode_diff) +{ + uint8_t a = otp[GXFP_OTP_TCODE_A_OFFSET]; + uint8_t inv = otp[GXFP_OTP_TCODE_INV_OFFSET]; + uint8_t b = otp[GXFP_OTP_TCODE_B_OFFSET]; + + if (a != 0 && a == (uint8_t)~inv) { + *tcode_diff = a; + return 0; + } + + if (b != 0 && b == (uint8_t)~inv) { + *tcode_diff = b; + return 0; + } + + if (a != 0 && a == b) { + *tcode_diff = a; + return 0; + } + + return -EBADMSG; +} + +static uint8_t parse_fdt_offset(uint8_t value) +{ + uint8_t low = value & 0x03u; + uint8_t high = (value >> 4) & 0x03u; + uint8_t inverted = ((uint8_t)~value >> 2) & 0x03u; + + if (low == high || low == inverted) + return low; + if (high == inverted) + return high; + + return 0; +} + +int gxfp_config_build_chicagohu_2504( + uint8_t config[GXFP_CONFIG_SIZE], + const void *otp_data, + size_t otp_len, + const struct gxfp_dac_info *dac, + struct gxfp_chicagohu_config_info *info) +{ + const uint8_t *otp = otp_data; + uint8_t tcode_diff; + uint16_t tcode; + uint16_t fdt_tmp; + uint8_t fdt_delta; + uint8_t fdt_offset; + int ret; + + if (!config || !otp || !dac || !info) + return -EINVAL; + if (otp_len < GXFP_CHICAGOHU_OTP_MIN_SIZE) + return -EBADMSG; + + memset(info, 0, sizeof(*info)); + memcpy(config, chicagohu_2504_base_config, GXFP_CONFIG_SIZE); + + ret = gxfp_config_patch_dac(config, dac->register_value); + if (ret) + return ret; + + ret = parse_tcode_diff(otp, &tcode_diff); + if (ret) + return ret; + + tcode = (uint16_t)((((uint16_t)(tcode_diff >> 4) + 1u) * 0x10u) + 0x40u); + if (tcode == 0) + return -ERANGE; + + ret = gxfp_config_modify_register(config, 0x005c, tcode, 0, + GXFP_CONFIG_WRITE_WORD, NULL); + if (ret) + return ret; + + fdt_tmp = (uint16_t)(((tcode_diff & 0x0fu) + 2u) * 100u); + fdt_delta = (uint8_t)(((((uint32_t)fdt_tmp * 0x100u) / tcode) / 3u) >> 4); + + ret = gxfp_config_modify_register(config, 0x0082, + (uint16_t)fdt_delta << 8, 2, + GXFP_CONFIG_WRITE_HIGH_BYTE, NULL); + if (ret) + return ret; + + fdt_offset = parse_fdt_offset(otp[GXFP_OTP_FDT_OFFSET]); + if (fdt_offset != 0) { + ret = gxfp_config_modify_register(config, 0x0056, + (uint16_t)(fdt_offset + 4u), 2, + GXFP_CONFIG_WRITE_LOW_BYTE, NULL); + if (ret) + return ret; + } + + ret = gxfp_config_update_checksum(config); + if (ret) + return ret; + + info->tcode_diff = tcode_diff; + info->tcode = tcode; + info->fdt_delta = fdt_delta; + info->fdt_offset = fdt_offset; + info->checksum = get_le16(config + GXFP_CONFIG_CHECKSUM_OFFSET); + + return 0; +} diff --git a/src/device.c b/src/device.c new file mode 100644 index 0000000..9724dfa --- /dev/null +++ b/src/device.c @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L + +#include "gxfp/device.h" +#include "gxfp/goodix_ec_uapi.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +int gxfp_device_open(struct gxfp_device *device, const char *path) +{ + if (!device || !path) + return -EINVAL; + + device->fd = open(path, O_RDWR | O_NONBLOCK); + if (device->fd < 0) + return -errno; + + return 0; +} + +void gxfp_device_close(struct gxfp_device *device) +{ + if (!device) + return; + + if (device->fd >= 0) + close(device->fd); + + device->fd = -1; +} + +int gxfp_device_flush_rx(struct gxfp_device *device) +{ + if (!device || device->fd < 0) + return -EINVAL; + + if (ioctl(device->fd, GOODIX_EC_IOCTL_FLUSH_RX) < 0) + return -errno; + + return 0; +} + +int gxfp_device_send_mp(struct gxfp_device *device, uint8_t mp_flags, + const void *payload, size_t payload_len) +{ + struct goodix_ec_tx_header header; + uint8_t *record; + size_t total_len; + ssize_t written; + + if (!device || device->fd < 0 || (payload_len && !payload)) + return -EINVAL; + + if (payload_len > GOODIX_EC_UAPI_TX_MAX) + return -EMSGSIZE; + + total_len = sizeof(header) + payload_len; + record = calloc(1, total_len); + if (!record) + return -ENOMEM; + + memset(&header, 0, sizeof(header)); + header.mp_flags = mp_flags; + header.payload_len = (uint16_t)payload_len; + + memcpy(record, &header, sizeof(header)); + if (payload_len) + memcpy(record + sizeof(header), payload, payload_len); + + written = write(device->fd, record, total_len); + free(record); + + if (written < 0) + return -errno; + + if ((size_t)written != total_len) + return -EIO; + + return 0; +} + +int gxfp_device_receive(struct gxfp_device *device, + struct gxfp_rx_record *record, + int timeout_ms) +{ + struct goodix_ec_record_header *header; + struct pollfd poll_fd; + uint8_t *buffer; + size_t capacity; + ssize_t bytes_read; + int ret; + + if (!device || device->fd < 0 || !record) + return -EINVAL; + + memset(record, 0, sizeof(*record)); + + poll_fd.fd = device->fd; + poll_fd.events = POLLIN; + + do { + ret = poll(&poll_fd, 1, timeout_ms); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) + return -errno; + + if (ret == 0) + return -ETIMEDOUT; + + if (!(poll_fd.revents & POLLIN)) + return -EIO; + + capacity = sizeof(*header) + GOODIX_EC_UAPI_RX_MAX; + buffer = malloc(capacity); + if (!buffer) + return -ENOMEM; + + bytes_read = read(device->fd, buffer, capacity); + if (bytes_read < 0) { + ret = -errno; + free(buffer); + return ret; + } + + if ((size_t)bytes_read < sizeof(*header)) { + free(buffer); + return -EBADMSG; + } + + header = (struct goodix_ec_record_header *)buffer; + if (header->len > (uint32_t)(bytes_read - sizeof(*header))) { + free(buffer); + return -EBADMSG; + } + + record->payload = malloc(header->len); + if (!record->payload) { + free(buffer); + return -ENOMEM; + } + + memcpy(record->payload, buffer + sizeof(*header), header->len); + record->payload_len = header->len; + record->mp_type = header->mp_type; + record->timestamp_ns = header->timestamp_ns; + + free(buffer); + return 0; +} + +void gxfp_rx_record_release(struct gxfp_rx_record *record) +{ + if (!record) + return; + + free(record->payload); + memset(record, 0, sizeof(*record)); +} diff --git a/src/fdt.c b/src/fdt.c new file mode 100644 index 0000000..2297a72 --- /dev/null +++ b/src/fdt.c @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L + +#include "gxfp/fdt.h" +#include "gxfp/device.h" +#include "gxfp/protocol.h" + +#include +#include +#include +#include + +#define GXFP_CMD_FDT_DOWN 0x32 +#define GXFP_CMD_FDT_UP 0x34 +#define GXFP_CMD_FDT_MODE 0x36 +#define GXFP_CMD_FDT_STATUS 0xDA + +#define GXFP_FDT_TABLE_LEN 24 +#define GXFP_FDT_TRAILER_LEN 8 +#define GXFP_FDT_PAYLOAD_LEN (2 + GXFP_FDT_TABLE_LEN + GXFP_FDT_TRAILER_LEN) + +static void sleep_ms(unsigned int milliseconds) +{ + struct timespec delay = { + .tv_sec = milliseconds / 1000, + .tv_nsec = (long)(milliseconds % 1000) * 1000000L, + }; + + while (nanosleep(&delay, &delay) < 0 && errno == EINTR) + ; +} + +static uint16_t get_le16(const uint8_t *data) +{ + return (uint16_t)data[0] | ((uint16_t)data[1] << 8); +} + +static int send_fdt_command(struct gxfp_device *device, + uint8_t command, + uint8_t magic0, + uint8_t magic1) +{ + uint8_t payload[GXFP_FDT_PAYLOAD_LEN] = { 0 }; + uint8_t frame[64]; + size_t frame_len; + int ret; + + payload[0] = magic0; + payload[1] = magic1; + + ret = gxfp_frame_build(command, payload, sizeof(payload), + frame, sizeof(frame), &frame_len); + if (ret) + return ret; + + return gxfp_device_send_mp(device, GXFP_MP_COMMAND, + frame, frame_len); +} + +static int arm_mode(struct gxfp_device *device) +{ + return send_fdt_command(device, GXFP_CMD_FDT_MODE, 0x09, 0x01); +} + +void gxfp_fdt_init(struct gxfp_fdt *fdt) +{ + if (!fdt) + return; + + memset(fdt, 0, sizeof(*fdt)); +} + +int gxfp_fdt_arm_down(struct gxfp_device *device) +{ + int ret; + + ret = arm_mode(device); + if (ret) + return ret; + + sleep_ms(10); + return send_fdt_command(device, GXFP_CMD_FDT_DOWN, 0x08, 0x01); +} + +int gxfp_fdt_arm_up(struct gxfp_device *device) +{ + int ret; + + ret = arm_mode(device); + if (ret) + return ret; + + sleep_ms(10); + return send_fdt_command(device, GXFP_CMD_FDT_UP, 0x0a, 0x01); +} + +enum gxfp_fdt_event +gxfp_fdt_decode(const struct gxfp_frame_view *frame, uint16_t *status) +{ + uint16_t value; + + if (status) + *status = 0; + + if (!frame || frame->payload_len < 2) + return GXFP_FDT_EVENT_NONE; + + if (frame->command != GXFP_CMD_FDT_STATUS && + frame->command != GXFP_CMD_FDT_DOWN && + frame->command != GXFP_CMD_FDT_MODE && + frame->command != GXFP_CMD_FDT_UP) + return GXFP_FDT_EVENT_NONE; + + value = get_le16(frame->payload); + if (status) + *status = value; + + switch (value) { + case 0x0100: + return GXFP_FDT_EVENT_READY; + case 0x0002: + return GXFP_FDT_EVENT_FINGER_DOWN; + case 0x0200: + return GXFP_FDT_EVENT_FINGER_UP; + case 0x0080: + case 0x0082: + return GXFP_FDT_EVENT_REVERSE; + default: + return GXFP_FDT_EVENT_NONE; + } +} + +int gxfp_fdt_handle_event(struct gxfp_fdt *fdt, + struct gxfp_device *device, + enum gxfp_fdt_event event) +{ + if (!fdt || !device) + return -EINVAL; + + switch (event) { + case GXFP_FDT_EVENT_FINGER_DOWN: + fdt->waiting_for_up = true; + return gxfp_fdt_arm_up(device); + + case GXFP_FDT_EVENT_FINGER_UP: + case GXFP_FDT_EVENT_REVERSE: + fdt->waiting_for_up = false; + return gxfp_fdt_arm_down(device); + + case GXFP_FDT_EVENT_NONE: + case GXFP_FDT_EVENT_READY: + default: + return 0; + } +} diff --git a/src/protocol.c b/src/protocol.c new file mode 100644 index 0000000..bad670b --- /dev/null +++ b/src/protocol.c @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/protocol.h" + +#include +#include +#include + +static uint16_t get_le16(const uint8_t *data) +{ + return (uint16_t)data[0] | ((uint16_t)data[1] << 8); +} + +static void put_le16(uint8_t *data, uint16_t value) +{ + data[0] = (uint8_t)(value & 0xff); + data[1] = (uint8_t)(value >> 8); +} + +static uint8_t checksum_for(uint8_t command, uint16_t declared_len, + const uint8_t *payload, size_t payload_len) +{ + uint8_t sum = command + + (uint8_t)(declared_len & 0xff) + + (uint8_t)(declared_len >> 8); + size_t index; + + for (index = 0; index < payload_len; index++) + sum = (uint8_t)(sum + payload[index]); + + return (uint8_t)(GXFP_GOODIX_CHECKSUM_TARGET - sum); +} + +int gxfp_frame_build(uint8_t command, + const void *payload, size_t payload_len, + uint8_t *output, size_t output_capacity, + size_t *output_len) +{ + const uint8_t *bytes = payload; + uint16_t declared_len; + size_t total_len; + + if (!output || !output_len || (payload_len && !payload)) + return -EINVAL; + + if (payload_len > UINT16_MAX - 1) + return -EOVERFLOW; + + declared_len = (uint16_t)(payload_len + 1); + total_len = 3 + payload_len + 1; + + if (total_len > output_capacity) + return -EMSGSIZE; + + output[0] = command; + put_le16(output + 1, declared_len); + + if (payload_len) + memcpy(output + 3, bytes, payload_len); + + output[3 + payload_len] = + checksum_for(command, declared_len, bytes, payload_len); + + *output_len = total_len; + return 0; +} + +int gxfp_frame_parse(const void *frame, size_t frame_len, + struct gxfp_frame_view *view) +{ + const uint8_t *bytes = frame; + uint16_t declared_len; + size_t payload_len; + size_t total_len; + uint8_t checksum; + + if (!frame || !view || frame_len < 4) + return -EINVAL; + + declared_len = get_le16(bytes + 1); + if (declared_len < 1) + return -EBADMSG; + + payload_len = declared_len - 1; + total_len = 3 + payload_len + 1; + + if (total_len > frame_len) + return -EBADMSG; + + checksum = bytes[3 + payload_len]; + if (checksum_for(bytes[0], declared_len, bytes + 3, payload_len) != + checksum) + return -EBADMSG; + + view->command = bytes[0]; + view->payload = bytes + 3; + view->payload_len = payload_len; + view->checksum = checksum; + + return 0; +} diff --git a/src/request.c b/src/request.c new file mode 100644 index 0000000..4523940 --- /dev/null +++ b/src/request.c @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/request.h" + +#include "gxfp/device.h" +#include "gxfp/protocol.h" +#include "gxfp/goodix_ec_uapi.h" +#include +#include +#include +#include +#include + +#define GXFP_TLS_RX_MP_TYPE 0x0bu + +static int gxfp_debug_enabled(void) +{ + const char *value = getenv("GXFP_DEBUG"); + + return value && value[0] && strcmp(value, "0") != 0; +} + +static void debug_dump(const uint8_t *data, size_t length, size_t limit) +{ + size_t dump_len = length < limit ? length : limit; + size_t i; + + for (i = 0; i < dump_len; i++) + fprintf(stderr, " %02x", data[i]); + if (length > dump_len) + fprintf(stderr, " ..."); + fputc('\n', stderr); +} + +int gxfp_request(struct gxfp_device *device, + uint8_t command, + const void *payload, + size_t payload_len, + uint8_t expected_command, + unsigned int max_frames, + int timeout_ms, + struct gxfp_response *response) +{ + uint8_t frame[GOODIX_EC_UAPI_TX_MAX]; + size_t frame_len; + unsigned int frame_index; + int ret; + + if (!device || !response || max_frames == 0) + return -EINVAL; + + memset(response, 0, sizeof(*response)); + + ret = gxfp_frame_build(command, payload, payload_len, + frame, sizeof(frame), &frame_len); + if (ret) + return ret; + + ret = gxfp_device_send_mp(device, GXFP_MP_COMMAND, frame, frame_len); + if (ret) + return ret; + + for (frame_index = 0; frame_index < max_frames; frame_index++) { + struct gxfp_rx_record record; + struct gxfp_frame_view view; + + ret = gxfp_device_receive(device, &record, timeout_ms); + if (ret) + return ret; + + if (record.mp_type == GXFP_TLS_RX_MP_TYPE) { + if (gxfp_debug_enabled()) { + fprintf(stderr, + "request RX: skipping TLS record len=%zu\n", + record.payload_len); + fprintf(stderr, "payload:"); + debug_dump(record.payload, record.payload_len, 64); + } + gxfp_rx_record_release(&record); + continue; + } + + ret = gxfp_frame_parse(record.payload, record.payload_len, &view); + if (ret) { + if (gxfp_debug_enabled()) { + fprintf(stderr, + "request RX parse failed: ret=%d len=%zu " + "mp=0x%02x\n", ret, record.payload_len, + record.mp_type); + fprintf(stderr, "raw payload:"); + debug_dump(record.payload, record.payload_len, 64); + } + gxfp_rx_record_release(&record); + continue; + } + + /* + * B0 may be an intermediate ACK. It is final only when the caller + * explicitly expects B0. + */ + if (view.command == GXFP_GOODIX_ACK && + expected_command != GXFP_GOODIX_ACK) { + gxfp_rx_record_release(&record); + continue; + } + + if (view.command != expected_command) { + if (gxfp_debug_enabled()) { + fprintf(stderr, + "request RX unmatched: cmd=0x%02x " + "expected=0x%02x len=%zu\n", + view.command, expected_command, + view.payload_len); + if (view.payload_len) { + fprintf(stderr, "payload:"); + debug_dump(view.payload, view.payload_len, 32); + } + } + gxfp_rx_record_release(&record); + continue; + } + + if (view.payload_len) { + response->payload = malloc(view.payload_len); + if (!response->payload) { + gxfp_rx_record_release(&record); + return -ENOMEM; + } + memcpy(response->payload, view.payload, view.payload_len); + } + + response->command = view.command; + response->payload_len = view.payload_len; + response->timestamp_ns = record.timestamp_ns; + + gxfp_rx_record_release(&record); + return 0; + } + + return -ETIMEDOUT; +} + +void gxfp_response_release(struct gxfp_response *response) +{ + if (!response) + return; + + free(response->payload); + memset(response, 0, sizeof(*response)); +} diff --git a/src/sensor.c b/src/sensor.c new file mode 100644 index 0000000..982ddc0 --- /dev/null +++ b/src/sensor.c @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/sensor.h" + +#include "gxfp/config.h" + +#include "gxfp/device.h" +#include "gxfp/request.h" +#include "gxfp/protocol.h" +#define GXFP_MP_COMMAND 0xA0 +#include +#include +#include +#include +#include + +#define GXFP_CMD_REG_READ 0x82 +#define GXFP_CMD_READ_OTP 0xA6 +#define GXFP_CMD_SENSOR_RESET 0xA2 +#define GXFP_CMD_CONFIG_DOWNLOAD 0x90 +#define GXFP_CMD_IMAGE_CAPTURE 0x20 +#define GXFP_CMD_ACK 0xB0 +#define GXFP_CMD_TLS_INIT 0xD0 +#define GXFP_CMD_PROTOCOL_INIT 0x01 +#define GXFP_CMD_NOTIFY_POWER 0x0E +#define GXFP_CMD_GET_VERSION 0xA8 +#define GXFP_CMD_QUERY_MCU_STATE 0xAE +#define GXFP_CMD_TRIGGER_MCU_STATE 0xAF +#define GXFP_CMD_SLEEP_CLEANUP 0xD2 +#define GXFP_CMD_TLS_UNLOCK 0xD4 + +#define GXFP_MCU_STATE_SIZE 20u +#define GXFP_RESET_SETTLE_MS 200L + +#define GXFP_OTP_SIZE 0x40u +#define GXFP_OTP_MT_DAC_OFFSET 0x2eu +#define GXFP_OTP_FT_DAC_OFFSET 0x32u +#define GXFP_OTP_DAC_COUNT 4u + +#define GXFP_OTP_CP_CRC_OFFSET 0x3cu +#define GXFP_OTP_FT_CRC_OFFSET 0x3du +#define GXFP_OTP_MT_CRC_OFFSET 0x3fu + +static void put_le16(uint8_t *data, uint16_t value) +{ + data[0] = (uint8_t)(value & 0xff); + data[1] = (uint8_t)(value >> 8); +} + +static uint8_t crc8_poly07_not(const uint8_t *data, size_t len) +{ + uint8_t crc = 0; + size_t i; + unsigned int bit; + + for (i = 0; i < len; i++) { + crc ^= data[i]; + + for (bit = 0; bit < 8; bit++) { + if (crc & 0x80u) + crc = (uint8_t)((crc << 1) ^ 0x07u); + else + crc <<= 1; + } + } + + return (uint8_t)~crc; +} + +int gxfp_sensor_read_register(struct gxfp_device *device, + uint16_t address, + void *output, + uint16_t output_len) +{ + uint8_t request_payload[5]; + struct gxfp_response response; + int ret; + + if (!device || !output || output_len == 0) + return -EINVAL; + + request_payload[0] = 0x00; + put_le16(request_payload + 1, address); + put_le16(request_payload + 3, output_len); + + ret = gxfp_request(device, + GXFP_CMD_REG_READ, + request_payload, + sizeof(request_payload), + GXFP_CMD_REG_READ, + 4, + 500, + &response); + if (ret) + return ret; + + if (response.payload_len < output_len) { + gxfp_response_release(&response); + return -EBADMSG; + } + + fprintf(stderr, + "REG 0x%04x response cmd=0x%02x len=%zu payload:", + address, + response.command, + response.payload_len); + + for (size_t i = 0; i < response.payload_len; i++) + fprintf(stderr, " %02x", response.payload[i]); + + fputc('\n', stderr); + + memcpy(output, response.payload, output_len); + gxfp_response_release(&response); + return 0; +} + +static int sleep_ms(long milliseconds) +{ + struct timespec request; + + request.tv_sec = milliseconds / 1000; + request.tv_nsec = (milliseconds % 1000) * 1000000L; + + while (nanosleep(&request, &request) != 0) { + if (errno != EINTR) + return -errno; + } + + return 0; +} + +static int sensor_expect_ack(struct gxfp_device *device, uint8_t command, + const void *payload, size_t payload_len, + int timeout_ms) +{ + struct gxfp_response response = { 0 }; + int ret; + + ret = gxfp_request(device, command, payload, payload_len, + GXFP_CMD_ACK, 8, timeout_ms, &response); + if (!ret) + gxfp_response_release(&response); + return ret; +} + +int gxfp_sensor_read_version(struct gxfp_device *device, + char *version, size_t version_capacity) +{ + static const uint8_t payload[] = { 0x01 }; + struct gxfp_response response = { 0 }; + size_t length; + int ret; + + if (!device || !version || version_capacity < 2) + return -EINVAL; + + version[0] = '\0'; + ret = gxfp_request(device, GXFP_CMD_GET_VERSION, + payload, sizeof(payload), GXFP_CMD_GET_VERSION, + 4, 1000, &response); + if (ret) + return ret; + + length = response.payload_len; + if (length >= version_capacity) + length = version_capacity - 1; + memcpy(version, response.payload, length); + version[length] = '\0'; + gxfp_response_release(&response); + return length ? 0 : -ENODATA; +} + +int gxfp_sensor_query_mcu_state(struct gxfp_device *device, + struct gxfp_mcu_state *state) +{ + struct timespec now; + struct gxfp_response response = { 0 }; + uint8_t payload[5]; + uint16_t timestamp; + int ret; + + if (!device || !state) + return -EINVAL; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) + return -errno; + + timestamp = (uint16_t)(((uint64_t)(now.tv_sec % 60) * 1000u) + + (uint64_t)now.tv_nsec / 1000000u); + payload[0] = 0x55; + payload[1] = (uint8_t)timestamp; + payload[2] = (uint8_t)(timestamp >> 8); + payload[3] = 0; + payload[4] = 0; + + ret = gxfp_request(device, GXFP_CMD_TRIGGER_MCU_STATE, + payload, sizeof(payload), GXFP_CMD_QUERY_MCU_STATE, + 4, 1000, &response); + if (ret) + return ret; + if (response.payload_len < GXFP_MCU_STATE_SIZE) { + gxfp_response_release(&response); + return -EBADMSG; + } + + memset(state, 0, sizeof(*state)); + state->version = response.payload[0]; + state->flags = response.payload[1]; + state->pov_image_valid = !!(state->flags & (1u << 0)); + state->tls_connected = !!(state->flags & (1u << 1)); + state->tls_used = !!(state->flags & (1u << 2)); + state->locked = !!(state->flags & (1u << 3)); + memcpy(state->reserved, response.payload + 2, + sizeof(state->reserved)); + gxfp_response_release(&response); + return 0; +} + +int gxfp_sensor_recover(struct gxfp_device *device, int unstick_tls) +{ + static const uint8_t power_on[] = { 0x01 }; + static const uint8_t zero2[] = { 0x00, 0x00 }; + static const uint8_t zero4[] = { 0x00, 0x00, 0x00, 0x00 }; + int first_error = 0; + int ret; + + if (!device) + return -EINVAL; + + ret = sensor_expect_ack(device, GXFP_CMD_NOTIFY_POWER, + power_on, sizeof(power_on), 500); + if (ret) + first_error = ret; + (void)sleep_ms(GXFP_RESET_SETTLE_MS); + + if (unstick_tls) { + ret = sensor_expect_ack(device, GXFP_CMD_PROTOCOL_INIT, + zero4, sizeof(zero4), 500); + if (ret && !first_error) + first_error = ret; + ret = sensor_expect_ack(device, GXFP_CMD_TLS_UNLOCK, + zero2, sizeof(zero2), 500); + if (ret && !first_error) + first_error = ret; + } + + ret = sensor_expect_ack(device, GXFP_CMD_SLEEP_CLEANUP, + zero2, sizeof(zero2), 500); + if (ret && !first_error) + first_error = ret; + (void)sleep_ms(GXFP_RESET_SETTLE_MS); + + ret = gxfp_sensor_reset(device); + if (ret) + return ret; + return first_error; +} + + + +int gxfp_sensor_start_tls(struct gxfp_device *device) +{ + static const uint8_t payload[2] = { 0x00, 0x00 }; + uint8_t frame[32]; + size_t frame_len; + int ret; + + if (!device) + return -EINVAL; + + ret = gxfp_frame_build(GXFP_CMD_TLS_INIT, + payload, + sizeof(payload), + frame, + sizeof(frame), + &frame_len); + if (ret) + return ret; + + /* + * The TLS ClientHello may arrive before the normal B0 ACK. Do not + * wait for a response in the request layer; the mbedTLS receive + * callback will consume the incoming TLS records. + */ + return gxfp_device_send_mp(device, + GXFP_MP_COMMAND, + frame, + frame_len); +} + +int gxfp_sensor_reset(struct gxfp_device *device) +{ + static const uint8_t payload[2] = { 0x01, 0x14 }; + struct gxfp_response response; + int ret; + + if (!device) + return -EINVAL; + + ret = gxfp_request(device, + GXFP_CMD_SENSOR_RESET, + payload, + sizeof(payload), + GXFP_CMD_ACK, + 8, + 1000, + &response); + if (ret) + return ret; + + gxfp_response_release(&response); + return sleep_ms(GXFP_RESET_SETTLE_MS); +} + +int gxfp_sensor_download_config(struct gxfp_device *device, + const void *config, + size_t config_len) +{ + struct gxfp_response response; + int ret; + + if (!device || !config) + return -EINVAL; + + if (config_len != GXFP_CONFIG_SIZE) + return -EINVAL; + + ret = gxfp_request(device, + GXFP_CMD_CONFIG_DOWNLOAD, + config, + config_len, + GXFP_CMD_CONFIG_DOWNLOAD, + 32, + 5000, + &response); + if (ret) + return ret; + + gxfp_response_release(&response); + return 0; +} + +int gxfp_sensor_reset_and_download_config( + struct gxfp_device *device, + const void *config, + size_t config_len) +{ + int ret; + + ret = gxfp_sensor_reset(device); + if (ret) + return ret; + + return gxfp_sensor_download_config(device, config, config_len); +} + +int gxfp_sensor_capture_oneframe(struct gxfp_device *device, + void *output, + size_t output_capacity, + size_t *output_len) +{ + static const uint8_t payload[2] = { 0x01, 0x00 }; + struct gxfp_response response = { 0 }; + int ret; + + if (!device || !output || output_capacity == 0 || !output_len) + return -EINVAL; + + *output_len = 0; + + ret = gxfp_request(device, + GXFP_CMD_IMAGE_CAPTURE, + payload, + sizeof(payload), + GXFP_CMD_IMAGE_CAPTURE, + 8, + 5000, + &response); + if (ret) + return ret; + + if (response.payload_len > output_capacity) { + gxfp_response_release(&response); + return -EMSGSIZE; + } + + memcpy(output, response.payload, response.payload_len); + *output_len = response.payload_len; + + gxfp_response_release(&response); + return 0; +} + +int gxfp_sensor_read_chip_id(struct gxfp_device *device, + uint16_t *chip_id) +{ + static const long retry_delay_ms[] = { 0, 25, 50 }; + uint8_t value[4]; + unsigned int attempt; + int ret; + + if (!device || !chip_id) + return -EINVAL; + + *chip_id = 0; + + for (attempt = 0; attempt < 3; attempt++) { + if (retry_delay_ms[attempt] != 0) { + ret = sleep_ms(retry_delay_ms[attempt]); + if (ret) + return ret; + } + + ret = gxfp_sensor_read_register(device, 0x0000, + value, sizeof(value)); + if (ret) + return ret; + + *chip_id = (uint16_t)value[1] | ((uint16_t)value[2] << 8); + + if (*chip_id != 0x0000u && + *chip_id != 0x8000u && + *chip_id != 0xffffu) + return 0; + } + + return -EAGAIN; +} + +int gxfp_sensor_read_otp(struct gxfp_device *device, + void *output, + size_t output_capacity, + size_t *output_len) +{ + static const uint8_t request_payload[2] = { 0x00, 0x00 }; + struct gxfp_response response; + int ret; + + if (!device || !output || output_capacity == 0 || !output_len) + return -EINVAL; + + *output_len = 0; + + ret = gxfp_request(device, + GXFP_CMD_READ_OTP, + request_payload, + sizeof(request_payload), + GXFP_CMD_READ_OTP, + 10, + 750, + &response); + if (ret) + return ret; + + if (response.payload_len > output_capacity) { + gxfp_response_release(&response); + return -EMSGSIZE; + } + + memcpy(output, response.payload, response.payload_len); + *output_len = response.payload_len; + + gxfp_response_release(&response); + return 0; +} + +int gxfp_sensor_check_chicagohu_otp_crc( + const void *otp_data, + size_t otp_len, + struct gxfp_otp_crc_info *info) +{ + const uint8_t *otp = otp_data; + uint8_t cp_data[15]; + uint8_t ft_data[19]; + uint8_t mt_data[27]; + size_t pos; + + if (!otp || !info) + return -EINVAL; + if (otp_len < GXFP_OTP_SIZE) + return -EBADMSG; + + memset(info, 0, sizeof(*info)); + + pos = 0; + memcpy(cp_data + pos, otp + 0x00, 11); + pos += 11; + memcpy(cp_data + pos, otp + 0x24, 4); + + pos = 0; + memcpy(ft_data + pos, otp + 0x0b, 9); + pos += 9; + ft_data[pos++] = otp[0x1c]; + memcpy(ft_data + pos, otp + 0x32, 4); + pos += 4; + memcpy(ft_data + pos, otp + 0x38, 4); + pos += 4; + ft_data[pos] = otp[0x3e]; + + pos = 0; + memcpy(mt_data + pos, otp + 0x14, 8); + pos += 8; + memcpy(mt_data + pos, otp + 0x1d, 7); + pos += 7; + memcpy(mt_data + pos, otp + 0x28, 10); + pos += 10; + memcpy(mt_data + pos, otp + 0x36, 2); + + info->cp_calculated = crc8_poly07_not(cp_data, sizeof(cp_data)); + info->ft_calculated = crc8_poly07_not(ft_data, sizeof(ft_data)); + info->mt_calculated = crc8_poly07_not(mt_data, sizeof(mt_data)); + + info->cp_stored = otp[GXFP_OTP_CP_CRC_OFFSET]; + info->ft_stored = otp[GXFP_OTP_FT_CRC_OFFSET]; + info->mt_stored = otp[GXFP_OTP_MT_CRC_OFFSET]; + + info->cp_valid = info->cp_calculated == info->cp_stored; + info->ft_valid = info->ft_calculated == info->ft_stored; + info->mt_valid = info->mt_calculated == info->mt_stored; + + return 0; +} + +int gxfp_sensor_parse_dac(const void *otp_data, + size_t otp_len, + int force_ft, + int force_mt, + struct gxfp_dac_info *info) +{ + const uint8_t *otp = otp_data; + unsigned int i; + unsigned int matches = 0; + int ret; + + if (!otp || !info) + return -EINVAL; + + if (otp_len < GXFP_OTP_FT_DAC_OFFSET + GXFP_OTP_DAC_COUNT) + return -EBADMSG; + + memset(info, 0, sizeof(*info)); + + ret = gxfp_sensor_check_chicagohu_otp_crc(otp, otp_len, &info->crc); + if (ret) + return ret; + + if (force_ft || info->crc.ft_valid) { + for (i = 0; i < GXFP_OTP_DAC_COUNT; i++) + info->raw[i] = otp[GXFP_OTP_FT_DAC_OFFSET + i]; + info->used_ft = 1; + goto finalize; + } + + if (force_mt || info->crc.mt_valid) { + for (i = 0; i < GXFP_OTP_DAC_COUNT; i++) + info->raw[i] = otp[GXFP_OTP_MT_DAC_OFFSET + i]; + info->used_mt = 1; + goto finalize; + } + + for (i = 0; i < GXFP_OTP_DAC_COUNT; i++) { + uint8_t mt = otp[GXFP_OTP_MT_DAC_OFFSET + i]; + uint8_t ft = otp[GXFP_OTP_FT_DAC_OFFSET + i]; + + if (mt == ft && ft != 0) + matches++; + } + + info->match_count = matches; + + if (matches < 3) + return -EBADMSG; + + if (matches == 4) { + for (i = 0; i < GXFP_OTP_DAC_COUNT; i++) + info->raw[i] = otp[GXFP_OTP_FT_DAC_OFFSET + i]; + info->used_ft = 1; + info->used_fallback = 1; + goto finalize; + } + + for (i = 0; i < GXFP_OTP_DAC_COUNT; i++) { + uint8_t mt = otp[GXFP_OTP_MT_DAC_OFFSET + i]; + uint8_t ft = otp[GXFP_OTP_FT_DAC_OFFSET + i]; + + if (mt == ft) { + info->raw[i] = ft; + } else { + unsigned int a = + otp[GXFP_OTP_MT_DAC_OFFSET + ((i + 1u) & 3u)]; + unsigned int b = + otp[GXFP_OTP_MT_DAC_OFFSET + ((i + 3u) & 3u)]; + unsigned int c = + otp[GXFP_OTP_MT_DAC_OFFSET + ((i + 2u) & 3u)]; + + info->raw[i] = (uint16_t)((a + b + c) / 3u); + info->used_fallback = 1; + } + } + +finalize: + if (info->match_count == 0) { + for (i = 0; i < GXFP_OTP_DAC_COUNT; i++) { + uint8_t mt = otp[GXFP_OTP_MT_DAC_OFFSET + i]; + uint8_t ft = otp[GXFP_OTP_FT_DAC_OFFSET + i]; + + if (mt == ft && ft != 0) + info->match_count++; + } + } + + info->register_value[0] = + (uint16_t)((info->raw[0] << 4) | 0x0008u); + info->register_value[1] = info->raw[1]; + info->register_value[2] = info->raw[2]; + info->register_value[3] = info->raw[3]; + + return 0; +} diff --git a/src/session.c b/src/session.c new file mode 100644 index 0000000..54e82f6 --- /dev/null +++ b/src/session.c @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/session.h" + +#include "gxfp/config.h" +#include "gxfp/device.h" +#include "gxfp/fdt.h" +#include "gxfp/protocol.h" +#include "gxfp/sensor.h" +#include "gxfp/tls.h" + +#include +#include +#include +#include +#include +#include + +#define GXFP_PROFILE_CHIP_ID 0x2504u +#define GXFP_CAPTURE_COMMAND 0x20u +#define GXFP_CAPTURE_DECLARED 7690u +#define GXFP_CAPTURE_FRAME_SIZE (3u + GXFP_CAPTURE_DECLARED) +#define GXFP_IMAGE_HEADER_SIZE 5u +#define GXFP_PACKED_SIZE 7680u +#define GXFP_IMAGE_CRC_SIZE 4u +#define GXFP_IMAGE_REGION_SIZE (GXFP_PACKED_SIZE + GXFP_IMAGE_CRC_SIZE) +#define GXFP_IMAGE_WIDTH 64u +#define GXFP_IMAGE_HEIGHT 80u +#define GXFP_IMAGE_SAMPLES (GXFP_IMAGE_WIDTH * GXFP_IMAGE_HEIGHT) +#define GXFP_TLS_READ_SLICE_MS 250 + +struct gxfp_session { + struct gxfp_device device; + struct gxfp_tls tls; + enum gxfp_session_state state; + atomic_bool cancel_requested; + int tls_ready; +}; + +static int64_t monotonic_ms(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return -1; + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static uint32_t crc32_mpeg2(const uint8_t *data, size_t length) +{ + uint32_t crc = UINT32_MAX; + size_t i; + + for (i = 0; i < length; i++) { + unsigned int bit; + crc ^= (uint32_t)data[i] << 24; + for (bit = 0; bit < 8; bit++) + crc = (crc & UINT32_C(0x80000000)) + ? (crc << 1) ^ UINT32_C(0x04c11db7) + : crc << 1; + } + return crc; +} + +static uint32_t get_goodix_crc(const uint8_t *p) +{ + return ((uint32_t)p[2] << 24) | ((uint32_t)p[3] << 16) | + ((uint32_t)p[0] << 8) | (uint32_t)p[1]; +} + +static void unpack_four(const uint8_t *p, uint16_t out[4]) +{ + out[0] = (uint16_t)(((p[0] & 0x0fu) << 8) | p[1]); + out[1] = (uint16_t)((p[3] << 4) | (p[0] >> 4)); + out[2] = (uint16_t)(((p[5] & 0x0fu) << 8) | p[2]); + out[3] = (uint16_t)((p[4] << 4) | (p[5] >> 4)); +} + +static int session_fail(struct gxfp_session *session, int error) +{ + session->state = GXFP_SESSION_ERROR; + return error; +} + +static int wait_fdt(struct gxfp_session *session, + enum gxfp_fdt_event wanted, int timeout_ms) +{ + int64_t deadline; + + if (timeout_ms <= 0) + return -EINVAL; + deadline = monotonic_ms(); + if (deadline < 0) + return -EIO; + deadline += timeout_ms; + + for (;;) { + struct gxfp_rx_record record = { 0 }; + struct gxfp_frame_view frame; + enum gxfp_fdt_event event; + int64_t now; + int slice; + int ret; + + if (atomic_load(&session->cancel_requested)) + return -ECANCELED; + now = monotonic_ms(); + if (now < 0) + return -EIO; + if (now >= deadline) + return -ETIMEDOUT; + slice = (int)(deadline - now); + if (slice > 100) + slice = 100; + ret = gxfp_device_receive(&session->device, &record, slice); + if (ret == -ETIMEDOUT || ret == -EAGAIN) + continue; + if (ret) + return ret; + if (record.mp_type != GXFP_MP_RX_COMMAND) { + gxfp_rx_record_release(&record); + continue; + } + ret = gxfp_frame_parse(record.payload, record.payload_len, &frame); + if (!ret) { + event = gxfp_fdt_decode(&frame, NULL); + gxfp_rx_record_release(&record); + if (event == wanted) + return 0; + } else { + gxfp_rx_record_release(&record); + } + } +} + +int gxfp_session_open(struct gxfp_session **out, const char *device_path) +{ + struct gxfp_session *session; + int ret; + + if (!out || !device_path) + return -EINVAL; + *out = NULL; + session = calloc(1, sizeof(*session)); + if (!session) + return -ENOMEM; + session->device.fd = -1; + ret = gxfp_device_open(&session->device, device_path); + if (ret) { + free(session); + return ret; + } + atomic_init(&session->cancel_requested, 0); + session->state = GXFP_SESSION_OPEN; + *out = session; + return 0; +} + +int gxfp_session_activate(struct gxfp_session *session, int timeout_ms) +{ + struct gxfp_dac_info dac; + struct gxfp_chicagohu_config_info info; + uint8_t otp[256]; + uint8_t config[GXFP_CONFIG_SIZE]; + size_t otp_length = 0; + uint16_t chip_id = 0; + int ret; + + if (!session || timeout_ms <= 0) + return -EINVAL; + if (session->state != GXFP_SESSION_OPEN) + return -EINVAL; + atomic_store(&session->cancel_requested, 0); + ret = gxfp_device_flush_rx(&session->device); + if (ret) + return session_fail(session, ret); + /* + * Recover from a previous process that exited while the MCU was + * armed for FDT or capture. Register reads are not reliable until + * that persistent sensor state has been reset. + */ + ret = gxfp_sensor_reset(&session->device); + if (ret) { + /* A stale MCU TLS/FDT state is sensor policy, so recover here. */ + ret = gxfp_sensor_recover(&session->device, 1); + if (ret) { + /* Recovery helpers are best-effort; the reset is authoritative. */ + ret = gxfp_sensor_reset(&session->device); + if (ret) + return session_fail(session, ret); + } + } + ret = gxfp_device_flush_rx(&session->device); + if (ret) + return session_fail(session, ret); + ret = gxfp_sensor_read_chip_id(&session->device, &chip_id); + if (ret) + return session_fail(session, ret); + if (chip_id != GXFP_PROFILE_CHIP_ID) + return session_fail(session, -ENODEV); + ret = gxfp_sensor_read_otp(&session->device, otp, sizeof(otp), + &otp_length); + if (ret) + return session_fail(session, ret); + ret = gxfp_sensor_parse_dac(otp, otp_length, 0, 0, &dac); + if (ret) + return session_fail(session, ret); + ret = gxfp_config_build_chicagohu_2504(config, otp, otp_length, + &dac, &info); + if (ret) + return session_fail(session, ret); + ret = gxfp_sensor_reset_and_download_config(&session->device, config, + sizeof(config)); + if (ret) + return session_fail(session, ret); + ret = gxfp_tls_init(&session->tls, &session->device); + if (ret) + return session_fail(session, ret); + session->tls_ready = 1; + ret = gxfp_sensor_start_tls(&session->device); + if (ret) + return session_fail(session, ret); + ret = gxfp_tls_handshake_timeout(&session->tls, timeout_ms); + if (ret) + return session_fail(session, ret); + session->state = GXFP_SESSION_ACTIVE; + return 0; +} + +int gxfp_session_arm_finger_down(struct gxfp_session *session) +{ + int ret; + if (!session || session->state != GXFP_SESSION_ACTIVE) + return -EINVAL; + atomic_store(&session->cancel_requested, 0); + ret = gxfp_fdt_arm_down(&session->device); + if (ret) + return session_fail(session, ret); + session->state = GXFP_SESSION_WAITING_FINGER_DOWN; + return 0; +} + +int gxfp_session_wait_finger_down(struct gxfp_session *session, int timeout_ms) +{ + int ret; + if (!session || session->state != GXFP_SESSION_WAITING_FINGER_DOWN) + return -EINVAL; + ret = wait_fdt(session, GXFP_FDT_EVENT_FINGER_DOWN, timeout_ms); + if (ret) + return session_fail(session, ret); + session->state = GXFP_SESSION_FINGER_PRESENT; + return 0; +} + +int gxfp_session_capture(struct gxfp_session *session, + struct gxfp_image12 *image, int timeout_ms) +{ + static const uint8_t capture_payload[2] = { 0x01, 0x00 }; + uint8_t request[16]; + uint8_t *stream = NULL; + size_t request_length = 0; + size_t stream_length = 0; + int64_t deadline; + int ret; + + if (!session || !image || timeout_ms <= 0) + return -EINVAL; + if (session->state != GXFP_SESSION_FINGER_PRESENT || image->pixels) + return -EINVAL; + atomic_store(&session->cancel_requested, 0); + session->state = GXFP_SESSION_CAPTURING; + ret = gxfp_frame_build(GXFP_CAPTURE_COMMAND, capture_payload, 2u, + request, sizeof(request), &request_length); + if (ret) + return session_fail(session, ret); + ret = gxfp_device_send_mp(&session->device, GXFP_MP_COMMAND, + request, request_length); + if (ret) + return session_fail(session, ret); + stream = malloc(GXFP_CAPTURE_FRAME_SIZE); + if (!stream) + return session_fail(session, -ENOMEM); + deadline = monotonic_ms(); + if (deadline < 0) { + ret = -EIO; + goto fail; + } + deadline += timeout_ms; + + while (stream_length < GXFP_CAPTURE_FRAME_SIZE) { + size_t chunk = 0; + int64_t now; + int remaining; + + if (atomic_load(&session->cancel_requested)) { + ret = -ECANCELED; + goto fail; + } + now = monotonic_ms(); + if (now < 0) { + ret = -EIO; + goto fail; + } + if (now >= deadline) { + ret = -ETIMEDOUT; + goto fail; + } + remaining = (int)(deadline - now); + if (remaining > GXFP_TLS_READ_SLICE_MS) + remaining = GXFP_TLS_READ_SLICE_MS; + ret = gxfp_tls_read_timeout(&session->tls, + stream + stream_length, + GXFP_CAPTURE_FRAME_SIZE - + stream_length, &chunk, remaining); + if (ret == -ETIMEDOUT || ret == -EAGAIN) + continue; + if (ret) + goto fail; + stream_length += chunk; + if (stream_length >= 3u) { + uint16_t declared = (uint16_t)stream[1] | + ((uint16_t)stream[2] << 8); + if (stream[0] != GXFP_CAPTURE_COMMAND || + declared != GXFP_CAPTURE_DECLARED) { + ret = -EPROTO; + goto fail; + } + } + } + + { + const uint8_t *payload = stream + 3u; + const uint8_t *packed = payload + GXFP_IMAGE_HEADER_SIZE; + const uint8_t *crc_bytes = packed + GXFP_PACKED_SIZE; + uint32_t calculated = crc32_mpeg2(packed, GXFP_PACKED_SIZE); + uint32_t stored = get_goodix_crc(crc_bytes); + uint16_t *pixels; + size_t offset; + size_t sample = 0; + + if (calculated != stored) { + ret = -EBADMSG; + goto fail; + } + pixels = calloc(GXFP_IMAGE_SAMPLES, sizeof(*pixels)); + if (!pixels) { + ret = -ENOMEM; + goto fail; + } + for (offset = 0; offset < GXFP_PACKED_SIZE; offset += 6u) { + uint16_t values[4]; + size_t i; + unpack_four(packed + offset, values); + for (i = 0; i < 4u; i++) + pixels[sample++] = values[i]; + } + if (sample != GXFP_IMAGE_SAMPLES) { + free(pixels); + ret = -EPROTO; + goto fail; + } + memset(image, 0, sizeof(*image)); + image->pixels = pixels; + image->width = GXFP_IMAGE_WIDTH; + image->height = GXFP_IMAGE_HEIGHT; + memcpy(image->header, payload, GXFP_IMAGE_HEADER_SIZE); + image->trailer = stream[GXFP_CAPTURE_FRAME_SIZE - 1u]; + image->crc_stored = stored; + image->crc_calculated = calculated; + } + free(stream); + session->state = GXFP_SESSION_FINGER_PRESENT; + return 0; + +fail: + free(stream); + return session_fail(session, ret); +} + +int gxfp_session_arm_finger_up(struct gxfp_session *session) +{ + int ret; + if (!session || session->state != GXFP_SESSION_FINGER_PRESENT) + return -EINVAL; + atomic_store(&session->cancel_requested, 0); + ret = gxfp_fdt_arm_up(&session->device); + if (ret) + return session_fail(session, ret); + session->state = GXFP_SESSION_WAITING_FINGER_UP; + return 0; +} + +int gxfp_session_wait_finger_up(struct gxfp_session *session, int timeout_ms) +{ + int ret; + if (!session || session->state != GXFP_SESSION_WAITING_FINGER_UP) + return -EINVAL; + ret = wait_fdt(session, GXFP_FDT_EVENT_FINGER_UP, timeout_ms); + if (ret) + return session_fail(session, ret); + session->state = GXFP_SESSION_ACTIVE; + return 0; +} + +int gxfp_session_deactivate(struct gxfp_session *session) +{ + int reset_ret = 0; + + if (!session) + return -EINVAL; + if (session->state == GXFP_SESSION_OPEN) + return 0; + atomic_store(&session->cancel_requested, 1); + /* FDT/capture mode survives closing /dev/gxfp; reset it explicitly. */ + reset_ret = gxfp_sensor_reset(&session->device); + if (session->tls_ready) { + gxfp_tls_close(&session->tls); + session->tls_ready = 0; + } + (void)gxfp_device_flush_rx(&session->device); + atomic_store(&session->cancel_requested, 0); + session->state = GXFP_SESSION_OPEN; + return reset_ret; +} + +int gxfp_image12_to_u8(const struct gxfp_image12 *source, + uint8_t *destination, size_t destination_size) +{ + size_t count; + size_t i; + uint16_t minimum = UINT16_MAX; + uint16_t maximum = 0; + + if (!source || !source->pixels || !destination) + return -EINVAL; + if (source->width && source->height > SIZE_MAX / source->width) + return -EOVERFLOW; + count = source->width * source->height; + if (!count || destination_size < count) + return -EMSGSIZE; + for (i = 0; i < count; i++) { + if (source->pixels[i] < minimum) + minimum = source->pixels[i]; + if (source->pixels[i] > maximum) + maximum = source->pixels[i]; + } + if (minimum == maximum) + return -EBADMSG; + for (i = 0; i < count; i++) + destination[i] = (uint8_t)(((uint32_t)(source->pixels[i] - minimum) + * 255u) / (maximum - minimum)); + return 0; +} + +void gxfp_image12_clear(struct gxfp_image12 *image) +{ + if (!image) + return; + free(image->pixels); + memset(image, 0, sizeof(*image)); +} + +void gxfp_session_cancel(struct gxfp_session *session) +{ + if (session) + atomic_store(&session->cancel_requested, 1); +} + +enum gxfp_session_state +gxfp_session_get_state(const struct gxfp_session *session) +{ + return session ? session->state : GXFP_SESSION_ERROR; +} + +void gxfp_session_close(struct gxfp_session *session) +{ + if (!session) + return; + (void)gxfp_session_deactivate(session); + gxfp_device_close(&session->device); + free(session); +} diff --git a/src/tls.c b/src/tls.c new file mode 100644 index 0000000..656c467 --- /dev/null +++ b/src/tls.c @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "gxfp/tls.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define GXFP_TLS_MP_FLAGS 0xb0u +#define GXFP_TLS_RX_MP_TYPE 0x0bu +#define GXFP_TLS_IO_TIMEOUT_MS 250 + +static int gxfp_debug_enabled(void) +{ + const char *value = getenv("GXFP_DEBUG"); + + return value && value[0] && strcmp(value, "0") != 0; +} + +static const unsigned char gxfp_tls_psk[32] = { + 0xB9, 0xE7, 0x19, 0x74, 0xBC, 0x72, 0xA0, 0xE9, + 0x0C, 0x53, 0x96, 0x59, 0x43, 0x60, 0x18, 0x9C, + 0xF9, 0xF5, 0x79, 0x33, 0x6F, 0xBB, 0x69, 0x00, + 0x89, 0x62, 0x79, 0xE5, 0x16, 0xF4, 0x2B, 0xAC +}; +static const unsigned char gxfp_tls_identity[] = "Client_identity"; + +static int gxfp_tls_mbed_error(int ret) +{ + if (ret >= 0) + return ret; + + if (ret == MBEDTLS_ERR_SSL_TIMEOUT) + return -ETIMEDOUT; + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) + return -EAGAIN; + + return -EPROTO; +} + +static void gxfp_tls_log_error(const char *operation, int ret) +{ + char message[160]; + + mbedtls_strerror(ret, message, sizeof(message)); + fprintf(stderr, "%s failed: -0x%04x (%s)\n", + operation, (unsigned int)-ret, message); +} + +static int gxfp_tls_send(void *context, const unsigned char *buffer, + size_t len) +{ + struct gxfp_tls *tls = context; + int ret; + + if (!tls || !tls->device || (!buffer && len != 0)) + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + + if (len > INT_MAX) + return MBEDTLS_ERR_SSL_BAD_INPUT_DATA; + + ret = gxfp_device_send_mp(tls->device, GXFP_TLS_MP_FLAGS, buffer, len); + if (ret == -EAGAIN || ret == -EINTR) + return MBEDTLS_ERR_SSL_WANT_WRITE; + if (ret) + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + + return (int)len; +} + +static int gxfp_tls_recv(void *context, unsigned char *buffer, size_t len) +{ + struct gxfp_tls *tls = context; + + if (!tls || !tls->device || !buffer || len == 0) + return MBEDTLS_ERR_SSL_BAD_INPUT_DATA; + + for (;;) { + size_t available; + size_t copy_len; + + available = tls->rx_length - tls->rx_offset; + if (available != 0) { + copy_len = available < len ? available : len; + memcpy(buffer, tls->rx_buffer + tls->rx_offset, copy_len); + tls->rx_offset += copy_len; + + if (tls->rx_offset == tls->rx_length) { + free(tls->rx_buffer); + tls->rx_buffer = NULL; + tls->rx_length = 0; + tls->rx_offset = 0; + } + + return (int)copy_len; + } + + for (;;) { + struct gxfp_rx_record record = { 0 }; + int ret; + + ret = gxfp_device_receive(tls->device, &record, + GXFP_TLS_IO_TIMEOUT_MS); + if (ret == -ETIMEDOUT || ret == -EAGAIN) + return MBEDTLS_ERR_SSL_WANT_READ; + if (ret == -EINTR) + continue; + if (ret) + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + + if (record.mp_type != GXFP_TLS_RX_MP_TYPE) { + if (gxfp_debug_enabled()) + fprintf(stderr, + "TLS: ignoring non-TLS MP frame " + "type=0x%02x len=%zu\n", + (unsigned int)record.mp_type, + record.payload_len); + gxfp_rx_record_release(&record); + continue; + } + + if (record.payload_len == 0) { + gxfp_rx_record_release(&record); + continue; + } + if (gxfp_debug_enabled()) { + size_t i; + + fprintf(stderr, "TLS RX MP=0x%02x len=%zu:\n", + (unsigned int)record.mp_type, + record.payload_len); + for (i = 0; i < record.payload_len; i++) + fprintf(stderr, "%02x%s", record.payload[i], + ((i + 1) % 16 == 0 || + i + 1 == record.payload_len) + ? "\n" : " "); + } + tls->rx_buffer = malloc(record.payload_len); + if (!tls->rx_buffer) { + gxfp_rx_record_release(&record); + return MBEDTLS_ERR_SSL_ALLOC_FAILED; + } + + memcpy(tls->rx_buffer, record.payload, record.payload_len); + tls->rx_length = record.payload_len; + tls->rx_offset = 0; + gxfp_rx_record_release(&record); + break; + } + } +} + +int gxfp_tls_init(struct gxfp_tls *tls, struct gxfp_device *device) +{ + static const int cipher_suites[] = { + MBEDTLS_TLS_PSK_WITH_AES_256_GCM_SHA384, + 0, + }; + static const unsigned char personalization[] = "gxfp-tls-server"; + int ret; + + if (!tls || !device || device->fd < 0) + return -EINVAL; + + memset(tls, 0, sizeof(*tls)); + tls->device = device; + + mbedtls_ssl_init(&tls->ssl); + mbedtls_ssl_config_init(&tls->config); + mbedtls_ctr_drbg_init(&tls->ctr_drbg); + mbedtls_entropy_init(&tls->entropy); + + ret = mbedtls_ctr_drbg_seed(&tls->ctr_drbg, + mbedtls_entropy_func, + &tls->entropy, + personalization, + sizeof(personalization) - 1); + if (ret) + goto fail; + + ret = mbedtls_ssl_config_defaults(&tls->config, + MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (ret) + goto fail; + + mbedtls_ssl_conf_rng(&tls->config, + mbedtls_ctr_drbg_random, + &tls->ctr_drbg); + mbedtls_ssl_conf_ciphersuites(&tls->config, cipher_suites); + mbedtls_ssl_conf_min_version(&tls->config, + MBEDTLS_SSL_MAJOR_VERSION_3, + MBEDTLS_SSL_MINOR_VERSION_3); + mbedtls_ssl_conf_max_version(&tls->config, + MBEDTLS_SSL_MAJOR_VERSION_3, + MBEDTLS_SSL_MINOR_VERSION_3); + + ret = mbedtls_ssl_conf_psk(&tls->config, + gxfp_tls_psk, + sizeof(gxfp_tls_psk), + gxfp_tls_identity, + sizeof(gxfp_tls_identity) - 1); + if (ret) + goto fail; + + ret = mbedtls_ssl_setup(&tls->ssl, &tls->config); + if (ret) + goto fail; + + mbedtls_ssl_set_bio(&tls->ssl, tls, + gxfp_tls_send, gxfp_tls_recv, NULL); + tls->initialized = 1; + return 0; + +fail: + gxfp_tls_log_error("TLS initialization", ret); + gxfp_tls_close(tls); + return gxfp_tls_mbed_error(ret); +} + +int gxfp_tls_handshake_timeout(struct gxfp_tls *tls, int timeout_ms) +{ + struct timespec ts; + int64_t deadline; + + if (!tls || !tls->initialized || timeout_ms <= 0) + return -EINVAL; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return -EIO; + deadline = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000 + + timeout_ms; + + for (;;) { + int ret = mbedtls_ssl_handshake(&tls->ssl); + if (ret == 0) + return 0; + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + int64_t now; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return -EIO; + now = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + if (now >= deadline) + return -ETIMEDOUT; + continue; + } + + gxfp_tls_log_error("TLS handshake", ret); + return gxfp_tls_mbed_error(ret); + } +} + +int gxfp_tls_handshake(struct gxfp_tls *tls) +{ + return gxfp_tls_handshake_timeout(tls, INT_MAX); +} + +int gxfp_tls_write_all(struct gxfp_tls *tls, const void *data, size_t len) +{ + const unsigned char *cursor = data; + size_t written = 0; + + if (!tls || !tls->initialized || (len != 0 && !data)) + return -EINVAL; + + while (written < len) { + int ret = mbedtls_ssl_write(&tls->ssl, + cursor + written, + len - written); + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) + continue; + if (ret < 0) { + gxfp_tls_log_error("TLS write", ret); + return gxfp_tls_mbed_error(ret); + } + if (ret == 0) + return -EIO; + + written += (size_t)ret; + } + + return 0; +} + +int gxfp_tls_read_timeout(struct gxfp_tls *tls, void *data, size_t capacity, + size_t *data_len, int timeout_ms) +{ + struct timespec ts; + int64_t deadline; + + if (!tls || !tls->initialized || !data || capacity == 0 || !data_len || + timeout_ms <= 0) + return -EINVAL; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return -EIO; + deadline = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000 + + timeout_ms; + + *data_len = 0; + + for (;;) { + int ret = mbedtls_ssl_read(&tls->ssl, data, capacity); + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + int64_t now; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return -EIO; + now = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + if (now >= deadline) + return -ETIMEDOUT; + continue; + } + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == 0) + return -ECONNRESET; + if (ret < 0) { + gxfp_tls_log_error("TLS read", ret); + return gxfp_tls_mbed_error(ret); + } + + *data_len = (size_t)ret; + return 0; + } +} + +int gxfp_tls_read(struct gxfp_tls *tls, void *data, size_t capacity, + size_t *data_len) +{ + return gxfp_tls_read_timeout(tls, data, capacity, data_len, INT_MAX); +} + +void gxfp_tls_close(struct gxfp_tls *tls) +{ + if (!tls) + return; + + if (tls->initialized) + (void)mbedtls_ssl_close_notify(&tls->ssl); + + free(tls->rx_buffer); + tls->rx_buffer = NULL; + mbedtls_ssl_free(&tls->ssl); + mbedtls_ssl_config_free(&tls->config); + mbedtls_ctr_drbg_free(&tls->ctr_drbg); + mbedtls_entropy_free(&tls->entropy); + memset(tls, 0, sizeof(*tls)); +} diff --git a/tools/analyze_capture.c b/tools/analyze_capture.c new file mode 100644 index 0000000..1cfb34c --- /dev/null +++ b/tools/analyze_capture.c @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include + +static uint16_t get_le16(const uint8_t *p) +{ + return (uint16_t)p[0] | ((uint16_t)p[1] << 8); +} + +static uint32_t get_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static uint32_t get_be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static uint32_t get_goodix_image_crc(const uint8_t *p) +{ + return ((uint32_t)p[2] << 24) | ((uint32_t)p[3] << 16) | + ((uint32_t)p[0] << 8) | (uint32_t)p[1]; +} + +static uint32_t crc32_mpeg2(const uint8_t *data, size_t length) +{ + uint32_t crc = UINT32_MAX; + size_t i; + + for (i = 0; i < length; i++) { + unsigned int bit; + + crc ^= (uint32_t)data[i] << 24; + for (bit = 0; bit < 8; bit++) + crc = (crc & UINT32_C(0x80000000)) + ? (crc << 1) ^ UINT32_C(0x04c11db7) + : crc << 1; + } + return crc; +} + +static void analyze_crc_candidate(const char *name, const uint8_t *data, + size_t size, size_t offset, size_t span) +{ + const uint8_t *stored; + uint32_t calculated; + uint32_t be; + uint32_t le; + uint32_t goodix; + + printf("CRC candidate %s: offset=%zu length=%zu", name, offset, span); + if (span < 4u || offset > size || span > size - offset) { + printf(" OUT_OF_RANGE\n"); + return; + } + + stored = data + offset + span - 4u; + calculated = crc32_mpeg2(data + offset, span - 4u); + be = get_be32(stored); + le = get_le32(stored); + goodix = get_goodix_image_crc(stored); + + printf(" data=%zu raw=%02x%02x%02x%02x calc=%08x", + span - 4u, stored[0], stored[1], stored[2], stored[3], + calculated); + if (calculated == be) + printf(" MATCH_BE"); + if (calculated == le) + printf(" MATCH_LE"); + if (calculated == goodix) + printf(" MATCH_GOODIX_2301"); + if (calculated != be && calculated != le && calculated != goodix) + printf(" NO_MATCH"); + putchar('\n'); +} + +static void dump_edge(const char *label, const uint8_t *data, size_t length) +{ + size_t i; + + printf("%s (%zu bytes):", label, length); + for (i = 0; i < length; i++) { + if ((i % 16) == 0) + printf("\n "); + printf("%02x%c", data[i], (i % 16) == 15 ? '\n' : ' '); + } + if ((length % 16) != 0) + putchar('\n'); +} + +static int read_file(const char *path, uint8_t **out, size_t *out_length) +{ + FILE *file; + long end; + uint8_t *data; + + file = fopen(path, "rb"); + if (!file) + return -errno; + if (fseek(file, 0, SEEK_END) != 0 || (end = ftell(file)) < 0 || + fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return -EIO; + } + data = malloc(end > 0 ? (size_t)end : 1u); + if (!data) { + fclose(file); + return -ENOMEM; + } + if (end > 0 && fread(data, 1, (size_t)end, file) != (size_t)end) { + free(data); + fclose(file); + return -EIO; + } + if (fclose(file) != 0) { + free(data); + return -EIO; + } + *out = data; + *out_length = (size_t)end; + return 0; +} + +int main(int argc, char **argv) +{ + const char *path = argc > 1 ? argv[1] : "capture_plaintext.bin"; + uint8_t *data = NULL; + size_t size = 0; + uint16_t declared; + size_t expected_total; + size_t edge; + int ret; + + ret = read_file(path, &data, &size); + if (ret) { + fprintf(stderr, "%s: %s\n", path, strerror(-ret)); + return EXIT_FAILURE; + } + if (size < 3) { + fprintf(stderr, "%s: file is shorter than the 3-byte header\n", path); + free(data); + return EXIT_FAILURE; + } + + declared = get_le16(data + 1); + expected_total = 3u + (size_t)declared; + printf("file: %s\n", path); + printf("command: 0x%02x\n", data[0]); + printf("declared_length: %u (0x%04x)\n", declared, declared); + printf("file_size: %zu\n", size); + printf("expected_total: %zu\n", expected_total); + printf("length_match: %s\n", size == expected_total ? "yes" : "no"); + + edge = size < 32u ? size : 32u; + dump_edge("first", data, edge); + dump_edge("last", data + size - edge, edge); + + printf("controlled CRC scope candidates:\n"); + analyze_crc_candidate("A", data, size, 3u, 7685u); + analyze_crc_candidate("B", data, size, 8u, 7680u); + analyze_crc_candidate("C", data, size, 3u, 7686u); + analyze_crc_candidate("D", data, size, 8u, 7684u); + + if (declared >= 6u && expected_total <= size) { + const uint8_t *frame_payload = data + 3u; + size_t frame_payload_length = declared; + const uint8_t *image_data = frame_payload + 5u; + size_t image_data_length = frame_payload_length - 6u; + const uint8_t *frame_trailer = frame_payload + + frame_payload_length - 1u; + uint8_t sum = 0; + size_t i; + + printf("frame_payload_offset: 3\n"); + printf("frame_payload_length: %zu\n", frame_payload_length); + printf("image_data_offset: 8\n"); + printf("image_data_length (DLL length - 6): %zu\n", + image_data_length); + printf("frame_trailer: 0x%02x\n", *frame_trailer); + + for (i = 0; i < expected_total - 1u; i++) + sum = (uint8_t)(sum + data[i]); + printf("checksum_candidate target_AA: 0x%02x\n", + (uint8_t)(UINT8_C(0xaa) - sum)); + printf("checksum_candidate target_00: 0x%02x\n", + (uint8_t)(0u - sum)); + printf("sum_with_trailer_mod256: 0x%02x\n", + (uint8_t)(sum + *frame_trailer)); + + if (image_data_length >= 4u) { + const uint8_t *crc_bytes = image_data + + image_data_length - 4u; + uint32_t calculated = crc32_mpeg2( + image_data, image_data_length - 4u); + + printf("image_bytes_before_crc: %zu\n", + image_data_length - 4u); + printf("image_crc_raw: %02x %02x %02x %02x\n", + crc_bytes[0], crc_bytes[1], + crc_bytes[2], crc_bytes[3]); + printf("crc32_mpeg2_calculated: 0x%08x\n", calculated); + printf("crc_candidate_be: 0x%08x%s\n", + get_be32(crc_bytes), + calculated == get_be32(crc_bytes) ? " MATCH" : ""); + printf("crc_candidate_le: 0x%08x%s\n", + get_le32(crc_bytes), + calculated == get_le32(crc_bytes) ? " MATCH" : ""); + printf("crc_candidate_goodix_2301: 0x%08x%s\n", + get_goodix_image_crc(crc_bytes), + calculated == get_goodix_image_crc(crc_bytes) + ? " MATCH" : ""); + } + } else { + printf("frame analysis skipped: declared length is incomplete\n"); + } + + free(data); + return EXIT_SUCCESS; +} diff --git a/tools/capture_to_pgm.c b/tools/capture_to_pgm.c new file mode 100644 index 0000000..5d1969b --- /dev/null +++ b/tools/capture_to_pgm.c @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Experimental offline decoder only. + * The 12-bit unpack and transpose candidates are adapted from Metrohan's + * gxfp5130-linux decoder. They are not part of the capture/library path and + * are not yet verified for this 0x2504 ChicagoHU device. + */ +#include +#include +#include +#include +#include + +#define IMAGE_OFFSET 8u +#define PACKED_LENGTH 7680u +#define CRC_LENGTH 4u +#define SAMPLE_COUNT 5120u +#define RAW_WIDTH 64u +#define RAW_HEIGHT 80u +#define TRANSPOSED_WIDTH 80u +#define TRANSPOSED_HEIGHT 64u +#define SAMPLE_MAX 4095u + +static uint16_t get_le16(const uint8_t *p) +{ + return (uint16_t)p[0] | ((uint16_t)p[1] << 8); +} + +static uint32_t crc32_mpeg2(const uint8_t *data, size_t length) +{ + uint32_t crc = UINT32_MAX; + size_t i; + + for (i = 0; i < length; i++) { + unsigned int bit; + + crc ^= (uint32_t)data[i] << 24; + for (bit = 0; bit < 8; bit++) + crc = (crc & UINT32_C(0x80000000)) + ? (crc << 1) ^ UINT32_C(0x04c11db7) + : crc << 1; + } + return crc; +} + +static uint32_t get_goodix_crc(const uint8_t *p) +{ + return ((uint32_t)p[2] << 24) | ((uint32_t)p[3] << 16) | + ((uint32_t)p[0] << 8) | (uint32_t)p[1]; +} + +static int read_file(const char *path, uint8_t **out, size_t *out_length) +{ + FILE *file = fopen(path, "rb"); + long end; + uint8_t *data; + + if (!file) + return -errno; + if (fseek(file, 0, SEEK_END) != 0 || (end = ftell(file)) < 0 || + fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return -EIO; + } + data = malloc(end > 0 ? (size_t)end : 1u); + if (!data) { + fclose(file); + return -ENOMEM; + } + if (end > 0 && fread(data, 1, (size_t)end, file) != (size_t)end) { + free(data); + fclose(file); + return -EIO; + } + if (fclose(file) != 0) { + free(data); + return -EIO; + } + *out = data; + *out_length = (size_t)end; + return 0; +} + +static void unpack_four(const uint8_t *p, uint16_t out[4]) +{ + out[0] = (uint16_t)(((p[0] & 0x0fu) << 8) | p[1]); + out[1] = (uint16_t)((p[3] << 4) | (p[0] >> 4)); + out[2] = (uint16_t)(((p[5] & 0x0fu) << 8) | p[2]); + out[3] = (uint16_t)((p[4] << 4) | (p[5] >> 4)); +} + +static int write_pgm(const char *path, const uint16_t *pixels, + size_t width, size_t height, int inverted) +{ + FILE *file = fopen(path, "wb"); + size_t i; + + if (!file) + return -errno; + if (fprintf(file, "P5\n%zu %zu\n%u\n", + width, height, SAMPLE_MAX) < 0) { + fclose(file); + return -EIO; + } + for (i = 0; i < width * height; i++) { + uint16_t value = inverted + ? (uint16_t)(SAMPLE_MAX - pixels[i]) + : pixels[i]; + uint8_t encoded[2] = { + (uint8_t)(value >> 8), (uint8_t)value + }; + + if (fwrite(encoded, 1, sizeof(encoded), file) != + sizeof(encoded)) { + fclose(file); + return -EIO; + } + } + return fclose(file) == 0 ? 0 : -EIO; +} + +int main(int argc, char **argv) +{ + const char *path = argc > 1 ? argv[1] : "capture_plaintext.bin"; + uint8_t *frame = NULL; + size_t frame_length = 0; + uint16_t *raw = NULL; + uint16_t *transposed = NULL; + uint16_t declared; + const uint8_t *packed; + const uint8_t *crc_bytes; + uint32_t calculated_crc; + uint32_t stored_crc; + size_t offset; + size_t sample = 0; + int ret; + + ret = read_file(path, &frame, &frame_length); + if (ret) { + fprintf(stderr, "%s: %s\n", path, strerror(-ret)); + return EXIT_FAILURE; + } + if (frame_length < 3u) { + fprintf(stderr, "input is shorter than the frame header\n"); + ret = -EBADMSG; + goto out; + } + declared = get_le16(frame + 1); + if (frame[0] != 0x20u || frame_length != 3u + declared || + frame_length < IMAGE_OFFSET + PACKED_LENGTH + CRC_LENGTH + 1u) { + fprintf(stderr, + "unexpected frame: cmd=0x%02x declared=%u size=%zu\n", + frame[0], declared, frame_length); + ret = -EBADMSG; + goto out; + } + + packed = frame + IMAGE_OFFSET; + crc_bytes = packed + PACKED_LENGTH; + calculated_crc = crc32_mpeg2(packed, PACKED_LENGTH); + stored_crc = get_goodix_crc(crc_bytes); + if (calculated_crc != stored_crc) { + fprintf(stderr, + "image CRC mismatch: calculated=%08x stored=%08x\n", + calculated_crc, stored_crc); + ret = -EBADMSG; + goto out; + } + + raw = calloc(SAMPLE_COUNT, sizeof(*raw)); + transposed = calloc(SAMPLE_COUNT, sizeof(*transposed)); + if (!raw || !transposed) { + ret = -ENOMEM; + goto out; + } + + for (offset = 0; offset < PACKED_LENGTH; offset += 6u) { + uint16_t values[4]; + size_t i; + + unpack_four(packed + offset, values); + for (i = 0; i < 4u; i++, sample++) { + size_t destination; + + raw[sample] = values[i]; + destination = (sample % RAW_WIDTH) * + TRANSPOSED_WIDTH + + sample / RAW_WIDTH; + transposed[destination] = values[i]; + } + } + + ret = write_pgm("candidate_raw.pgm", raw, + RAW_WIDTH, RAW_HEIGHT, 0); + if (!ret) + ret = write_pgm("candidate_transposed.pgm", transposed, + TRANSPOSED_WIDTH, TRANSPOSED_HEIGHT, 0); + if (!ret) + ret = write_pgm("candidate_inverted.pgm", raw, + RAW_WIDTH, RAW_HEIGHT, 1); + if (!ret) + ret = write_pgm("candidate_transposed_inverted.pgm", + transposed, TRANSPOSED_WIDTH, + TRANSPOSED_HEIGHT, 1); + if (ret) { + fprintf(stderr, "PGM write failed: %s\n", strerror(-ret)); + goto out; + } + + printf("Input CRC valid: 0x%08x\n", calculated_crc); + printf("Experimental outputs written:\n"); + printf(" candidate_raw.pgm (64x80)\n"); + printf(" candidate_transposed.pgm (80x64)\n"); + printf(" candidate_inverted.pgm (64x80)\n"); + printf(" candidate_transposed_inverted.pgm (80x64)\n"); + +out: + free(transposed); + free(raw); + free(frame); + return ret ? EXIT_FAILURE : EXIT_SUCCESS; +}