diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..58a8dcc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://www.paypal.com/donate?hosted_button_id=BWUECKFDNY446 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..55ed5aa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: Continuous Integration +on: [push, pull_request] +jobs: + build: + # Newer images have CONFIG_SND_PCM disabled + runs-on: ubuntu-16.04 + steps: + - name: Install prerequisites + run: sudo apt update && sudo apt install dkms + - name: Checkout + uses: actions/checkout@v2 + - name: Install (debug) + run: sudo ./install.sh + - name: Uninstall (debug) + run: sudo ./uninstall.sh + - name: Install (release) + run: sudo ./install.sh --release + - name: Uninstall (release) + run: sudo ./uninstall.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfb511b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +internal/* +.vscode/* diff --git a/Kbuild b/Kbuild new file mode 100644 index 0000000..03133b5 --- /dev/null +++ b/Kbuild @@ -0,0 +1,7 @@ +xone-wired-y := transport/wired.o +xone-gip-bus-y := bus/bus.o bus/protocol.o +xone-gip-common-y := driver/common.o +xone-gip-gamepad-y := driver/gamepad.o +xone-gip-headset-y := driver/headset.o +xone-gip-chatpad-y := driver/chatpad.o +obj-m := xone-wired.o xone-gip-bus.o xone-gip-common.o xone-gip-gamepad.o xone-gip-headset.o xone-gip-chatpad.o diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + 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 Lesser 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +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 Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e519d41 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# xone [![Build Badge](https://img.shields.io/github/workflow/status/medusalix/xone/Continuous%20Integration?logo=github)](https://github.com/medusalix/xone/actions) [![Release Badge](https://img.shields.io/github/v/release/medusalix/xone?logo=github)](https://github.com/medusalix/xone/releases/latest) [![Discord Badge](https://img.shields.io/discord/733964971842732042?label=discord&logo=discord)](https://discord.gg/FDQxwWk) [![Donate Button](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/donate?hosted_button_id=BWUECKFDNY446) + +xone is a Linux kernel driver for Xbox One and Xbox Series X|S accessories. It serves as a modern replacement for `xpad`, aiming to be compatible with Microsoft's *Game Input Protocol* (GIP). +If you are looking for a way to use your controller via Bluetooth, check out [xpadneo](https://github.com/atar-axis/xpadneo). + +## Important notes + +This driver is still in active development. Use at your own risk! +**Any feedback including bug reports, suggestions or ideas is [*greatly appreciated*](https://discord.gg/FDQxwWk).** + +## Features + +- [x] Input and force feedback (rumble) +- [x] Battery reporting (`UPower` integration) +- [x] LED brightness control (using `/sys/class/leds`) +- [x] Audio capture/playback (through `ALSA`) +- [x] Power management (suspend/resume) +- [ ] Wireless connectivity (via dongle) + +## Supported devices + +- [x] Gamepads + - [x] Xbox One Controllers + - [x] Xbox Series X|S Controllers + - [x] Third party controllers (PowerA, PDP, etc.) +- [ ] Headsets + - [x] Xbox One Chat Headset + - [x] Xbox One Stereo Headset (adapter or jack) + - [ ] Xbox Wireless Headset + - [ ] Third party wireless headsets (SteelSeries, Razer, etc.) +- [ ] Racing wheels +- [x] Xbox One Chatpad +- [x] Xbox Adaptive Controller +- [ ] Mad Catz Rock Band 4 Wireless Stratocaster +- [ ] Mad Catz Rock Band 4 Wireless Drum Kit + +## Releases + +[![Packaging status](https://repology.org/badge/vertical-allrepos/xone.svg)](https://repology.org/project/xone/versions) + +Feel free to package xone for any Linux distribution or hardware you like. +Any issues regarding the packaging should be reported to the respective maintainers. + +## Installation + +### Prerequisites + +- Linux (kernel 4.15+ and headers) +- DKMS + +Clone the repository: + +``` +git clone https://github.com/medusalix/xone +``` + +Install xone using the following command: + +``` +sudo ./install.sh --release +``` + +**NOTE:** Please omit the `--release` flag when asked for your debug logs. + +### Updating + +Make sure to completely uninstall xone before updating: + +``` +sudo ./uninstall.sh +``` + +## License + +xone is released under the [GNU General Public License, Version 2](LICENSE). + +``` +Copyright (C) 2021 Severin von Wnuck + +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. +``` diff --git a/bus/bus.c b/bus/bus.c new file mode 100644 index 0000000..4a0c590 --- /dev/null +++ b/bus/bus.c @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include +#include + +#include "bus.h" + +#define to_gip_adapter(d) container_of(d, struct gip_adapter, dev) +#define to_gip_client(d) container_of(d, struct gip_client, dev) +#define to_gip_driver(d) container_of(d, struct gip_driver, drv) + +static DEFINE_IDA(gip_adapter_ida); + +static void gip_adapter_release(struct device *dev) +{ + kfree(to_gip_adapter(dev)); +} + +static struct device_type gip_adapter_type = { + .release = gip_adapter_release, +}; + +static void gip_add_client(struct gip_client *client) +{ + int err; + + err = device_add(&client->dev); + if (err) { + dev_err(&client->dev, "%s: add device failed: %d\n", + __func__, err); + return; + } + + dev_dbg(&client->dev, "%s: added\n", __func__); +} + +static void gip_remove_client(struct gip_client *client) +{ + dev_dbg(&client->dev, "%s: removed\n", __func__); + + if (device_is_registered(&client->dev)) + device_del(&client->dev); + + put_device(&client->dev); +} + +static void gip_client_state_changed(struct work_struct *work) +{ + struct gip_client *client = container_of(work, typeof(*client), + state_work); + + switch (atomic_read(&client->state)) { + case GIP_CL_IDENTIFIED: + gip_add_client(client); + break; + case GIP_CL_DISCONNECTED: + gip_remove_client(client); + break; + default: + dev_warn(&client->dev, "%s: invalid state\n", __func__); + break; + } +} + +static int gip_client_uevent(struct device *dev, struct kobj_uevent_env *env) +{ + struct gip_client *client = to_gip_client(dev); + + if (!client->classes || !client->classes->count) + return -EINVAL; + + return add_uevent_var(env, "MODALIAS=gip:%s", + client->classes->strings[0]); +} + +static void gip_client_release(struct device *dev) +{ + struct gip_client *client = to_gip_client(dev); + + gip_free_client_info(client); + kfree(client->chunk_buf); + kfree(client); +} + +static struct device_type gip_client_type = { + .uevent = gip_client_uevent, + .release = gip_client_release, +}; + +static int gip_bus_match(struct device *dev, struct device_driver *driver) +{ + struct gip_client *client; + struct gip_driver *drv; + int i; + + if (dev->type != &gip_client_type) + return false; + + client = to_gip_client(dev); + drv = to_gip_driver(driver); + + for (i = 0; i < client->classes->count; i++) + if (!strcmp(client->classes->strings[i], drv->class)) + return true; + + return false; +} + +static int gip_bus_probe(struct device *dev) +{ + struct gip_client *client = to_gip_client(dev); + struct gip_driver *drv = to_gip_driver(dev->driver); + int err; + unsigned long flags; + + if (client->drv) + return 0; + + err = drv->probe(client); + if (!err) { + spin_lock_irqsave(&client->lock, flags); + client->drv = drv; + spin_unlock_irqrestore(&client->lock, flags); + } + + return err; +} + +static int gip_bus_remove(struct device *dev) +{ + struct gip_client *client = to_gip_client(dev); + struct gip_driver *drv = client->drv; + unsigned long flags; + + if (!drv) + return 0; + + spin_lock_irqsave(&client->lock, flags); + client->drv = NULL; + spin_unlock_irqrestore(&client->lock, flags); + + drv->remove(client); + + return 0; +} + +static struct bus_type gip_bus_type = { + .name = "xone-gip", + .match = gip_bus_match, + .probe = gip_bus_probe, + .remove = gip_bus_remove, +}; + +struct gip_adapter *gip_create_adapter(struct device *parent, + struct gip_adapter_ops *ops, int audio_pkts) +{ + struct gip_adapter *adap; + int err; + + adap = kzalloc(sizeof(*adap), GFP_KERNEL); + if (!adap) + return ERR_PTR(-ENOMEM); + + adap->id = ida_simple_get(&gip_adapter_ida, 0, 0, GFP_KERNEL); + if (adap->id < 0) { + err = adap->id; + goto err_put_device; + } + + adap->state_queue = alloc_ordered_workqueue("gip%d", 0, adap->id); + if (!adap->state_queue) { + err = -ENOMEM; + goto err_remove_ida; + } + + adap->dev.parent = parent; + adap->dev.type = &gip_adapter_type; + adap->dev.bus = &gip_bus_type; + adap->ops = ops; + adap->audio_packet_count = audio_pkts; + dev_set_name(&adap->dev, "gip%d", adap->id); + spin_lock_init(&adap->clients_lock); + spin_lock_init(&adap->send_lock); + + err = device_register(&adap->dev); + if (err) + goto err_destroy_queue; + + dev_dbg(&adap->dev, "%s: registered\n", __func__); + + return adap; + +err_destroy_queue: + destroy_workqueue(adap->state_queue); +err_remove_ida: + ida_simple_remove(&gip_adapter_ida, adap->id); +err_put_device: + put_device(&adap->dev); + + return ERR_PTR(err); +} +EXPORT_SYMBOL_GPL(gip_create_adapter); + +void gip_remove_all_clients(struct gip_adapter *adap) +{ + int i; + + for (i = 0; i < GIP_MAX_CLIENTS; i++) { + if (adap->clients[i]) { + gip_remove_client(adap->clients[i]); + adap->clients[i] = NULL; + } + } +} + +int gip_suspend_adapter(struct gip_adapter *adap) +{ + struct gip_client *client = adap->clients[0]; + int err = 0; + + /* ensure all state changes have been processed */ + flush_workqueue(adap->state_queue); + + /* suspend main client */ + if (client && client->drv && client->drv->suspend) + err = client->drv->suspend(client); + + gip_remove_all_clients(adap); + + return err; +} +EXPORT_SYMBOL_GPL(gip_suspend_adapter); + +void gip_destroy_adapter(struct gip_adapter *adap) +{ + /* ensure all state changes have been processed */ + flush_workqueue(adap->state_queue); + gip_remove_all_clients(adap); + ida_simple_remove(&gip_adapter_ida, adap->id); + destroy_workqueue(adap->state_queue); + + dev_dbg(&adap->dev, "%s: unregistered\n", __func__); + device_unregister(&adap->dev); +} +EXPORT_SYMBOL_GPL(gip_destroy_adapter); + +static struct gip_client *gip_init_client(struct gip_adapter *adap, u8 id) +{ + struct gip_client *client; + + client = kzalloc(sizeof(*client), GFP_ATOMIC); + if (!client) + return ERR_PTR(-ENOMEM); + + client->dev.parent = &adap->dev; + client->dev.type = &gip_client_type; + client->dev.bus = &gip_bus_type; + client->id = id; + client->adapter = adap; + dev_set_name(&client->dev, "gip%d.%u", adap->id, client->id); + atomic_set(&client->state, GIP_CL_CONNECTED); + spin_lock_init(&client->lock); + INIT_WORK(&client->state_work, gip_client_state_changed); + + device_initialize(&client->dev); + dev_dbg(&client->dev, "%s: initialized\n", __func__); + + return client; +} + +struct gip_client *gip_get_or_init_client(struct gip_adapter *adap, u8 id) +{ + struct gip_client *client; + unsigned long flags; + + spin_lock_irqsave(&adap->clients_lock, flags); + + client = adap->clients[id]; + if (!client) { + client = gip_init_client(adap, id); + if (IS_ERR(client)) + goto err_unlock; + + adap->clients[id] = client; + } + + get_device(&client->dev); + +err_unlock: + spin_unlock_irqrestore(&adap->clients_lock, flags); + + return client; +} + +void gip_put_client(struct gip_client *client) +{ + put_device(&client->dev); +} + +void gip_register_client(struct gip_client *client) +{ + atomic_set(&client->state, GIP_CL_IDENTIFIED); + queue_work(client->adapter->state_queue, &client->state_work); +} + +void gip_unregister_client(struct gip_client *client) +{ + struct gip_adapter *adap = client->adapter; + unsigned long flags; + + spin_lock_irqsave(&adap->clients_lock, flags); + adap->clients[client->id] = NULL; + spin_unlock_irqrestore(&adap->clients_lock, flags); + + atomic_set(&client->state, GIP_CL_DISCONNECTED); + queue_work(adap->state_queue, &client->state_work); +} + +void gip_free_client_info(struct gip_client *client) +{ + int i; + + kfree(client->audio_formats); + kfree(client->capabilities_out); + kfree(client->capabilities_in); + + if (client->classes) + for (i = 0; i < client->classes->count; i++) + kfree(client->classes->strings[i]); + + kfree(client->classes); + kfree(client->interfaces); + kfree(client->hid_descriptor); + + client->audio_formats = NULL; + client->capabilities_out = NULL; + client->capabilities_in = NULL; + client->classes = NULL; + client->interfaces = NULL; + client->hid_descriptor = NULL; +} + +int __gip_register_driver(struct gip_driver *drv, struct module *owner, + const char *mod_name) +{ + drv->drv.name = drv->name; + drv->drv.bus = &gip_bus_type; + drv->drv.owner = owner; + drv->drv.mod_name = mod_name; + + return driver_register(&drv->drv); +} +EXPORT_SYMBOL_GPL(__gip_register_driver); + +void gip_unregister_driver(struct gip_driver *drv) +{ + driver_unregister(&drv->drv); +} +EXPORT_SYMBOL_GPL(gip_unregister_driver); + +static int __init gip_bus_init(void) +{ + return bus_register(&gip_bus_type); +} + +static void __exit gip_bus_exit(void) +{ + bus_unregister(&gip_bus_type); +} + +module_init(gip_bus_init); +module_exit(gip_bus_exit); + +MODULE_AUTHOR("Severin von Wnuck "); +MODULE_DESCRIPTION("xone GIP bus driver"); +MODULE_LICENSE("GPL"); diff --git a/bus/bus.h b/bus/bus.h new file mode 100644 index 0000000..4b1b8e6 --- /dev/null +++ b/bus/bus.h @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#pragma once + +#include + +#include "protocol.h" + +#define GIP_MAX_CLIENTS 16 + +#define gip_register_driver(drv) \ + __gip_register_driver(drv, THIS_MODULE, KBUILD_MODNAME) + +#define module_gip_driver(drv) \ + module_driver(drv, gip_register_driver, gip_unregister_driver) + +struct gip_adapter_buffer { + enum gip_adapter_buffer_type { + GIP_BUF_DATA, + GIP_BUF_AUDIO, + } type; + + void *context; + void *data; + int length; +}; + +struct gip_adapter_ops { + int (*get_buffer)(struct gip_adapter *adap, + struct gip_adapter_buffer *buf); + int (*submit_buffer)(struct gip_adapter *adap, + struct gip_adapter_buffer *buf); + int (*enable_audio)(struct gip_adapter *adap); + int (*init_audio_in)(struct gip_adapter *adap); + int (*init_audio_out)(struct gip_adapter *adap, int pkt_len); + int (*disable_audio)(struct gip_adapter *adap); +}; + +struct gip_adapter { + struct device dev; + int id; + + struct gip_adapter_ops *ops; + int audio_packet_count; + + struct gip_client *clients[GIP_MAX_CLIENTS]; + struct workqueue_struct *state_queue; + spinlock_t clients_lock; + spinlock_t send_lock; + + u8 data_sequence; + u8 audio_sequence; +}; + +struct gip_client { + struct device dev; + u8 id; + atomic_t state; + + struct gip_adapter *adapter; + struct gip_driver *drv; + + struct gip_chunk_buffer *chunk_buf; + struct gip_hardware hardware; + + struct gip_info_element *audio_formats; + struct gip_info_element *capabilities_in; + struct gip_info_element *capabilities_out; + struct gip_classes *classes; + struct gip_info_element *interfaces; + struct gip_info_element *hid_descriptor; + + struct gip_audio_config audio_config_in; + struct gip_audio_config audio_config_out; + + spinlock_t lock; + struct work_struct state_work; +}; + +struct gip_driver_ops { + int (*battery)(struct gip_client *client, enum gip_battery_type type, + enum gip_battery_level level); + int (*guide_button)(struct gip_client *client, bool pressed); + int (*audio_ready)(struct gip_client *client); + int (*audio_volume)(struct gip_client *client, int in, int out); + int (*hid_report)(struct gip_client *client, void *data, int len); + int (*input)(struct gip_client *client, void *data, int len); + int (*audio_samples)(struct gip_client *client, void *data, int len); +}; + +struct gip_driver { + struct device_driver drv; + const char *name; + const char *class; + + struct gip_driver_ops ops; + + int (*probe)(struct gip_client *client); + void (*remove)(struct gip_client *client); + int (*suspend)(struct gip_client *client); +}; + +struct gip_adapter *gip_create_adapter(struct device *parent, + struct gip_adapter_ops *ops, int audio_pkts); +int gip_suspend_adapter(struct gip_adapter *adap); +void gip_destroy_adapter(struct gip_adapter *adap); + +struct gip_client *gip_get_or_init_client(struct gip_adapter *adap, u8 id); +void gip_put_client(struct gip_client *client); +void gip_register_client(struct gip_client *client); +void gip_unregister_client(struct gip_client *client); +void gip_free_client_info(struct gip_client *client); + +int __gip_register_driver(struct gip_driver *drv, struct module *owner, + const char *mod_name); +void gip_unregister_driver(struct gip_driver *drv); diff --git a/bus/protocol.c b/bus/protocol.c new file mode 100644 index 0000000..ecca05c --- /dev/null +++ b/bus/protocol.c @@ -0,0 +1,1254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include +#include + +#include "bus.h" + +/* product ID for the chat headset */ +#define GIP_PID_CHAT_HEADSET 0x0111 + +#define GIP_HEADER_CLIENT_ID GENMASK(3, 0) +#define GIP_HEADER_LENGTH GENMASK(6, 0) +#define GIP_HEADER_EXTENDED BIT(7) + +#define GIP_BATT_LEVEL GENMASK(1, 0) +#define GIP_BATT_TYPE GENMASK(3, 2) +#define GIP_STATUS_CONNECTED BIT(7) + +#define GIP_AUD_LENGTH_EXTRA GENMASK(3, 0) + +enum gip_option { + GIP_OPT_ACKNOWLEDGE = BIT(4), + GIP_OPT_REQUEST = BIT(5), + GIP_OPT_CHUNK_START = BIT(6), + GIP_OPT_CHUNK = BIT(7), +}; + +enum gip_audio_control { + GIP_AUD_CTRL_VOLUME_CHAT = 0x00, + GIP_AUD_CTRL_FORMAT_CHAT = 0x01, + GIP_AUD_CTRL_FORMAT = 0x02, + GIP_AUD_CTRL_VOLUME = 0x03, +}; + +enum gip_audio_volume_mute { + GIP_AUD_VOLUME_UNMUTED = 0x04, + GIP_AUD_VOLUME_MIC_MUTED = 0x05, +}; + +struct gip_header { + u8 command; + u8 options; + u8 sequence; + u8 length; +} __packed; + +struct gip_chunk_header { + u8 offset_extra; + u8 offset; +} __packed; + +struct gip_pkt_acknowledge { + u8 unknown; + struct gip_header inner; + u8 padding[2]; + __le16 remaining; +} __packed; + +struct gip_pkt_announce { + u8 address[6]; + __le16 unknown; + __le16 vendor_id; + __le16 product_id; + struct gip_version { + __le16 major; + __le16 minor; + __le16 build; + __le16 revision; + } __packed fw_version, hw_version; +} __packed; + +struct gip_pkt_status { + u8 status; + u8 unknown[3]; +} __packed; + +struct gip_pkt_identify { + u8 unknown[16]; + __le16 unknown_offset1; + __le16 unknown_offset2; + __le16 audio_formats_offset; + __le16 capabilities_out_offset; + __le16 capabilities_in_offset; + __le16 classes_offset; + __le16 interfaces_offset; + __le16 hid_descriptor_offset; +} __packed; + +struct gip_pkt_power { + u8 mode; +} __packed; + +struct gip_pkt_authenticate { + u8 unknown1; + u8 unknown2; +} __packed; + +struct gip_pkt_guide_button { + u8 pressed; + u8 unknown; +} __packed; + +struct gip_pkt_audio_control { + u8 subcommand; +} __packed; + +struct gip_pkt_audio_volume_chat { + struct gip_pkt_audio_control control; + u8 mute; + u8 gain_out; + u8 out; + u8 in; +} __packed; + +struct gip_pkt_audio_format_chat { + struct gip_pkt_audio_control control; + u8 in_out; +} __packed; + +struct gip_pkt_audio_format { + struct gip_pkt_audio_control control; + u8 in; + u8 out; +} __packed; + +struct gip_pkt_audio_volume { + struct gip_pkt_audio_control control; + u8 mute; + u8 out; + u8 unknown1; + u8 in; + u8 unknown2; + u8 unknown3[2]; +} __packed; + +struct gip_pkt_led { + u8 unknown; + u8 mode; + u8 brightness; +} __packed; + +struct gip_pkt_serial_number { + u8 unknown[2]; + char serial[14]; +} __packed; + +struct gip_pkt_audio_header { + u8 length_extra; + u8 unknown; +} __packed; + +struct gip_pkt_audio_header_ext { + u8 unknown[2]; +} __packed; + +struct gip_chunk { + int offset; + void *data; + int length; +}; + +static int gip_send_pkt(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_adapter *adap = client->adapter; + struct gip_adapter_buffer buf = {}; + int err; + unsigned long flags; + + buf.type = GIP_BUF_DATA; + + spin_lock_irqsave(&adap->send_lock, flags); + + /* sequence number is always greater than zero */ + while (!header->sequence) + header->sequence = adap->data_sequence++; + + err = adap->ops->get_buffer(adap, &buf); + if (err) { + dev_err(&client->dev, "%s: get buffer failed: %d\n", + __func__, err); + goto err_unlock; + } + + /* check available space */ + if (buf.length < sizeof(*header) + len) { + dev_err(&client->dev, "%s: buffer too small\n", __func__); + err = -ENOSPC; + goto err_unlock; + } + + memcpy(buf.data, header, sizeof(*header)); + if (data) + memcpy(buf.data + sizeof(*header), data, len); + + /* set actual length */ + buf.length = sizeof(*header) + len; + + /* always fails on adapter removal */ + err = adap->ops->submit_buffer(adap, &buf); + if (err) + dev_dbg(&client->dev, "%s: submit buffer failed: %d\n", + __func__, err); + +err_unlock: + spin_unlock_irqrestore(&adap->send_lock, flags); + + return err; +} + +static int gip_acknowledge_pkt(struct gip_client *client, + struct gip_header *ack, u16 len, u16 remaining) +{ + struct gip_header header = {}; + struct gip_pkt_acknowledge pkt = {}; + + header.command = GIP_CMD_ACKNOWLEDGE; + header.options = client->id | GIP_OPT_REQUEST; + header.sequence = ack->sequence; + header.length = sizeof(pkt); + + pkt.inner.command = ack->command; + pkt.inner.options = client->id | GIP_OPT_REQUEST; + pkt.inner.sequence = len; + pkt.inner.length = len >> 8; + + /* only required for the start chunk */ + pkt.remaining = cpu_to_le16(remaining); + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} + +static int gip_request_identification(struct gip_client *client) +{ + struct gip_header header = {}; + + header.command = GIP_CMD_IDENTIFY; + header.options = client->id | GIP_OPT_REQUEST; + + return gip_send_pkt(client, &header, NULL, 0); +} + +int gip_set_power_mode(struct gip_client *client, enum gip_power_mode mode) +{ + struct gip_header header = {}; + struct gip_pkt_power pkt = {}; + + header.command = GIP_CMD_POWER; + header.options = client->id | GIP_OPT_REQUEST; + header.length = sizeof(pkt); + + pkt.mode = mode; + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} +EXPORT_SYMBOL_GPL(gip_set_power_mode); + +int gip_complete_authentication(struct gip_client *client) +{ + struct gip_header header = {}; + struct gip_pkt_authenticate pkt = {}; + + header.command = GIP_CMD_AUTHENTICATE; + header.options = client->id | GIP_OPT_REQUEST; + header.length = sizeof(pkt); + + pkt.unknown1 = 0x01; + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} +EXPORT_SYMBOL_GPL(gip_complete_authentication); + +static int gip_set_audio_format_chat(struct gip_client *client, + enum gip_audio_format_chat in_out) +{ + struct gip_header header = {}; + struct gip_pkt_audio_format_chat pkt = {}; + + header.command = GIP_CMD_AUDIO_CONTROL; + header.options = client->id | GIP_OPT_REQUEST; + header.length = sizeof(pkt); + + pkt.control.subcommand = GIP_AUD_CTRL_FORMAT_CHAT; + pkt.in_out = in_out; + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} + +static int gip_set_audio_format(struct gip_client *client, + enum gip_audio_format in, enum gip_audio_format out) +{ + struct gip_header header = {}; + struct gip_pkt_audio_format pkt = {}; + + header.command = GIP_CMD_AUDIO_CONTROL; + header.options = client->id | GIP_OPT_REQUEST; + header.length = sizeof(pkt); + + pkt.control.subcommand = GIP_AUD_CTRL_FORMAT; + pkt.in = in; + pkt.out = out; + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} + +int gip_suggest_audio_format(struct gip_client *client, + enum gip_audio_format in, enum gip_audio_format out) +{ + int err; + + /* special handling for the chat headset */ + if (client->hardware.product == GIP_PID_CHAT_HEADSET) + err = gip_set_audio_format_chat(client, + GIP_AUD_FORMAT_CHAT_24KHZ); + else + err = gip_set_audio_format(client, in, out); + + if (err) { + dev_err(&client->dev, "%s: set format failed: %d\n", + __func__, err); + return err; + } + + client->audio_config_in.format = in; + client->audio_config_out.format = out; + + return 0; +} +EXPORT_SYMBOL_GPL(gip_suggest_audio_format); + +static int gip_set_audio_volume(struct gip_client *client, u8 in, u8 out) +{ + struct gip_header header = {}; + struct gip_pkt_audio_volume pkt = {}; + + header.command = GIP_CMD_AUDIO_CONTROL; + header.options = client->id | GIP_OPT_REQUEST; + header.length = sizeof(pkt); + + pkt.control.subcommand = GIP_AUD_CTRL_VOLUME; + pkt.mute = GIP_AUD_VOLUME_UNMUTED; + pkt.out = out; + pkt.in = in; + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} + +int gip_fix_audio_volume(struct gip_client *client) +{ + /* chat headsets have buttons to adjust the hardware volume */ + if (client->hardware.product == GIP_PID_CHAT_HEADSET) + return 0; + + /* set hardware volume to maximum */ + return gip_set_audio_volume(client, 100, 100); +} +EXPORT_SYMBOL_GPL(gip_fix_audio_volume); + +int gip_send_rumble(struct gip_client *client, void *pkt, u8 len) +{ + struct gip_header header = {}; + + header.command = GIP_CMD_RUMBLE; + header.options = client->id; + header.length = len; + + return gip_send_pkt(client, &header, pkt, len); +} +EXPORT_SYMBOL_GPL(gip_send_rumble); + +int gip_set_led_mode(struct gip_client *client, + enum gip_led_mode mode, u8 brightness) +{ + struct gip_header header = {}; + struct gip_pkt_led pkt = {}; + + header.command = GIP_CMD_LED; + header.options = client->id | GIP_OPT_REQUEST; + header.length = sizeof(pkt); + + pkt.mode = mode; + pkt.brightness = brightness; + + return gip_send_pkt(client, &header, &pkt, sizeof(pkt)); +} +EXPORT_SYMBOL_GPL(gip_set_led_mode); + +static void gip_copy_audio_samples(struct gip_client *client, + void *samples, void *buf) +{ + struct gip_audio_config *cfg = &client->audio_config_out; + struct gip_header header = {}; + struct gip_pkt_audio_header pkt = {}; + void *src, *dest; + int i; + + /* packet length does not include audio header size */ + header.command = GIP_CMD_AUDIO_SAMPLES; + header.options = client->id | GIP_OPT_REQUEST; + header.length = cfg->fragment_size; + + if (cfg->fragment_size > GIP_HEADER_LENGTH) { + header.length |= GIP_HEADER_EXTENDED; + pkt.length_extra = GIP_HEADER_EXTENDED | + (cfg->fragment_size >> 7); + } + + for (i = 0; i < client->adapter->audio_packet_count; i++) { + src = samples + i * cfg->fragment_size; + dest = buf + i * cfg->packet_size; + + /* sequence number is always greater than zero */ + do { + header.sequence = client->adapter->audio_sequence++; + } while (!header.sequence); + + memcpy(dest, &header, sizeof(header)); + + if (cfg->fragment_size > GIP_HEADER_LENGTH) { + memcpy(dest + sizeof(header), &pkt, sizeof(pkt)); + memcpy(dest + sizeof(header) + sizeof(pkt), + src, cfg->fragment_size); + } else { + memcpy(dest + sizeof(header), src, cfg->fragment_size); + } + } +} + +int gip_send_audio_samples(struct gip_client *client, void *samples) +{ + struct gip_adapter *adap = client->adapter; + struct gip_adapter_buffer buf = {}; + int err; + + buf.type = GIP_BUF_AUDIO; + + err = adap->ops->get_buffer(adap, &buf); + if (err) { + dev_err(&client->dev, "%s: get buffer failed: %d\n", + __func__, err); + return err; + } + + gip_copy_audio_samples(client, samples, buf.data); + + /* always fails on adapter removal */ + err = adap->ops->submit_buffer(adap, &buf); + if (err) + dev_dbg(&client->dev, "%s: submit buffer failed: %d\n", + __func__, err); + + return err; +} +EXPORT_SYMBOL_GPL(gip_send_audio_samples); + +int gip_enable_audio(struct gip_client *client) +{ + struct gip_adapter *adap = client->adapter; + int err; + + err = adap->ops->enable_audio(adap); + if (err) + dev_err(&client->dev, "%s: enable failed: %d\n", + __func__, err); + + return err; +} +EXPORT_SYMBOL_GPL(gip_enable_audio); + +int gip_init_audio_in(struct gip_client *client) +{ + struct gip_adapter *adap = client->adapter; + int err; + + err = adap->ops->init_audio_in(adap); + if (err) + dev_err(&client->dev, "%s: init failed: %d\n", + __func__, err); + + return err; +} +EXPORT_SYMBOL_GPL(gip_init_audio_in); + +int gip_init_audio_out(struct gip_client *client) +{ + struct gip_adapter *adap = client->adapter; + int err; + + err = adap->ops->init_audio_out(adap, + client->audio_config_out.packet_size); + if (err) + dev_err(&client->dev, "%s: init failed: %d\n", + __func__, err); + + return err; +} +EXPORT_SYMBOL_GPL(gip_init_audio_out); + +void gip_disable_audio(struct gip_client *client) +{ + struct gip_adapter *adap = client->adapter; + int err; + + /* always fails on adapter removal */ + err = adap->ops->disable_audio(adap); + if (err) + dev_dbg(&client->dev, "%s: disable failed: %d\n", + __func__, err); +} +EXPORT_SYMBOL_GPL(gip_disable_audio); + +static int gip_make_audio_config(struct gip_client *client, + struct gip_audio_config *cfg) +{ + switch (cfg->format) { + case GIP_AUD_FORMAT_24KHZ_MONO: + cfg->channels = 1; + cfg->sample_rate = 24000; + break; + case GIP_AUD_FORMAT_48KHZ_STEREO: + cfg->channels = 2; + cfg->sample_rate = 48000; + break; + default: + dev_err(&client->dev, "%s: unknown format: 0x%02x\n", + __func__, cfg->format); + return -ENOTSUPP; + } + + cfg->valid = true; + cfg->buffer_size = cfg->sample_rate * cfg->channels * + sizeof(s16) * GIP_AUDIO_INTERVAL / MSEC_PER_SEC; + cfg->fragment_size = cfg->buffer_size / client->adapter->audio_packet_count; + cfg->packet_size = cfg->fragment_size + sizeof(struct gip_header); + + if (cfg->fragment_size > GIP_HEADER_LENGTH) + cfg->packet_size += sizeof(struct gip_pkt_audio_header); + + dev_dbg(&client->dev, "%s: rate=%d/%d, buffer=%d\n", __func__, + cfg->sample_rate, cfg->channels, cfg->buffer_size); + + return 0; +} + +static struct gip_info_element *gip_parse_info_element(u8 *data, int len, + __le16 offset, int item_length) +{ + struct gip_info_element *elem; + u16 off = le16_to_cpu(offset); + u8 count; + int total; + + if (!off) + return ERR_PTR(-ENOTSUPP); + + if (len < off + sizeof(count)) + return ERR_PTR(-EINVAL); + + count = data[off]; + if (!count) + return ERR_PTR(-ENOTSUPP); + + total = count * item_length; + if (len < off + sizeof(count) + total) + return ERR_PTR(-EINVAL); + + elem = kzalloc(sizeof(*elem) + total, GFP_ATOMIC); + if (!elem) + return ERR_PTR(-ENOMEM); + + elem->length = total; + memcpy(elem->data, data + off + sizeof(count), total); + + return elem; +} + +static int gip_parse_audio_formats(struct gip_client *client, + struct gip_pkt_identify *pkt, u8 *data, int len) +{ + struct gip_info_element *fmts; + + fmts = gip_parse_info_element(data, len, + pkt->audio_formats_offset, sizeof(u8) * 2); + if (IS_ERR(fmts)) { + if (PTR_ERR(fmts) == -ENOTSUPP) + return 0; + + dev_err(&client->dev, "%s: parse failed: %ld\n", + __func__, PTR_ERR(fmts)); + return PTR_ERR(fmts); + } + + dev_dbg(&client->dev, "%s: formats=%*phD\n", __func__, + fmts->length, fmts->data); + client->audio_formats = fmts; + + return 0; +} + +static int gip_parse_capabilities(struct gip_client *client, + struct gip_pkt_identify *pkt, u8 *data, int len) +{ + struct gip_info_element *caps; + + caps = gip_parse_info_element(data, len, + pkt->capabilities_out_offset, sizeof(u8)); + if (IS_ERR(caps)) { + dev_err(&client->dev, "%s: parse out failed: %ld\n", + __func__, PTR_ERR(caps)); + return PTR_ERR(caps); + } + + dev_dbg(&client->dev, "%s: out=%*phD\n", __func__, + caps->length, caps->data); + client->capabilities_out = caps; + + caps = gip_parse_info_element(data, len, + pkt->capabilities_in_offset, sizeof(u8)); + if (IS_ERR(caps)) { + dev_err(&client->dev, "%s: parse in failed: %ld\n", + __func__, PTR_ERR(caps)); + return PTR_ERR(caps); + } + + dev_dbg(&client->dev, "%s: in=%*phD\n", __func__, + caps->length, caps->data); + client->capabilities_in = caps; + + return 0; +} + +static int gip_parse_classes(struct gip_client *client, + struct gip_pkt_identify *pkt, u8 *data, int len) +{ + struct gip_classes *classes; + u16 off = le16_to_cpu(pkt->classes_offset); + u8 count; + u16 str_len; + char *str; + + if (len < off + sizeof(count)) + return -EINVAL; + + /* number of individual strings */ + count = data[off++]; + if (!count) + return -EINVAL; + + classes = kzalloc(sizeof(*classes) + sizeof(char *) * count, GFP_ATOMIC); + if (!classes) + return -ENOMEM; + + client->classes = classes; + + while (classes->count < count) { + if (len < off + sizeof(str_len)) + return -EINVAL; + + str_len = le16_to_cpup((u16 *)(data + off)); + if (!str_len || len < off + sizeof(str_len) + str_len) + return -EINVAL; + + /* null-terminated string */ + str = kzalloc(str_len + 1, GFP_ATOMIC); + if (!str) + return -ENOMEM; + + memcpy(str, data + off + sizeof(str_len), str_len); + classes->strings[classes->count] = str; + classes->count++; + off += str_len; + + dev_dbg(&client->dev, "%s: class=%s\n", __func__, str); + } + + return 0; +} + +static int gip_parse_interfaces(struct gip_client *client, + struct gip_pkt_identify *pkt, u8 *data, int len) +{ + struct gip_info_element *intfs; + int i; + + intfs = gip_parse_info_element(data, len, + pkt->interfaces_offset, UUID_SIZE); + if (IS_ERR(intfs)) { + dev_err(&client->dev, "%s: parse failed: %ld\n", + __func__, PTR_ERR(intfs)); + return PTR_ERR(intfs); + } + + for (i = 0; i < intfs->length; i += UUID_SIZE) + dev_dbg(&client->dev, "%s: guid=%pU\n", __func__, + intfs->data + i); + + client->interfaces = intfs; + + return 0; +} + +static int gip_parse_hid_descriptor(struct gip_client *client, + struct gip_pkt_identify *pkt, u8 *data, int len) +{ + struct gip_info_element *desc; + + desc = gip_parse_info_element(data, len, + pkt->hid_descriptor_offset, sizeof(u8)); + if (IS_ERR(desc)) { + if (PTR_ERR(desc) == -ENOTSUPP) + return 0; + + dev_err(&client->dev, "%s: parse failed: %ld\n", + __func__, PTR_ERR(desc)); + return PTR_ERR(desc); + } + + dev_dbg(&client->dev, "%s: length=0x%02x\n", __func__, desc->length); + client->hid_descriptor = desc; + + return 0; +} + +static int gip_handle_pkt_announce(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_announce *pkt = data; + struct gip_hardware *hw = &client->hardware; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + if (atomic_read(&client->state) != GIP_CL_CONNECTED) { + dev_warn(&client->dev, "%s: invalid state\n", __func__); + return 0; + } + + hw->vendor = le16_to_cpu(pkt->vendor_id); + hw->product = le16_to_cpu(pkt->product_id); + hw->version = (le16_to_cpu(pkt->fw_version.major) << 8) | + le16_to_cpu(pkt->fw_version.minor); + + dev_dbg(&client->dev, "%s: address=%pM, vendor=0x%04x, product=0x%04x\n", + __func__, pkt->address, hw->vendor, hw->product); + dev_dbg(&client->dev, "%s: firmware=%u.%u.%u.%u, hardware=%u.%u.%u.%u\n", + __func__, + le16_to_cpu(pkt->fw_version.major), + le16_to_cpu(pkt->fw_version.minor), + le16_to_cpu(pkt->fw_version.build), + le16_to_cpu(pkt->fw_version.revision), + le16_to_cpu(pkt->hw_version.major), + le16_to_cpu(pkt->hw_version.minor), + le16_to_cpu(pkt->hw_version.build), + le16_to_cpu(pkt->hw_version.revision)); + + atomic_set(&client->state, GIP_CL_ANNOUNCED); + + return gip_request_identification(client); +} + +static int gip_handle_pkt_status(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_status *pkt = data; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + if (!(pkt->status & GIP_STATUS_CONNECTED)) { + /* schedule client removal */ + dev_dbg(&client->dev, "%s: disconnected\n", __func__); + gip_unregister_client(client); + return 0; + } + + if (!client->drv || !client->drv->ops.battery) + return 0; + + return client->drv->ops.battery(client, + FIELD_GET(GIP_BATT_TYPE, pkt->status), + FIELD_GET(GIP_BATT_LEVEL, pkt->status)); +} + +static int gip_handle_pkt_identify(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_identify *pkt = data; + int err; + + if (len < sizeof(*pkt)) + return -EINVAL; + + if (atomic_read(&client->state) != GIP_CL_ANNOUNCED) { + dev_warn(&client->dev, "%s: invalid state\n", __func__); + return 0; + } + + /* skip unknown header */ + data += sizeof(pkt->unknown); + len -= sizeof(pkt->unknown); + + err = gip_parse_audio_formats(client, pkt, data, len); + if (err) + goto err_free_info; + + err = gip_parse_capabilities(client, pkt, data, len); + if (err) + goto err_free_info; + + err = gip_parse_classes(client, pkt, data, len); + if (err) + goto err_free_info; + + err = gip_parse_interfaces(client, pkt, data, len); + if (err) + goto err_free_info; + + err = gip_parse_hid_descriptor(client, pkt, data, len); + if (err) + goto err_free_info; + + /* schedule client registration */ + gip_register_client(client); + + return 0; + +err_free_info: + gip_free_client_info(client); + + return err; +} + +static int gip_handle_pkt_guide_button(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_guide_button *pkt = data; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + if (!client->drv || !client->drv->ops.guide_button) + return 0; + + return client->drv->ops.guide_button(client, pkt->pressed); +} + +static int gip_handle_pkt_audio_format_chat(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_audio_format_chat *pkt = data; + struct gip_audio_config *in = &client->audio_config_in; + struct gip_audio_config *out = &client->audio_config_out; + int err; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + /* chat headsets apparently default to 24 kHz */ + if (pkt->in_out != GIP_AUD_FORMAT_CHAT_24KHZ || in->valid || out->valid) + return -EPROTO; + + err = gip_make_audio_config(client, in); + if (err) + return err; + + err = gip_make_audio_config(client, out); + if (err) + return err; + + if (!client->drv || !client->drv->ops.audio_ready) + return 0; + + return client->drv->ops.audio_ready(client); +} + +static int gip_handle_pkt_audio_volume_chat(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_audio_volume_chat *pkt = data; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + if (!client->drv || !client->drv->ops.audio_volume) + return 0; + + return client->drv->ops.audio_volume(client, pkt->in, pkt->out); +} + +static int gip_handle_pkt_audio_format(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_audio_format *pkt = data; + struct gip_audio_config *in = &client->audio_config_in; + struct gip_audio_config *out = &client->audio_config_out; + int err; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + /* format has already been accepted */ + if (in->valid || out->valid) + return -EPROTO; + + /* client rejected format, accept new format */ + if (pkt->in != in->format || pkt->out != out->format) { + dev_warn(&client->dev, "%s: rejected: 0x%02x/0x%02x\n", + __func__, in->format, out->format); + return gip_suggest_audio_format(client, pkt->in, pkt->out); + } + + err = gip_make_audio_config(client, in); + if (err) + return err; + + err = gip_make_audio_config(client, out); + if (err) + return err; + + if (!client->drv || !client->drv->ops.audio_ready) + return 0; + + return client->drv->ops.audio_ready(client); +} + +static int gip_handle_pkt_audio_volume(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_audio_volume *pkt = data; + + if (len != header->length || len != sizeof(*pkt)) + return -EINVAL; + + if (!client->drv || !client->drv->ops.audio_volume) + return 0; + + return client->drv->ops.audio_volume(client, pkt->in, pkt->out); +} + +static int gip_handle_pkt_audio_control(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_audio_control *pkt = data; + + if (len < sizeof(*pkt)) + return -EINVAL; + + switch (pkt->subcommand) { + case GIP_AUD_CTRL_FORMAT_CHAT: + return gip_handle_pkt_audio_format_chat(client, header, data, len); + case GIP_AUD_CTRL_VOLUME_CHAT: + return gip_handle_pkt_audio_volume_chat(client, header, data, len); + case GIP_AUD_CTRL_FORMAT: + return gip_handle_pkt_audio_format(client, header, data, len); + case GIP_AUD_CTRL_VOLUME: + return gip_handle_pkt_audio_volume(client, header, data, len); + } + + dev_err(&client->dev, "%s: unknown subcommand: 0x%02x\n", + __func__, pkt->subcommand); + + return -EPROTO; +} + +static int gip_handle_pkt_hid_report(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + if (len != header->length) + return -EINVAL; + + if (!client->drv || !client->drv->ops.hid_report) + return 0; + + return client->drv->ops.hid_report(client, data, len); +} + +static int gip_handle_pkt_input(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + if (len != header->length) + return -EINVAL; + + if (!client->drv || !client->drv->ops.input) + return 0; + + return client->drv->ops.input(client, data, len); +} + +static int gip_handle_pkt_audio_samples(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + struct gip_pkt_audio_header *pkt = data; + struct gip_pkt_audio_header_ext *ext = data + sizeof(*pkt); + int total = header->length & GIP_HEADER_LENGTH; + + if (len < sizeof(*pkt)) + return -EINVAL; + + /* extended audio headers are used by wireless clients */ + if (header->options & GIP_HEADER_EXTENDED) { + total |= (pkt->length_extra & GIP_AUD_LENGTH_EXTRA) << 7; + if (total < sizeof(*pkt) + sizeof(*ext) || len < total) + return -EINVAL; + + data += sizeof(*pkt) + sizeof(*ext); + total -= sizeof(*pkt) + sizeof(*ext); + } else { + if (total < sizeof(*pkt) || len < total) + return -EINVAL; + + data += sizeof(*pkt); + total -= sizeof(*pkt); + } + + if (!client->drv || !client->drv->ops.audio_samples) + return 0; + + return client->drv->ops.audio_samples(client, data, total); +} + +static int gip_handle_pkt(struct gip_client *client, + struct gip_header *header, void *data, int len) +{ + switch (header->command) { + case GIP_CMD_ACKNOWLEDGE: + /* ignore acknowledgements */ + return 0; + case GIP_CMD_ANNOUNCE: + return gip_handle_pkt_announce(client, header, data, len); + case GIP_CMD_STATUS: + return gip_handle_pkt_status(client, header, data, len); + case GIP_CMD_IDENTIFY: + return gip_handle_pkt_identify(client, header, data, len); + case GIP_CMD_GUIDE_BUTTON: + return gip_handle_pkt_guide_button(client, header, data, len); + case GIP_CMD_AUDIO_CONTROL: + return gip_handle_pkt_audio_control(client, header, data, len); + case GIP_CMD_HID_REPORT: + return gip_handle_pkt_hid_report(client, header, data, len); + case GIP_CMD_INPUT: + return gip_handle_pkt_input(client, header, data, len); + case GIP_CMD_AUDIO_SAMPLES: + return gip_handle_pkt_audio_samples(client, header, data, len); + } + + dev_warn(&client->dev, "%s: unknown command: 0x%02x\n", + __func__, header->command); + + return 0; +} + +static int gip_parse_chunk(struct gip_client *client, + void *data, int len, struct gip_chunk *chunk) +{ + struct gip_header *header = data; + struct gip_chunk_header *chunk_header = data + sizeof(*header); + + if (len < sizeof(*header) + sizeof(*chunk_header)) { + dev_err(&client->dev, "%s: invalid length\n", __func__); + return -EINVAL; + } + + if (header->length & GIP_HEADER_EXTENDED) + chunk->offset = chunk_header->offset; + else + chunk->offset = (chunk_header->offset_extra & 0x7f) | + (chunk_header->offset << 7); + + chunk->data = data + sizeof(*header) + sizeof(*chunk_header); + chunk->length = len - sizeof(*header) - sizeof(*chunk_header); + + if (chunk->length != (header->length & GIP_HEADER_LENGTH)) { + dev_err(&client->dev, "%s: length mismatch\n", __func__); + return -EINVAL; + } + + dev_dbg(&client->dev, "%s: offset=0x%02x, length=0x%02x\n", + __func__, chunk->offset, chunk->length); + + return 0; +} + +static int gip_init_chunk_buffer(struct gip_client *client, int len) +{ + struct gip_chunk_buffer *buf = client->chunk_buf; + + if (buf) { + dev_err(&client->dev, "%s: already initialized\n", __func__); + kfree(buf); + } + + buf = kzalloc(sizeof(*buf) + len, GFP_ATOMIC); + if (!buf) + return -ENOMEM; + + dev_dbg(&client->dev, "%s: length=0x%02x\n", __func__, len); + buf->length = len; + client->chunk_buf = buf; + + return 0; +} + +static int gip_copy_chunk_data(struct gip_client *client, + struct gip_chunk chunk) +{ + struct gip_chunk_buffer *buf = client->chunk_buf; + + if (!buf) { + dev_err(&client->dev, "%s: buffer not allocated\n", __func__); + return -EPROTO; + } + + if (buf->full) { + dev_err(&client->dev, "%s: buffer full\n", __func__); + return -ENOMEM; + } + + if (chunk.offset + chunk.length > buf->length) { + dev_err(&client->dev, "%s: buffer too small\n", __func__); + return -EINVAL; + } + + /* last chunk is empty, offset is total length of all chunks */ + if (!chunk.length && chunk.offset == buf->length) { + dev_dbg(&client->dev, "%s: buffer complete\n", __func__); + buf->full = true; + } else { + memcpy(buf->data + chunk.offset, chunk.data, chunk.length); + } + + return 0; +} + +static int gip_process_chunk(struct gip_client *client, void *data, int len) +{ + struct gip_header *header = data; + struct gip_chunk chunk = {}; + int err; + + err = gip_parse_chunk(client, data, len, &chunk); + if (err) + return err; + + if (header->options & GIP_OPT_CHUNK_START) { + /* offset is total length of all chunks */ + err = gip_init_chunk_buffer(client, chunk.offset); + if (err) + return err; + + /* acknowledge with remaining length */ + err = gip_acknowledge_pkt(client, header, chunk.length, + client->chunk_buf->length - chunk.length); + if (err) + return err; + + chunk.offset = 0; + } else if (header->options & GIP_OPT_ACKNOWLEDGE) { + /* acknowledge with total buffer length */ + err = gip_acknowledge_pkt(client, header, + client->chunk_buf->length, 0); + if (err) + return err; + } + + return gip_copy_chunk_data(client, chunk); +} + +static int gip_process_pkt_coherent(struct gip_client *client, + void *data, int len) +{ + struct gip_header *header = data; + int err; + + if (header->options & GIP_OPT_ACKNOWLEDGE) { + err = gip_acknowledge_pkt(client, header, header->length, 0); + if (err) + return err; + } + + return gip_handle_pkt(client, header, data + sizeof(*header), + len - sizeof(*header)); +} + +static int gip_process_pkt_chunked(struct gip_client *client, + void *data, int len) +{ + struct gip_header *header = data; + struct gip_chunk_buffer *buf; + int err; + + err = gip_process_chunk(client, data, len); + if (err) + return err; + + /* all chunks have been received */ + buf = client->chunk_buf; + if (buf->full) { + err = gip_handle_pkt(client, header, buf->data, buf->length); + + kfree(buf); + client->chunk_buf = NULL; + } + + return err; +} + +int gip_process_buffer(struct gip_adapter *adap, void *data, int len) +{ + struct gip_header *header = data; + struct gip_client *client; + u8 id = header->options & GIP_HEADER_CLIENT_ID; + int err = 0; + unsigned long flags; + + if (len < sizeof(*header)) { + dev_err(&adap->dev, "%s: invalid length\n", __func__); + return -EINVAL; + } + + client = gip_get_or_init_client(adap, id); + if (IS_ERR(client)) { + dev_err(&adap->dev, "%s: get/init client failed: %ld\n", + __func__, PTR_ERR(client)); + return PTR_ERR(client); + } + + spin_lock_irqsave(&client->lock, flags); + + if (atomic_read(&client->state) == GIP_CL_DISCONNECTED) + goto err_unlock; + + if (header->options & GIP_OPT_CHUNK) + err = gip_process_pkt_chunked(client, data, len); + else + err = gip_process_pkt_coherent(client, data, len); + + if (err) { + dev_err(&adap->dev, "%s: process packet failed: %d\n", + __func__, err); + print_hex_dump_debug("", DUMP_PREFIX_NONE, 16, 1, + data, len, false); + } + +err_unlock: + spin_unlock_irqrestore(&client->lock, flags); + gip_put_client(client); + + return err; +} +EXPORT_SYMBOL_GPL(gip_process_buffer); diff --git a/bus/protocol.h b/bus/protocol.h new file mode 100644 index 0000000..5fef525 --- /dev/null +++ b/bus/protocol.h @@ -0,0 +1,130 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#pragma once + +#include + +#define GIP_AUDIO_INTERVAL 8 + +enum gip_client_state { + GIP_CL_CONNECTED, + GIP_CL_ANNOUNCED, + GIP_CL_IDENTIFIED, + GIP_CL_DISCONNECTED, +}; + +enum gip_command { + GIP_CMD_ACKNOWLEDGE = 0x01, + GIP_CMD_ANNOUNCE = 0x02, + GIP_CMD_STATUS = 0x03, + GIP_CMD_IDENTIFY = 0x04, + GIP_CMD_POWER = 0x05, + GIP_CMD_AUTHENTICATE = 0x06, + GIP_CMD_GUIDE_BUTTON = 0x07, + GIP_CMD_AUDIO_CONTROL = 0x08, + GIP_CMD_RUMBLE = 0x09, + GIP_CMD_LED = 0x0a, + GIP_CMD_HID_REPORT = 0x0b, + GIP_CMD_FIRMWARE = 0x0c, + GIP_CMD_SERIAL_NUMBER = 0x1e, + GIP_CMD_INPUT = 0x20, + GIP_CMD_AUDIO_SAMPLES = 0x60, +}; + +enum gip_battery_type { + GIP_BATT_TYPE_NONE = 0x00, + GIP_BATT_TYPE_STANDARD = 0x01, + GIP_BATT_TYPE_KIT = 0x02, + GIP_BATT_TYPE_UNKNOWN = 0x03, +}; + +enum gip_battery_level { + GIP_BATT_LEVEL_LOW = 0x00, + GIP_BATT_LEVEL_NORMAL = 0x01, + GIP_BATT_LEVEL_HIGH = 0x02, + GIP_BATT_LEVEL_FULL = 0x03, +}; + +enum gip_power_mode { + GIP_PWR_ON = 0x00, + GIP_PWR_SLEEP = 0x01, + GIP_PWR_OFF = 0x04, + GIP_PWR_RESET = 0x07, +}; + +enum gip_audio_format { + GIP_AUD_FORMAT_24KHZ_MONO = 0x09, + GIP_AUD_FORMAT_48KHZ_STEREO = 0x10, +}; + +enum gip_audio_format_chat { + GIP_AUD_FORMAT_CHAT_24KHZ = 0x04, + GIP_AUD_FORMAT_CHAT_16KHZ = 0x05, +}; + +enum gip_led_mode { + GIP_LED_OFF = 0x00, + GIP_LED_ON = 0x01, + GIP_LED_BLINK_FAST = 0x02, + GIP_LED_BLINK_MED = 0x03, + GIP_LED_BLINK_SLOW = 0x04, + GIP_LED_FADE_SLOW = 0x08, + GIP_LED_FADE_FAST = 0x09, +}; + +struct gip_chunk_buffer { + bool full; + int length; + u8 data[]; +}; + +struct gip_hardware { + u16 vendor; + u16 product; + u16 version; +}; + +struct gip_info_element { + int length; + u8 data[]; +}; + +struct gip_audio_config { + enum gip_audio_format format; + bool valid; + + int channels; + int sample_rate; + + int buffer_size; + int fragment_size; + int packet_size; +}; + +struct gip_classes { + u8 count; + const char *strings[]; +}; + +struct gip_client; +struct gip_adapter; + +int gip_set_power_mode(struct gip_client *client, enum gip_power_mode mode); +int gip_complete_authentication(struct gip_client *client); +int gip_suggest_audio_format(struct gip_client *client, + enum gip_audio_format in, enum gip_audio_format out); +int gip_fix_audio_volume(struct gip_client *client); +int gip_send_rumble(struct gip_client *client, void *pkt, u8 len); +int gip_set_led_mode(struct gip_client *client, + enum gip_led_mode mode, u8 brightness); +int gip_send_audio_samples(struct gip_client *client, void *samples); + +int gip_enable_audio(struct gip_client *client); +int gip_init_audio_in(struct gip_client *client); +int gip_init_audio_out(struct gip_client *client); +void gip_disable_audio(struct gip_client *client); + +int gip_process_buffer(struct gip_adapter *adap, void *data, int len); diff --git a/dkms.conf b/dkms.conf new file mode 100644 index 0000000..6998c30 --- /dev/null +++ b/dkms.conf @@ -0,0 +1,15 @@ +PACKAGE_NAME="xone" +PACKAGE_VERSION="#VERSION#" +BUILT_MODULE_NAME[0]="xone-wired" +BUILT_MODULE_NAME[1]="xone-gip-bus" +BUILT_MODULE_NAME[2]="xone-gip-common" +BUILT_MODULE_NAME[3]="xone-gip-gamepad" +BUILT_MODULE_NAME[4]="xone-gip-headset" +BUILT_MODULE_NAME[5]="xone-gip-chatpad" +DEST_MODULE_LOCATION[0]="/kernel/drivers/input/joystick" +DEST_MODULE_LOCATION[1]="/kernel/drivers/input/joystick" +DEST_MODULE_LOCATION[2]="/kernel/drivers/input/joystick" +DEST_MODULE_LOCATION[3]="/kernel/drivers/input/joystick" +DEST_MODULE_LOCATION[4]="/kernel/drivers/input/joystick" +DEST_MODULE_LOCATION[5]="/kernel/drivers/input/joystick" +AUTOINSTALL="yes" diff --git a/driver/chatpad.c b/driver/chatpad.c new file mode 100644 index 0000000..088ccb8 --- /dev/null +++ b/driver/chatpad.c @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include + +#include "common.h" + +struct gip_chatpad { + struct gip_common common; + + struct hid_device *hid_dev; +}; + +static int gip_chatpad_hid_start(struct hid_device *dev) +{ + return 0; +} + +static void gip_chatpad_hid_stop(struct hid_device *dev) +{ +} + +static int gip_chatpad_hid_open(struct hid_device *dev) +{ + return 0; +} + +static void gip_chatpad_hid_close(struct hid_device *dev) +{ +} + +static int gip_chatpad_hid_parse(struct hid_device *dev) +{ + struct gip_chatpad *chatpad = dev->driver_data; + struct gip_client *client = chatpad->common.client; + struct hid_descriptor *desc; + + desc = (struct hid_descriptor *)client->hid_descriptor->data; + if (desc->bLength < sizeof(*desc) || desc->bNumDescriptors != 1) { + dev_err(&client->dev, "%s: invalid descriptor\n", __func__); + return -EINVAL; + } + + dev->version = le16_to_cpu(desc->bcdHID); + dev->country = desc->bCountryCode; + + return hid_parse_report(dev, client->hid_descriptor->data + sizeof(*desc), + client->hid_descriptor->length - sizeof(*desc)); +} + +static int gip_chatpad_hid_raw_request(struct hid_device *dev, + unsigned char report_num, __u8 *buf, size_t len, + unsigned char report_type, int request_type) +{ + return 0; +} + +static struct hid_ll_driver gip_chatpad_hid_driver = { + .start = gip_chatpad_hid_start, + .stop = gip_chatpad_hid_stop, + .open = gip_chatpad_hid_open, + .close = gip_chatpad_hid_close, + .parse = gip_chatpad_hid_parse, + .raw_request = gip_chatpad_hid_raw_request, +}; + +static int gip_chatpad_init_input(struct gip_chatpad *chatpad) +{ + struct gip_client *client = chatpad->common.client; + struct input_dev *dev = chatpad->common.input_dev; + int err; + + input_set_capability(dev, EV_KEY, BTN_MODE); + + err = input_register_device(dev); + if (err) + dev_err(&client->dev, "%s: register failed: %d\n", + __func__, err); + + return err; +} + +static int gip_chatpad_init_hid(struct gip_chatpad *chatpad) +{ + struct gip_client *client = chatpad->common.client; + struct hid_device *dev; + int err; + + dev = hid_allocate_device(); + if (IS_ERR(dev)) { + dev_err(&client->dev, "%s: allocate failed: %ld\n", + __func__, PTR_ERR(dev)); + return PTR_ERR(dev); + } + + dev->bus = BUS_USB; + dev->vendor = client->hardware.vendor; + dev->product = client->hardware.product; + dev->version = client->hardware.version; + dev->dev.parent = &client->dev; + dev->ll_driver = &gip_chatpad_hid_driver; + + strcpy(dev->name, chatpad->common.name); + snprintf(dev->phys, sizeof(dev->phys), "%s/input1", + dev_name(&client->dev)); + + dev->driver_data = chatpad; + + err = hid_add_device(dev); + if (err) { + dev_err(&client->dev, "%s: add failed: %d\n", __func__, err); + goto err_destroy_hid; + } + + chatpad->hid_dev = dev; + + return 0; + +err_destroy_hid: + hid_destroy_device(dev); + + return err; +} + +static int gip_chatpad_op_guide_button(struct gip_client *client, bool pressed) +{ + struct gip_chatpad *chatpad = dev_get_drvdata(&client->dev); + struct input_dev *dev = chatpad->common.input_dev; + + input_report_key(dev, BTN_MODE, pressed); + input_sync(dev); + + return 0; +} + +static int gip_chatpad_op_hid_report(struct gip_client *client, + void *data, int len) +{ + struct gip_chatpad *chatpad = dev_get_drvdata(&client->dev); + + return hid_input_report(chatpad->hid_dev, HID_INPUT_REPORT, + data, len, true); +} + +static int gip_chatpad_probe(struct gip_client *client) +{ + struct gip_chatpad *chatpad; + struct gip_info_element *hid_desc = client->hid_descriptor; + int err; + + if (!hid_desc || hid_desc->length < sizeof(struct hid_descriptor)) + return -ENODEV; + + chatpad = devm_kzalloc(&client->dev, sizeof(*chatpad), GFP_KERNEL); + if (!chatpad) + return -ENOMEM; + + chatpad->common.client = client; + chatpad->common.name = "Microsoft X-Box One chatpad"; + + err = gip_init_input(&chatpad->common); + if (err) + return err; + + err = gip_chatpad_init_input(chatpad); + if (err) + return err; + + err = gip_chatpad_init_hid(chatpad); + if (err) + return err; + + err = gip_set_power_mode(client, GIP_PWR_ON); + if (err) + return err; + + dev_set_drvdata(&client->dev, chatpad); + + return 0; +} + +static void gip_chatpad_remove(struct gip_client *client) +{ + struct gip_chatpad *chatpad = dev_get_drvdata(&client->dev); + + hid_destroy_device(chatpad->hid_dev); + dev_set_drvdata(&client->dev, NULL); +} + +static struct gip_driver gip_chatpad_driver = { + .name = "xone-gip-chatpad", + .class = "Windows.Xbox.Input.Chatpad", + .ops = { + .guide_button = gip_chatpad_op_guide_button, + .hid_report = gip_chatpad_op_hid_report, + }, + .probe = gip_chatpad_probe, + .remove = gip_chatpad_remove, +}; +module_gip_driver(gip_chatpad_driver); + +MODULE_ALIAS("gip:Windows.Xbox.Input.Chatpad"); +MODULE_AUTHOR("Severin von Wnuck "); +MODULE_DESCRIPTION("xone GIP chatpad driver"); +MODULE_VERSION("#VERSION#"); +MODULE_LICENSE("GPL"); diff --git a/driver/common.c b/driver/common.c new file mode 100644 index 0000000..85804e5 --- /dev/null +++ b/driver/common.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include +#include +#include + +#include "common.h" + +#define GIP_LED_BRIGHTNESS_DEFAULT 20 +#define GIP_LED_BRIGHTNESS_MAX 50 + +static enum power_supply_property gip_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_MODEL_NAME, +}; + +struct gip_battery { + const char *name; + int status; + int capacity; +}; + +static int gip_get_battery_prop(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct gip_battery *batt = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = batt->status; + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + val->intval = batt->capacity; + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_DEVICE; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = batt->name; + break; + default: + return -EINVAL; + } + + return 0; +} + +int gip_init_battery(struct gip_common *common) +{ + struct gip_client *client = common->client; + struct gip_battery *batt; + struct power_supply_desc *desc; + struct power_supply_config cfg = {}; + struct power_supply *psy; + + batt = devm_kzalloc(&client->dev, sizeof(*batt), GFP_KERNEL); + if (!batt) + return -ENOMEM; + + desc = devm_kzalloc(&client->dev, sizeof(*desc), GFP_KERNEL); + if (!desc) + return -ENOMEM; + + batt->name = common->name; + batt->status = POWER_SUPPLY_STATUS_UNKNOWN; + batt->capacity = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + + desc->name = dev_name(&client->dev); + desc->type = POWER_SUPPLY_TYPE_BATTERY; + desc->properties = gip_battery_props; + desc->num_properties = ARRAY_SIZE(gip_battery_props); + desc->get_property = gip_get_battery_prop; + + cfg.drv_data = batt; + + psy = devm_power_supply_register(&client->dev, desc, &cfg); + if (IS_ERR(psy)) { + dev_err(&client->dev, "%s: register failed: %ld\n", + __func__, PTR_ERR(psy)); + return PTR_ERR(psy); + } + + power_supply_powers(psy, &client->dev); + + common->power_supply = psy; + + return 0; +} +EXPORT_SYMBOL_GPL(gip_init_battery); + +int gip_report_battery(struct gip_common *common, enum gip_battery_type type, + enum gip_battery_level level) +{ + struct gip_battery *batt = power_supply_get_drvdata(common->power_supply); + + if (type == GIP_BATT_TYPE_NONE) + batt->status = POWER_SUPPLY_STATUS_NOT_CHARGING; + else + batt->status = POWER_SUPPLY_STATUS_DISCHARGING; + + if (type == GIP_BATT_TYPE_NONE) + batt->capacity = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + else if (level == GIP_BATT_LEVEL_LOW) + batt->capacity = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (level == GIP_BATT_LEVEL_NORMAL) + batt->capacity = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + else if (level == GIP_BATT_LEVEL_HIGH) + batt->capacity = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + else if (level == GIP_BATT_LEVEL_FULL) + batt->capacity = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + + power_supply_changed(common->power_supply); + + return 0; +} +EXPORT_SYMBOL_GPL(gip_report_battery); + +static void gip_led_brightness_set(struct led_classdev *dev, + enum led_brightness brightness) +{ + struct gip_client *client = container_of(dev->dev->parent, + typeof(*client), dev); + int err; + + if (dev->flags & LED_UNREGISTERING) + return; + + dev_dbg(&client->dev, "%s: brightness=%d\n", __func__, brightness); + + err = gip_set_led_mode(client, GIP_LED_ON, brightness); + if (err) + dev_err(&client->dev, "%s: set LED mode failed: %d\n", + __func__, err); +} + +int gip_init_led(struct gip_common *common) +{ + struct gip_client *client = common->client; + struct led_classdev *dev; + int err; + + /* set default brightness */ + err = gip_set_led_mode(client, GIP_LED_ON, GIP_LED_BRIGHTNESS_DEFAULT); + if (err) { + dev_err(&client->dev, "%s: set brightness failed: %d\n", + __func__, err); + return err; + } + + dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + dev->name = devm_kasprintf(&client->dev, GFP_KERNEL, + "%s:white:status", dev_name(&client->dev)); + if (!dev->name) + return -ENOMEM; + + dev->brightness = GIP_LED_BRIGHTNESS_DEFAULT; + dev->max_brightness = GIP_LED_BRIGHTNESS_MAX; + dev->brightness_set = gip_led_brightness_set; + + err = devm_led_classdev_register(&client->dev, dev); + if (err) { + dev_err(&client->dev, "%s: register failed: %d\n", + __func__, err); + return err; + } + + common->led_dev = dev; + + return 0; +} +EXPORT_SYMBOL_GPL(gip_init_led); + +int gip_init_input(struct gip_common *common) +{ + struct gip_client *client = common->client; + struct input_dev *dev; + + dev = devm_input_allocate_device(&client->dev); + if (!dev) + return -ENOMEM; + + dev->phys = devm_kasprintf(&client->dev, GFP_KERNEL, + "%s/input0", dev_name(&client->dev)); + if (!dev->phys) + return -ENOMEM; + + dev->name = common->name; + dev->id.bustype = BUS_VIRTUAL; + dev->id.vendor = client->hardware.vendor; + dev->id.product = client->hardware.product; + dev->id.version = client->hardware.version; + dev->dev.parent = &client->dev; + + common->input_dev = dev; + + return 0; +} +EXPORT_SYMBOL_GPL(gip_init_input); + +MODULE_AUTHOR("Severin von Wnuck "); +MODULE_DESCRIPTION("xone GIP common driver"); +MODULE_VERSION("#VERSION#"); +MODULE_LICENSE("GPL"); diff --git a/driver/common.h b/driver/common.h new file mode 100644 index 0000000..833f754 --- /dev/null +++ b/driver/common.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#pragma once + +#include "../bus/bus.h" + +struct power_supply; +struct led_classdev; +struct input_dev; + +struct gip_common { + struct gip_client *client; + const char *name; + + struct power_supply *power_supply; + struct led_classdev *led_dev; + struct input_dev *input_dev; +}; + +int gip_init_battery(struct gip_common *common); +int gip_report_battery(struct gip_common *common, enum gip_battery_type type, + enum gip_battery_level level); +int gip_init_led(struct gip_common *common); +int gip_init_input(struct gip_common *common); diff --git a/driver/gamepad.c b/driver/gamepad.c new file mode 100644 index 0000000..7b3c2d5 --- /dev/null +++ b/driver/gamepad.c @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include +#include + +#include "common.h" + +#define GIP_GP_RUMBLE_DELAY msecs_to_jiffies(10) +#define GIP_GP_RUMBLE_MAX 100 + +enum gip_gamepad_button { + GIP_GP_BTN_MENU = BIT(2), + GIP_GP_BTN_VIEW = BIT(3), + GIP_GP_BTN_A = BIT(4), + GIP_GP_BTN_B = BIT(5), + GIP_GP_BTN_X = BIT(6), + GIP_GP_BTN_Y = BIT(7), + GIP_GP_BTN_DPAD_U = BIT(8), + GIP_GP_BTN_DPAD_D = BIT(9), + GIP_GP_BTN_DPAD_L = BIT(10), + GIP_GP_BTN_DPAD_R = BIT(11), + GIP_GP_BTN_BUMPER_L = BIT(12), + GIP_GP_BTN_BUMPER_R = BIT(13), + GIP_GP_BTN_STICK_L = BIT(14), + GIP_GP_BTN_STICK_R = BIT(15), +}; + +enum gip_gamepad_motor { + GIP_GP_MOTOR_R = BIT(0), + GIP_GP_MOTOR_L = BIT(1), + GIP_GP_MOTOR_RT = BIT(2), + GIP_GP_MOTOR_LT = BIT(3), +}; + +struct gip_gamepad_pkt_input { + __le16 buttons; + __le16 trigger_left; + __le16 trigger_right; + __le16 stick_left_x; + __le16 stick_left_y; + __le16 stick_right_x; + __le16 stick_right_y; +} __packed; + +struct gip_gamepad_pkt_rumble { + u8 unknown; + u8 motors; + u8 left_trigger; + u8 right_trigger; + u8 left; + u8 right; + u8 duration; + u8 delay; + u8 repeat; +} __packed; + +struct gip_gamepad { + struct gip_common common; + + struct gip_gamepad_rumble { + spinlock_t lock; + bool queued; + unsigned long last; + struct timer_list timer; + struct gip_gamepad_pkt_rumble pkt; + } rumble; +}; + +static void gip_gamepad_send_rumble(struct timer_list *timer) +{ + struct gip_gamepad_rumble *rumble = from_timer(rumble, timer, timer); + struct gip_gamepad *gamepad = container_of(rumble, + typeof(*gamepad), rumble); + struct gip_client *client = gamepad->common.client; + unsigned long flags; + + spin_lock_irqsave(&rumble->lock, flags); + + gip_send_rumble(client, &rumble->pkt, sizeof(rumble->pkt)); + rumble->last = jiffies; + + spin_unlock_irqrestore(&rumble->lock, flags); +} + +static int gip_gamepad_queue_rumble(struct input_dev *dev, + void *data, struct ff_effect *effect) +{ + struct gip_gamepad_rumble *rumble = input_get_drvdata(dev); + u32 mag_left = effect->u.rumble.strong_magnitude; + u32 mag_right = effect->u.rumble.weak_magnitude; + unsigned long flags; + + if (effect->type != FF_RUMBLE) + return 0; + + spin_lock_irqsave(&rumble->lock, flags); + + rumble->pkt.motors = GIP_GP_MOTOR_R | GIP_GP_MOTOR_L; + rumble->pkt.left = (mag_left * GIP_GP_RUMBLE_MAX + S16_MAX) / U16_MAX; + rumble->pkt.right = (mag_right * GIP_GP_RUMBLE_MAX + S16_MAX) / U16_MAX; + rumble->pkt.duration = 0xff; + rumble->pkt.repeat = 0xeb; + + /* delay rumble to work around firmware bug */ + if (!timer_pending(&rumble->timer)) + mod_timer(&rumble->timer, rumble->last + GIP_GP_RUMBLE_DELAY); + + spin_unlock_irqrestore(&rumble->lock, flags); + + return 0; +} + +static int gip_gamepad_init_input(struct gip_gamepad *gamepad) +{ + struct gip_client *client = gamepad->common.client; + struct input_dev *dev = gamepad->common.input_dev; + int err; + + input_set_capability(dev, EV_KEY, BTN_MODE); + input_set_capability(dev, EV_KEY, BTN_START); + input_set_capability(dev, EV_KEY, BTN_SELECT); + input_set_capability(dev, EV_KEY, BTN_A); + input_set_capability(dev, EV_KEY, BTN_B); + input_set_capability(dev, EV_KEY, BTN_X); + input_set_capability(dev, EV_KEY, BTN_Y); + input_set_capability(dev, EV_KEY, BTN_TL); + input_set_capability(dev, EV_KEY, BTN_TR); + input_set_capability(dev, EV_KEY, BTN_THUMBL); + input_set_capability(dev, EV_KEY, BTN_THUMBR); + input_set_capability(dev, EV_FF, FF_RUMBLE); + input_set_abs_params(dev, ABS_X, -32768, 32767, 16, 128); + input_set_abs_params(dev, ABS_RX, -32768, 32767, 16, 128); + input_set_abs_params(dev, ABS_Y, -32768, 32767, 16, 128); + input_set_abs_params(dev, ABS_RY, -32768, 32767, 16, 128); + input_set_abs_params(dev, ABS_Z, 0, 1023, 0, 0); + input_set_abs_params(dev, ABS_RZ, 0, 1023, 0, 0); + input_set_abs_params(dev, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(dev, ABS_HAT0Y, -1, 1, 0, 0); + input_set_drvdata(dev, &gamepad->rumble); + + err = input_ff_create_memless(dev, NULL, gip_gamepad_queue_rumble); + if (err) { + dev_err(&client->dev, "%s: create FF failed: %d\n", + __func__, err); + return err; + } + + err = input_register_device(dev); + if (err) { + dev_err(&client->dev, "%s: register failed: %d\n", + __func__, err); + return err; + } + + spin_lock_init(&gamepad->rumble.lock); + timer_setup(&gamepad->rumble.timer, gip_gamepad_send_rumble, 0); + + return 0; +} + +static int gip_gamepad_op_battery(struct gip_client *client, + enum gip_battery_type type, enum gip_battery_level level) +{ + struct gip_gamepad *gamepad = dev_get_drvdata(&client->dev); + + return gip_report_battery(&gamepad->common, type, level); +} + +static int gip_gamepad_op_guide_button(struct gip_client *client, bool pressed) +{ + struct gip_gamepad *gamepad = dev_get_drvdata(&client->dev); + struct input_dev *dev = gamepad->common.input_dev; + + input_report_key(dev, BTN_MODE, pressed); + input_sync(dev); + + return 0; +} + +static int gip_gamepad_op_input(struct gip_client *client, void *data, int len) +{ + struct gip_gamepad *gamepad = dev_get_drvdata(&client->dev); + struct input_dev *dev = gamepad->common.input_dev; + struct gip_gamepad_pkt_input *pkt = data; + u16 buttons = le16_to_cpu(pkt->buttons); + + if (len < sizeof(*pkt)) + return -EINVAL; + + input_report_key(dev, BTN_START, buttons & GIP_GP_BTN_MENU); + input_report_key(dev, BTN_SELECT, buttons & GIP_GP_BTN_VIEW); + input_report_key(dev, BTN_A, buttons & GIP_GP_BTN_A); + input_report_key(dev, BTN_B, buttons & GIP_GP_BTN_B); + input_report_key(dev, BTN_X, buttons & GIP_GP_BTN_X); + input_report_key(dev, BTN_Y, buttons & GIP_GP_BTN_Y); + input_report_key(dev, BTN_TL, buttons & GIP_GP_BTN_BUMPER_L); + input_report_key(dev, BTN_TR, buttons & GIP_GP_BTN_BUMPER_R); + input_report_key(dev, BTN_THUMBL, buttons & GIP_GP_BTN_STICK_L); + input_report_key(dev, BTN_THUMBR, buttons & GIP_GP_BTN_STICK_R); + input_report_abs(dev, ABS_X, (s16)le16_to_cpu(pkt->stick_left_x)); + input_report_abs(dev, ABS_RX, (s16)le16_to_cpu(pkt->stick_right_x)); + input_report_abs(dev, ABS_Y, ~(s16)le16_to_cpu(pkt->stick_left_y)); + input_report_abs(dev, ABS_RY, ~(s16)le16_to_cpu(pkt->stick_right_y)); + input_report_abs(dev, ABS_Z, le16_to_cpu(pkt->trigger_left)); + input_report_abs(dev, ABS_RZ, le16_to_cpu(pkt->trigger_right)); + input_report_abs(dev, ABS_HAT0X, + !!(buttons & GIP_GP_BTN_DPAD_R) - !!(buttons & GIP_GP_BTN_DPAD_L)); + input_report_abs(dev, ABS_HAT0Y, + !!(buttons & GIP_GP_BTN_DPAD_D) - !!(buttons & GIP_GP_BTN_DPAD_U)); + input_sync(dev); + + return 0; +} + +static int gip_gamepad_probe(struct gip_client *client) +{ + struct gip_gamepad *gamepad; + int err; + + gamepad = devm_kzalloc(&client->dev, sizeof(*gamepad), GFP_KERNEL); + if (!gamepad) + return -ENOMEM; + + gamepad->common.client = client; + gamepad->common.name = "Microsoft X-Box One pad"; + + err = gip_init_input(&gamepad->common); + if (err) + return err; + + err = gip_gamepad_init_input(gamepad); + if (err) + return err; + + err = gip_init_battery(&gamepad->common); + if (err) + return err; + + err = gip_set_power_mode(client, GIP_PWR_ON); + if (err) + return err; + + err = gip_init_led(&gamepad->common); + if (err) + return err; + + err = gip_complete_authentication(client); + if (err) + return err; + + dev_set_drvdata(&client->dev, gamepad); + + return 0; +} + +static void gip_gamepad_remove(struct gip_client *client) +{ + struct gip_gamepad *gamepad = dev_get_drvdata(&client->dev); + + del_timer_sync(&gamepad->rumble.timer); + dev_set_drvdata(&client->dev, NULL); +} + +static int gip_gamepad_suspend(struct gip_client *client) +{ + int err; + + err = gip_set_power_mode(client, GIP_PWR_OFF); + if (err) + dev_err(&client->dev, "%s: set power mode failed: %d\n", + __func__, err); + + return err; +} + +static struct gip_driver gip_gamepad_driver = { + .name = "xone-gip-gamepad", + .class = "Windows.Xbox.Input.Gamepad", + .ops = { + .battery = gip_gamepad_op_battery, + .guide_button = gip_gamepad_op_guide_button, + .input = gip_gamepad_op_input, + }, + .probe = gip_gamepad_probe, + .remove = gip_gamepad_remove, + .suspend = gip_gamepad_suspend, +}; +module_gip_driver(gip_gamepad_driver); + +MODULE_ALIAS("gip:Windows.Xbox.Input.Gamepad"); +MODULE_AUTHOR("Severin von Wnuck "); +MODULE_DESCRIPTION("xone GIP gamepad driver"); +MODULE_VERSION("#VERSION#"); +MODULE_LICENSE("GPL"); diff --git a/driver/headset.c b/driver/headset.c new file mode 100644 index 0000000..06d45e5 --- /dev/null +++ b/driver/headset.c @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include +#include +#include +#include + +#include "common.h" + +#define GIP_HS_ENABLE_DELAY msecs_to_jiffies(1000) + +static const struct snd_pcm_hardware gip_headset_pcm_hw = { + .info = SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_BATCH | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER, + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_CONTINUOUS, + .periods_min = 2, + .periods_max = 1024, +}; + +struct gip_headset { + struct gip_common common; + + struct delayed_work enable_work; + struct work_struct register_work; + bool registered; + + struct hrtimer timer; + void *buffer; + + struct gip_headset_stream { + struct snd_pcm_substream *substream; + snd_pcm_uframes_t pointer; + snd_pcm_uframes_t period; + } playback, capture; + + struct snd_card *card; + struct snd_pcm *pcm; +}; + +static int gip_headset_pcm_open(struct snd_pcm_substream *sub) +{ + struct gip_headset *headset = snd_pcm_substream_chip(sub); + struct gip_client *client = headset->common.client; + struct gip_audio_config *cfg; + struct snd_pcm_hardware hw = gip_headset_pcm_hw; + + if (sub->stream == SNDRV_PCM_STREAM_PLAYBACK) + cfg = &client->audio_config_out; + else + cfg = &client->audio_config_in; + + hw.rate_min = cfg->sample_rate; + hw.rate_max = cfg->sample_rate; + hw.channels_min = cfg->channels; + hw.channels_max = cfg->channels; + hw.buffer_bytes_max = cfg->buffer_size * 8; + hw.period_bytes_min = cfg->buffer_size; + hw.period_bytes_max = cfg->buffer_size * 8; + + sub->runtime->hw = hw; + + return 0; +} + +static int gip_headset_pcm_close(struct snd_pcm_substream *sub) +{ + return 0; +} + +static int gip_headset_pcm_hw_params(struct snd_pcm_substream *sub, + struct snd_pcm_hw_params *params) +{ + return snd_pcm_lib_alloc_vmalloc_buffer(sub, params_buffer_bytes(params)); +} + +static int gip_headset_pcm_hw_free(struct snd_pcm_substream *sub) +{ + return snd_pcm_lib_free_vmalloc_buffer(sub); +} + +static int gip_headset_pcm_prepare(struct snd_pcm_substream *sub) +{ + return 0; +} + +static int gip_headset_pcm_trigger(struct snd_pcm_substream *sub, int cmd) +{ + struct gip_headset *headset = snd_pcm_substream_chip(sub); + struct gip_headset_stream *stream; + struct gip_client *client = headset->common.client; + + if (sub->stream == SNDRV_PCM_STREAM_PLAYBACK) + stream = &headset->playback; + else + stream = &headset->capture; + + stream->pointer = 0; + stream->period = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + stream->substream = sub; + break; + case SNDRV_PCM_TRIGGER_STOP: + stream->substream = NULL; + break; + default: + return -EINVAL; + } + + if (!stream->substream && sub->stream == SNDRV_PCM_STREAM_PLAYBACK) + memset(headset->buffer, 0, client->audio_config_out.buffer_size); + + return 0; +} + +static snd_pcm_uframes_t gip_headset_pcm_pointer(struct snd_pcm_substream *sub) +{ + struct gip_headset *headset = snd_pcm_substream_chip(sub); + struct gip_headset_stream *stream; + + if (sub->stream == SNDRV_PCM_STREAM_PLAYBACK) + stream = &headset->playback; + else + stream = &headset->capture; + + return bytes_to_frames(sub->runtime, stream->pointer); +} + +static const struct snd_pcm_ops gip_headset_pcm_ops = { + .open = gip_headset_pcm_open, + .close = gip_headset_pcm_close, + .ioctl = snd_pcm_lib_ioctl, + .hw_params = gip_headset_pcm_hw_params, + .hw_free = gip_headset_pcm_hw_free, + .prepare = gip_headset_pcm_prepare, + .trigger = gip_headset_pcm_trigger, + .pointer = gip_headset_pcm_pointer, + .page = snd_pcm_lib_get_vmalloc_page, +}; + +static bool gip_headset_copy_playback(struct gip_headset_stream *stream, + unsigned char *data, int len) +{ + struct snd_pcm_runtime *runtime = stream->substream->runtime; + size_t buf_size = snd_pcm_lib_buffer_bytes(stream->substream); + size_t remaining = buf_size - stream->pointer; + + if (len <= remaining) { + memcpy(data, runtime->dma_area + stream->pointer, len); + } else { + memcpy(data, runtime->dma_area + stream->pointer, remaining); + memcpy(data + remaining, runtime->dma_area, len - remaining); + } + + stream->pointer += len; + if (stream->pointer >= buf_size) + stream->pointer -= buf_size; + + stream->period += len; + if (stream->period >= runtime->period_size) { + stream->period -= runtime->period_size; + return true; + } + + return false; +} + +static bool gip_headset_copy_capture(struct gip_headset_stream *stream, + unsigned char *data, int len) +{ + struct snd_pcm_runtime *runtime = stream->substream->runtime; + size_t buf_size = snd_pcm_lib_buffer_bytes(stream->substream); + size_t remaining = buf_size - stream->pointer; + + if (len <= remaining) { + memcpy(runtime->dma_area + stream->pointer, data, len); + } else { + memcpy(runtime->dma_area + stream->pointer, data, remaining); + memcpy(runtime->dma_area, data + remaining, len - remaining); + } + + stream->pointer += len; + if (stream->pointer >= buf_size) + stream->pointer -= buf_size; + + stream->period += len; + if (stream->period >= runtime->period_size) { + stream->period -= runtime->period_size; + return true; + } + + return false; +} + +static enum hrtimer_restart gip_headset_send_samples(struct hrtimer *timer) +{ + struct gip_headset *headset = container_of(timer, + typeof(*headset), timer); + struct gip_headset_stream *stream = &headset->playback; + struct gip_client *client = headset->common.client; + struct snd_pcm_substream *sub = stream->substream; + bool elapsed = false; + unsigned long flags; + + if (sub) { + snd_pcm_stream_lock_irqsave(sub, flags); + + if (sub->runtime && snd_pcm_running(sub)) + elapsed = gip_headset_copy_playback(stream, + headset->buffer, + client->audio_config_out.buffer_size); + + snd_pcm_stream_unlock_irqrestore(sub, flags); + + if (elapsed) + snd_pcm_period_elapsed(sub); + } + + if (gip_send_audio_samples(client, headset->buffer)) + return HRTIMER_NORESTART; + + hrtimer_forward_now(timer, ms_to_ktime(GIP_AUDIO_INTERVAL)); + + return HRTIMER_RESTART; +} + +static int gip_headset_init_card(struct gip_headset *headset) +{ + struct gip_client *client = headset->common.client; + struct snd_card *card; + int err; + + err = snd_card_new(&client->dev, SNDRV_DEFAULT_IDX1, + SNDRV_DEFAULT_STR1, THIS_MODULE, 0, &card); + if (err) + return err; + + strcpy(card->driver, "GIP Headset"); + strcpy(card->shortname, headset->common.name); + snprintf(card->longname, sizeof(card->longname), "%s at %s", + headset->common.name, dev_name(&client->dev)); + + headset->card = card; + + return 0; +} + +static int gip_headset_init_pcm(struct gip_headset *headset) +{ + struct snd_pcm *pcm; + int err; + + err = snd_pcm_new(headset->card, "GIP Headset", 0, 1, 1, &pcm); + if (err) + return err; + + strcpy(pcm->name, "GIP Headset"); + + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &gip_headset_pcm_ops); + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &gip_headset_pcm_ops); + + pcm->private_data = headset; + headset->pcm = pcm; + + return 0; +} + +static int gip_headset_init_audio_out(struct gip_headset *headset) +{ + struct gip_client *client = headset->common.client; + int err; + + /* set headset volume to maximum */ + err = gip_fix_audio_volume(client); + if (err) + return err; + + err = gip_init_audio_out(client); + if (err) + return err; + + headset->buffer = devm_kzalloc(&client->dev, + client->audio_config_out.buffer_size, GFP_KERNEL); + if (!headset->buffer) + return -ENOMEM; + + hrtimer_start(&headset->timer, 0, HRTIMER_MODE_REL); + + return 0; +} + +static void gip_headset_enable(struct work_struct *work) +{ + struct gip_headset *headset = container_of(to_delayed_work(work), + typeof(*headset), enable_work); + struct gip_client *client = headset->common.client; + struct gip_info_element *fmts = client->audio_formats; + int err; + + dev_dbg(&client->dev, "%s: format=0x%02x/0x%02x\n", __func__, + fmts->data[0], fmts->data[1]); + + /* suggest initial audio format */ + err = gip_suggest_audio_format(client, fmts->data[0], fmts->data[1]); + if (err) + dev_err(&client->dev, "%s: suggest format failed: %d\n", + __func__, err); +} + +static void gip_headset_register(struct work_struct *work) +{ + struct gip_headset *headset = container_of(work, typeof(*headset), + register_work); + struct gip_client *client = headset->common.client; + int err; + + err = gip_headset_init_card(headset); + if (err) { + dev_err(&client->dev, "%s: init card failed: %d\n", + __func__, err); + return; + } + + err = gip_headset_init_pcm(headset); + if (err) { + dev_err(&client->dev, "%s: init PCM failed: %d\n", + __func__, err); + goto err_free_card; + } + + err = snd_card_register(headset->card); + if (err) { + dev_err(&client->dev, "%s: register card failed: %d\n", + __func__, err); + goto err_free_card; + } + + err = gip_headset_init_audio_out(headset); + if (err) { + dev_err(&client->dev, "%s: init audio out failed: %d\n", + __func__, err); + goto err_free_card; + } + + return; + +err_free_card: + snd_card_free(headset->card); +} + +static int gip_headset_op_audio_ready(struct gip_client *client) +{ + int err; + + err = gip_set_power_mode(client, GIP_PWR_ON); + if (err) + dev_err(&client->dev, "%s: set power mode failed: %d\n", + __func__, err); + + return err; +} + +static int gip_headset_op_audio_volume(struct gip_client *client, + int in, int out) +{ + struct gip_headset *headset = dev_get_drvdata(&client->dev); + + /* headset reported initial volume, start audio I/O */ + if (!headset->registered) { + schedule_work(&headset->register_work); + headset->registered = true; + } + + /* ignore hardware volume, let software handle volume changes */ + return 0; +} + +static int gip_headset_op_audio_samples(struct gip_client *client, + void *data, int len) +{ + struct gip_headset *headset = dev_get_drvdata(&client->dev); + struct gip_headset_stream *stream = &headset->capture; + struct snd_pcm_substream *sub = stream->substream; + bool elapsed = false; + unsigned long flags; + + if (!sub) + return 0; + + snd_pcm_stream_lock_irqsave(sub, flags); + + if (sub->runtime && snd_pcm_running(sub)) + elapsed = gip_headset_copy_capture(stream, data, len); + + snd_pcm_stream_unlock_irqrestore(sub, flags); + + if (elapsed) + snd_pcm_period_elapsed(sub); + + return 0; +} + +static int gip_headset_probe(struct gip_client *client) +{ + struct gip_headset *headset; + struct gip_info_element *fmts = client->audio_formats; + int err; + + if (!fmts || fmts->length < sizeof(u8) * 2) + return -ENODEV; + + headset = devm_kzalloc(&client->dev, sizeof(*headset), GFP_KERNEL); + if (!headset) + return -ENOMEM; + + headset->common.client = client; + headset->common.name = "Microsoft X-Box One headset"; + + INIT_DELAYED_WORK(&headset->enable_work, gip_headset_enable); + INIT_WORK(&headset->register_work, gip_headset_register); + + hrtimer_init(&headset->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + headset->timer.function = gip_headset_send_samples; + + err = gip_enable_audio(client); + if (err) + return err; + + err = gip_init_audio_in(client); + if (err) + return err; + + dev_set_drvdata(&client->dev, headset); + + /* delay to prevent response from being dropped */ + schedule_delayed_work(&headset->enable_work, GIP_HS_ENABLE_DELAY); + + return 0; +} + +static void gip_headset_remove(struct gip_client *client) +{ + struct gip_headset *headset = dev_get_drvdata(&client->dev); + + cancel_delayed_work_sync(&headset->enable_work); + cancel_work_sync(&headset->register_work); + hrtimer_cancel(&headset->timer); + gip_disable_audio(client); + + if (headset->card) { + snd_card_disconnect(headset->card); + snd_card_free_when_closed(headset->card); + } + + dev_set_drvdata(&client->dev, NULL); +} + +static struct gip_driver gip_headset_driver = { + .name = "xone-gip-headset", + .class = "Windows.Xbox.Input.Headset", + .ops = { + .audio_ready = gip_headset_op_audio_ready, + .audio_volume = gip_headset_op_audio_volume, + .audio_samples = gip_headset_op_audio_samples, + }, + .probe = gip_headset_probe, + .remove = gip_headset_remove, +}; +module_gip_driver(gip_headset_driver); + +MODULE_ALIAS("gip:Windows.Xbox.Input.Headset"); +MODULE_AUTHOR("Severin von Wnuck "); +MODULE_DESCRIPTION("xone GIP headset driver"); +MODULE_VERSION("#VERSION#"); +MODULE_LICENSE("GPL"); diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..66fa02d --- /dev/null +++ b/install.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env sh + +# The blacklist should be placed in /usr/local/lib/modprobe.d for kmod 29+ +VERSION=$(git describe --tags 2> /dev/null || echo 'unknown') +SOURCE="/usr/src/xone-$VERSION" +BLACKLIST='/etc/modprobe.d/xone-blacklist.conf' +LOG="/var/lib/dkms/xone/$VERSION/build/make.log" + +echo "Installing xone $VERSION..." +cp -r . "$SOURCE" +find "$SOURCE" -type f \( -name 'dkms.conf' -o -name '*.c' \) -exec sed -i "s/#VERSION#/$VERSION/" {} + + +if [ "$1" != '--release' ]; then + echo 'ccflags-y += -DDEBUG' >> "$SOURCE/Kbuild" +fi + +if dkms install "xone/$VERSION"; then + install -D -m 644 modprobe.conf "$BLACKLIST" +else + cat "$LOG" + exit 1 +fi diff --git a/modprobe.conf b/modprobe.conf new file mode 100644 index 0000000..76f498f --- /dev/null +++ b/modprobe.conf @@ -0,0 +1 @@ +blacklist xpad diff --git a/transport/wired.c b/transport/wired.c new file mode 100644 index 0000000..ad49c8f --- /dev/null +++ b/transport/wired.c @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Severin von Wnuck + */ + +#include +#include +#include + +#include "../bus/bus.h" + +#define XONE_WIRED_NUM_DATA_URBS 8 + +#define XONE_WIRED_NUM_AUDIO_URBS 12 +#define XONE_WIRED_NUM_AUDIO_PKTS 8 + +#define XONE_WIRED_VENDOR(vendor) \ + .match_flags = USB_DEVICE_ID_MATCH_VENDOR | \ + USB_DEVICE_ID_MATCH_INT_INFO | \ + USB_DEVICE_ID_MATCH_INT_NUMBER, \ + .idVendor = (vendor), \ + .bInterfaceClass = USB_CLASS_VENDOR_SPEC, \ + .bInterfaceSubClass = 0x47, \ + .bInterfaceProtocol = 0xd0, \ + .bInterfaceNumber = 0x00, + +struct xone_wired { + struct usb_device *udev; + + struct xone_wired_port { + struct usb_interface *intf; + + struct usb_endpoint_descriptor *ep_in; + struct usb_endpoint_descriptor *ep_out; + + struct urb *urb_in; + struct usb_anchor urbs_out_idle; + struct usb_anchor urbs_out_busy; + + int buffer_length_in; + int buffer_length_out; + } data_port, audio_port; + + struct gip_adapter *adapter; +}; + +static void xone_wired_data_in_complete(struct urb *urb) +{ + struct xone_wired *xone = urb->context; + struct device *dev = &xone->data_port.intf->dev; + int err; + + switch (urb->status) { + case 0: + break; + case -ENOENT: + case -ECONNRESET: + case -ESHUTDOWN: + return; + default: + goto resubmit; + } + + if (urb->actual_length) { + err = gip_process_buffer(xone->adapter, + urb->transfer_buffer, urb->actual_length); + if (err) + dev_err(dev, "%s: process failed: %d\n", __func__, err); + } + +resubmit: + /* can fail during USB device removal */ + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err) + dev_dbg(dev, "%s: submit failed: %d\n", __func__, err); +} + +static void xone_wired_audio_in_complete(struct urb *urb) +{ + struct xone_wired *xone = urb->context; + struct device *dev = &xone->audio_port.intf->dev; + struct usb_iso_packet_descriptor *desc; + int i, err; + + if (urb->status) + return; + + for (i = 0; i < urb->number_of_packets; i++) { + desc = &urb->iso_frame_desc[i]; + if (!desc->actual_length) + continue; + + err = gip_process_buffer(xone->adapter, + urb->transfer_buffer + desc->offset, + desc->actual_length); + if (err) + dev_err(dev, "%s: process failed: %d\n", __func__, err); + } + + /* can fail during USB device removal */ + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err) + dev_dbg(dev, "%s: submit failed: %d\n", __func__, err); +} + +static void xone_wired_out_complete(struct urb *urb) +{ + struct xone_wired_port *port = urb->context; + + usb_anchor_urb(urb, &port->urbs_out_idle); +} + +static int xone_wired_init_data_in(struct xone_wired *xone) +{ + struct xone_wired_port *port = &xone->data_port; + struct urb *urb; + int len; + void *buf; + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) + return -ENOMEM; + + len = usb_endpoint_maxp(port->ep_in); + buf = usb_alloc_coherent(xone->udev, len, GFP_KERNEL, + &urb->transfer_dma); + if (!buf) + return -ENOMEM; + + usb_fill_int_urb(urb, xone->udev, + usb_rcvintpipe(xone->udev, port->ep_in->bEndpointAddress), + buf, len, xone_wired_data_in_complete, + xone, port->ep_in->bInterval); + urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + port->urb_in = urb; + port->buffer_length_in = len; + + return 0; +} + +static int xone_wired_init_data_out(struct xone_wired *xone) +{ + struct xone_wired_port *port = &xone->data_port; + struct urb *urb; + void *buf; + int len, i; + + len = usb_endpoint_maxp(port->ep_out); + + for (i = 0; i < XONE_WIRED_NUM_DATA_URBS; i++) { + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) + return -ENOMEM; + + buf = usb_alloc_coherent(xone->udev, len, GFP_KERNEL, + &urb->transfer_dma); + if (!buf) + return -ENOMEM; + + usb_fill_int_urb(urb, xone->udev, + usb_sndintpipe(xone->udev, port->ep_out->bEndpointAddress), + buf, len, xone_wired_out_complete, + port, port->ep_out->bInterval); + urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + usb_anchor_urb(urb, &port->urbs_out_idle); + } + + port->buffer_length_out = len; + + return 0; +} + +static void xone_wired_free_port(struct xone_wired_port *port) +{ + struct urb *urb; + + if (port->urb_in) { + usb_free_coherent(port->urb_in->dev, port->buffer_length_in, + port->urb_in->transfer_buffer, + port->urb_in->transfer_dma); + usb_free_urb(port->urb_in); + } + + while ((urb = usb_get_from_anchor(&port->urbs_out_idle))) { + usb_free_coherent(urb->dev, port->buffer_length_out, + urb->transfer_buffer, + urb->transfer_dma); + usb_free_urb(urb); + } + + port->intf = NULL; + port->ep_in = NULL; + port->ep_out = NULL; + port->urb_in = NULL; +} + +static int xone_wired_get_buffer(struct gip_adapter *adap, + struct gip_adapter_buffer *buf) +{ + struct xone_wired *xone = dev_get_drvdata(&adap->dev); + struct xone_wired_port *port; + struct urb *urb; + + if (buf->type == GIP_BUF_DATA) + port = &xone->data_port; + else if (buf->type == GIP_BUF_AUDIO) + port = &xone->audio_port; + else + return -EINVAL; + + urb = usb_get_from_anchor(&port->urbs_out_idle); + if (!urb) + return -ENOSPC; + + buf->context = urb; + buf->data = urb->transfer_buffer; + buf->length = port->buffer_length_out; + + return 0; +} + +static int xone_wired_submit_buffer(struct gip_adapter *adap, + struct gip_adapter_buffer *buf) +{ + struct xone_wired *xone = dev_get_drvdata(&adap->dev); + struct xone_wired_port *port; + struct urb *urb = buf->context; + int err; + + if (buf->type == GIP_BUF_DATA) + port = &xone->data_port; + else if (buf->type == GIP_BUF_AUDIO) + port = &xone->audio_port; + else + return -EINVAL; + + urb->transfer_buffer_length = buf->length; + usb_anchor_urb(urb, &port->urbs_out_busy); + + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err) + usb_unanchor_urb(urb); + + usb_free_urb(urb); + + return err; +} + +static int xone_wired_enable_audio(struct gip_adapter *adap) +{ + struct xone_wired *xone = dev_get_drvdata(&adap->dev); + struct xone_wired_port *port = &xone->audio_port; + struct usb_interface *intf; + struct usb_endpoint_descriptor *ep, *ep_in, *ep_out; + int i, err; + + intf = usb_ifnum_to_if(xone->udev, 1); + if (!intf) + return -ENODEV; + + if (intf->cur_altsetting->desc.bAlternateSetting == 1) + return -EALREADY; + + err = usb_set_interface(xone->udev, 1, 1); + if (err) + return err; + + for (i = 0; i < intf->cur_altsetting->desc.bNumEndpoints; i++) { + ep = &intf->cur_altsetting->endpoint[i].desc; + if (usb_endpoint_is_isoc_in(ep)) + ep_in = ep; + else if (usb_endpoint_is_isoc_out(ep)) + ep_out = ep; + } + + if (!ep_in || !ep_out) + return -ENODEV; + + port->intf = intf; + port->ep_in = ep_in; + port->ep_out = ep_out; + + return 0; +} + +static int xone_wired_init_audio_in(struct gip_adapter *adap) +{ + struct xone_wired *xone = dev_get_drvdata(&adap->dev); + struct xone_wired_port *port = &xone->audio_port; + struct urb *urb; + void *buf; + int len, i; + + urb = usb_alloc_urb(XONE_WIRED_NUM_AUDIO_PKTS, GFP_KERNEL); + if (!urb) + return -ENOMEM; + + len = usb_endpoint_maxp(port->ep_in); + buf = usb_alloc_coherent(xone->udev, len * XONE_WIRED_NUM_AUDIO_PKTS, + GFP_KERNEL, &urb->transfer_dma); + if (!buf) + return -ENOMEM; + + urb->dev = xone->udev; + urb->pipe = usb_rcvisocpipe(xone->udev, port->ep_in->bEndpointAddress); + urb->transfer_flags = URB_NO_TRANSFER_DMA_MAP; + urb->transfer_buffer = buf; + urb->transfer_buffer_length = len * XONE_WIRED_NUM_AUDIO_PKTS; + urb->number_of_packets = XONE_WIRED_NUM_AUDIO_PKTS; + urb->interval = port->ep_in->bInterval; + urb->context = xone; + urb->complete = xone_wired_audio_in_complete; + + for (i = 0; i < XONE_WIRED_NUM_AUDIO_PKTS; i++) { + urb->iso_frame_desc[i].offset = i * len; + urb->iso_frame_desc[i].length = len; + } + + port->urb_in = urb; + port->buffer_length_in = len * XONE_WIRED_NUM_AUDIO_PKTS; + + return usb_submit_urb(port->urb_in, GFP_KERNEL); +} + +static int xone_wired_init_audio_out(struct gip_adapter *adap, int pkt_len) +{ + struct xone_wired *xone = dev_get_drvdata(&adap->dev); + struct xone_wired_port *port = &xone->audio_port; + struct urb *urb; + void *buf; + int i, j; + + for (i = 0; i < XONE_WIRED_NUM_AUDIO_URBS; i++) { + urb = usb_alloc_urb(XONE_WIRED_NUM_AUDIO_PKTS, GFP_KERNEL); + if (!urb) + return -ENOMEM; + + buf = usb_alloc_coherent(xone->udev, + pkt_len * XONE_WIRED_NUM_AUDIO_PKTS, + GFP_KERNEL, &urb->transfer_dma); + if (!buf) + return -ENOMEM; + + urb->dev = xone->udev; + urb->pipe = usb_sndisocpipe(xone->udev, port->ep_out->bEndpointAddress); + urb->transfer_flags = URB_NO_TRANSFER_DMA_MAP; + urb->transfer_buffer = buf; + urb->transfer_buffer_length = pkt_len * XONE_WIRED_NUM_AUDIO_PKTS; + urb->number_of_packets = XONE_WIRED_NUM_AUDIO_PKTS; + urb->interval = port->ep_in->bInterval; + urb->context = port; + urb->complete = xone_wired_out_complete; + + for (j = 0; j < XONE_WIRED_NUM_AUDIO_PKTS; j++) { + urb->iso_frame_desc[j].offset = j * pkt_len; + urb->iso_frame_desc[j].length = pkt_len; + } + + usb_anchor_urb(urb, &port->urbs_out_idle); + } + + port->buffer_length_out = pkt_len * XONE_WIRED_NUM_AUDIO_PKTS; + + return 0; +} + +static int xone_wired_disable_audio(struct gip_adapter *adap) +{ + struct xone_wired *xone = dev_get_drvdata(&adap->dev); + struct xone_wired_port *port = &xone->audio_port; + int err; + + usb_kill_urb(port->urb_in); + usb_kill_anchored_urbs(&port->urbs_out_busy); + + err = usb_set_interface(xone->udev, 1, 0); + xone_wired_free_port(port); + + return err; +} + +static struct gip_adapter_ops xone_wired_gip_ops = { + .get_buffer = xone_wired_get_buffer, + .submit_buffer = xone_wired_submit_buffer, + .enable_audio = xone_wired_enable_audio, + .init_audio_in = xone_wired_init_audio_in, + .init_audio_out = xone_wired_init_audio_out, + .disable_audio = xone_wired_disable_audio, +}; + +static int xone_wired_probe(struct usb_interface *intf, + const struct usb_device_id *id) +{ + struct xone_wired *xone; + int err; + + xone = devm_kzalloc(&intf->dev, sizeof(*xone), GFP_KERNEL); + if (!xone) + return -ENOMEM; + + err = usb_find_common_endpoints(intf->cur_altsetting, NULL, NULL, + &xone->data_port.ep_in, &xone->data_port.ep_out); + if (err) + return err; + + xone->udev = interface_to_usbdev(intf); + xone->data_port.intf = intf; + init_usb_anchor(&xone->data_port.urbs_out_idle); + init_usb_anchor(&xone->data_port.urbs_out_busy); + init_usb_anchor(&xone->audio_port.urbs_out_idle); + init_usb_anchor(&xone->audio_port.urbs_out_busy); + + err = xone_wired_init_data_in(xone); + if (err) + goto err_free_port; + + err = xone_wired_init_data_out(xone); + if (err) + goto err_free_port; + + xone->adapter = gip_create_adapter(&intf->dev, + &xone_wired_gip_ops, XONE_WIRED_NUM_AUDIO_PKTS); + if (IS_ERR(xone->adapter)) + goto err_free_port; + + dev_set_drvdata(&xone->adapter->dev, xone); + usb_set_intfdata(intf, xone); + + err = usb_submit_urb(xone->data_port.urb_in, GFP_KERNEL); + if (err) + goto err_destroy_adapter; + + return 0; + +err_destroy_adapter: + gip_destroy_adapter(xone->adapter); +err_free_port: + xone_wired_free_port(&xone->data_port); + + return err; +} + +static void xone_wired_disconnect(struct usb_interface *intf) +{ + struct xone_wired *xone = usb_get_intfdata(intf); + struct xone_wired_port *data = &xone->data_port; + struct xone_wired_port *audio = &xone->audio_port; + + usb_kill_urb(data->urb_in); + usb_kill_urb(audio->urb_in); + + gip_destroy_adapter(xone->adapter); + + usb_kill_anchored_urbs(&data->urbs_out_busy); + usb_kill_anchored_urbs(&audio->urbs_out_busy); + + xone_wired_free_port(data); + xone_wired_free_port(audio); + + usb_set_intfdata(intf, NULL); +} + +static int xone_wired_suspend(struct usb_interface *intf, pm_message_t message) +{ + struct xone_wired *xone = usb_get_intfdata(intf); + struct xone_wired_port *data = &xone->data_port; + struct xone_wired_port *audio = &xone->audio_port; + int err; + + usb_kill_urb(data->urb_in); + usb_kill_urb(audio->urb_in); + + /* 1708 controllers disconnect before suspend */ + /* 1537 controllers power on automatically after resume */ + err = gip_suspend_adapter(xone->adapter); + if (err) + dev_err(&intf->dev, "%s: suspend adapter failed: %d\n", + __func__, err); + + if (!usb_wait_anchor_empty_timeout(&data->urbs_out_busy, 1000)) + usb_kill_anchored_urbs(&data->urbs_out_busy); + + if (!usb_wait_anchor_empty_timeout(&audio->urbs_out_busy, 1000)) + usb_kill_anchored_urbs(&audio->urbs_out_busy); + + return err; +} + +static int xone_wired_resume(struct usb_interface *intf) +{ + struct xone_wired *xone = usb_get_intfdata(intf); + int err; + + err = usb_submit_urb(xone->data_port.urb_in, GFP_KERNEL); + if (err) { + dev_err(&intf->dev, "%s: submit data failed: %d\n", + __func__, err); + return err; + } + + if (xone->audio_port.urb_in) { + err = usb_submit_urb(xone->audio_port.urb_in, GFP_KERNEL); + if (err) + dev_err(&intf->dev, "%s: submit audio failed: %d\n", + __func__, err); + } + + return err; +} + +static const struct usb_device_id xone_wired_id_table[] = { + { XONE_WIRED_VENDOR(0x045e) }, /* Microsoft */ + { XONE_WIRED_VENDOR(0x0738) }, /* Mad Catz */ + { XONE_WIRED_VENDOR(0x0e6f) }, /* PDP */ + { XONE_WIRED_VENDOR(0x0f0d) }, /* Hori */ + { XONE_WIRED_VENDOR(0x1532) }, /* Razer */ + { XONE_WIRED_VENDOR(0x24c6) }, /* PowerA */ + { XONE_WIRED_VENDOR(0x044f) }, /* Thrustmaster */ + { }, +}; + +static struct usb_driver xone_wired_driver = { + .name = "xone-wired", + .probe = xone_wired_probe, + .disconnect = xone_wired_disconnect, + .suspend = xone_wired_suspend, + .resume = xone_wired_resume, + .id_table = xone_wired_id_table, +}; + +module_usb_driver(xone_wired_driver); + +MODULE_DEVICE_TABLE(usb, xone_wired_id_table); +MODULE_AUTHOR("Severin von Wnuck "); +MODULE_DESCRIPTION("xone wired driver"); +MODULE_VERSION("#VERSION#"); +MODULE_LICENSE("GPL"); diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..1f27d84 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +MODULES=$(lsmod | grep '^xone_' | cut -d ' ' -f 1 | tr '\n' ' ') +INSTALLED=$(dkms status xone | tr -s ',:' ' ' | cut -d ' ' -f 2) +SOURCE="/usr/src/xone-$INSTALLED" +BLACKLIST='/etc/modprobe.d/xone-blacklist.conf' + +if [ -n "$MODULES" ]; then + echo "Unloading modules: $MODULES..." + # shellcheck disable=SC2086 + modprobe -r -a $MODULES +fi + +if [ -n "$INSTALLED" ]; then + echo "Uninstalling xone $INSTALLED..." + dkms remove --all "xone/$INSTALLED" + rm -r "$SOURCE" + rm -f "$BLACKLIST" +else + echo 'Driver is not installed!' +fi