├── .gitignore ├── LICENSE ├── README.md ├── cmd └── go-hidproxy │ └── main.go ├── go.mod ├── go.sum ├── hidproxy.go └── hidproxy.service /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-hidproxy 2 | 3 | Proxies Bluetooth keyboards and mouse as HID devices (eg. with Raspberry Zero Pi W) 4 | 5 | ## Build 6 | 7 | Requires `libudev-dev` package (`sudo apt install libudev-dev`). 8 | 9 | Build with Go 1.20+: 10 | 11 | ```sh 12 | go install github.com/rosmo/go-hidproxy/cmd/go-hidproxy@latest 13 | sudo cp ~/go/bin/go-hidproxy /usr/bin/go-hidproxy 14 | ``` 15 | 16 | Or even with a more complete example: 17 | ```sh 18 | wget https://go.dev/dl/go1.22.2.linux-arm64.tar.gz 19 | sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.2.linux-arm64.tar.gz 20 | export PATH=$PATH:/usr/local/go/bin 21 | sudo apt-get install -y libudev-dev 22 | go install github.com/rosmo/go-hidproxy/cmd/go-hidproxy@latest 23 | sudo cp ~/go/bin/go-hidproxy /usr/bin/go-hidproxy 24 | sudo go-hidproxy 25 | ``` 26 | 27 | ## Install 28 | 29 | - Build the binary 30 | - Copy binary to `/usr/sbin/go-hidproxy` 31 | - Install systemd unit file to `/etc/systemd/system` 32 | - Reload daemons: `sudo systemctl daemon-reload` 33 | - Enable hidproxy: `sudo systemctl enable hidproxy` 34 | - (Optionally) Start hidproxy: `sudo systemctl start hidproxy` 35 | 36 | ## Raspberry Pi Zero W setup 37 | 38 | I used a pretty standard Raspbian image: 39 | ``` 40 | Distributor ID: Raspbian 41 | Description: Raspbian GNU/Linux 10 (buster) 42 | Release: 10 43 | Codename: buster 44 | ``` 45 | 46 | You'll need to setup `/boot/config.txt` with: 47 | ```` 48 | dtoverlay=dwc2 49 | ```` 50 | 51 | In `/etc/modules` you should have: 52 | ``` 53 | dwc2 54 | libcomposite 55 | evdev 56 | ``` 57 | 58 | ## Pair Bluetooth keyboard/mouse 59 | 60 | One time pairing: 61 | 62 | ``` 63 | # sudo bluetoothctl 64 | > discoverable on 65 | > pairable on 66 | > agent NoInputNoOutput 67 | > default-agent 68 | > scan on 69 | > pair aa:bb:cc:dd:ee:ff 70 | > connect aa:bb:cc:dd:ee:ff 71 | > trust aa:bb:cc:dd:ee:ff 72 | ``` 73 | 74 | ### Tested with 75 | 76 | - TEX Shinobi keyboard / Trackpoint combo 77 | - Razer Deathadder V2 Pro 78 | - Microsoft Designer Compact Keyboard 79 | -------------------------------------------------------------------------------- /cmd/go-hidproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Go implementation of Bluetooth to USB HID proxy 4 | // Author: Taneli Leppä 5 | // Licensed under Apache License 2.0 6 | 7 | import ( 8 | hidproxy "github.com/rosmo/go-hidproxy" 9 | "flag" 10 | "fmt" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func main() { 15 | logLevelPtr := flag.String("loglevel", "info", "log level (panic, fatal, error, warn, info, debug, trace)") 16 | setupHid := flag.Bool("setuphid", true, "setup HID files on startup") 17 | setupMouse := flag.Bool("mouse", true, "setup mouse(s)") 18 | setupKeyboard := flag.Bool("keyboard", true, "setup keyboard(s)") 19 | monitorUdev := flag.Bool("monitor-udev", true, "monitor udev & BlueZ events for disconnects") 20 | adapterId := flag.String("bluez-adapter", "hci0", "BlueZ adapter (default hci0)") 21 | kbdRepeat := flag.Int("kbdrepeat", 62, "set keyboard repeat rate (default 62)") 22 | kbdDelay := flag.Int("kbddelay", 300, "set keyboard repeat delay in ms (default 300)") 23 | flag.Parse() 24 | 25 | logLevel, err := log.ParseLevel(*logLevelPtr) 26 | if err != nil { 27 | panic(err) 28 | } 29 | fmt.Printf("Set log level: %v\n", logLevel) 30 | log.SetLevel(logLevel) 31 | 32 | hidproxy.Start(hidproxy.Config{ 33 | SetupHid: *setupHid, 34 | SetupMouse: *setupMouse, 35 | SetupKeyboard: *setupKeyboard, 36 | MonitorUdev: *monitorUdev, 37 | AdapterId: *adapterId, 38 | KbdRepeat: *kbdRepeat, 39 | KbdDelay: *kbdDelay, 40 | LogLevel: logLevel, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rosmo/go-hidproxy 2 | 3 | require ( 4 | github.com/gvalkov/golang-evdev v0.0.0-20191114124502-287e62b94bcb 5 | github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 // indirect 6 | github.com/jochenvg/go-udev v0.0.0-20171110120927-d6b62d56d37b 7 | github.com/loov/hrtime v1.0.3 8 | github.com/muka/go-bluetooth v0.0.0-20201211051136-07f31c601d33 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/wk8/go-ordered-map v0.2.0 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 4 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 5 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 6 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 7 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/gvalkov/golang-evdev v0.0.0-20191114124502-287e62b94bcb h1:WHSAxLz3P5t4DKukfJ5wu7+aMyVkuTNSbCiAjVS92sM= 9 | github.com/gvalkov/golang-evdev v0.0.0-20191114124502-287e62b94bcb/go.mod h1:SAzVFKCRezozJTGavF3GX8MBUruETCqzivVLYiywouA= 10 | github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 h1:smvLGU3obGU5kny71BtE/ibR0wIXRUiRFDmSn0Nxz1E= 11 | github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1/go.mod h1:fP/NdyhRVOv09PLRbVXrSqHhrfQypdZwgE2L4h2U5C8= 12 | github.com/jochenvg/go-udev v0.0.0-20171110120927-d6b62d56d37b h1:dgF9Rx3oPIz2d816jKSjnShkJfmtYc/N/DxGDFv2CGk= 13 | github.com/jochenvg/go-udev v0.0.0-20171110120927-d6b62d56d37b/go.mod h1:IBDUGq30U56w969YNPomhMbRje1GrhUsCh7tHdwgLXA= 14 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 15 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/loov/hrtime v1.0.3 h1:LiWKU3B9skJwRPUf0Urs9+0+OE3TxdMuiRPOTwR0gcU= 18 | github.com/loov/hrtime v1.0.3/go.mod h1:yDY3Pwv2izeY4sq7YcPX/dtLwzg5NU1AxWuWxKwd0p0= 19 | github.com/muka/go-bluetooth v0.0.0-20201211051136-07f31c601d33 h1:p3srutpE8TpQmOUQ5Qw94jYFUdoG2jBbILeYLroQNoI= 20 | github.com/muka/go-bluetooth v0.0.0-20201211051136-07f31c601d33/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0= 21 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 22 | github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= 23 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 26 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 27 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 30 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8= 32 | github.com/wk8/go-ordered-map v0.2.0 h1:KlvGyHstD1kkGZkPtHCyCfRYS0cz84uk6rrW/Dnhdtk= 33 | github.com/wk8/go-ordered-map v0.2.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= 34 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 38 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 39 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 40 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 41 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 42 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 48 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE= 51 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 53 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 54 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 55 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 57 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 59 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | -------------------------------------------------------------------------------- /hidproxy.go: -------------------------------------------------------------------------------- 1 | package hidproxy 2 | 3 | // Go implementation of Bluetooth to USB HID proxy 4 | // Author: Taneli Leppä 5 | // Licensed under Apache License 2.0 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | evdev "github.com/gvalkov/golang-evdev" 11 | udev "github.com/jochenvg/go-udev" 12 | "github.com/loov/hrtime" 13 | "github.com/muka/go-bluetooth/api" 14 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 15 | log "github.com/sirupsen/logrus" 16 | orderedmap "github.com/wk8/go-ordered-map" 17 | "io/ioutil" 18 | "os" 19 | "path/filepath" 20 | "strings" 21 | "sync" 22 | "syscall" 23 | "time" 24 | ) 25 | 26 | type Config struct { 27 | SetupHid bool 28 | SetupMouse bool 29 | SetupKeyboard bool 30 | MonitorUdev bool 31 | AdapterId string 32 | KbdRepeat int 33 | KbdDelay int 34 | LogLevel log.Level 35 | } 36 | 37 | type InputDevice struct { 38 | Device string 39 | Name string 40 | } 41 | 42 | type InputMessage struct { 43 | Message []byte 44 | Timestamp time.Duration 45 | } 46 | 47 | var Scancodes = map[uint16]uint16{ 48 | 2: 30, // 1 49 | 3: 31, // 2 50 | 4: 32, // 3 51 | 5: 33, // 4 52 | 6: 34, // 5 53 | 7: 35, // 6 54 | 8: 36, // 7 55 | 9: 37, // 8 56 | 10: 38, // 9 57 | 11: 39, // 0 58 | 57: 44, // space 59 | 14: 42, // bkspc 60 | 28: 40, // enter 61 | 1: 41, // ESC 62 | 106: 79, // RIGHT 63 | 105: 80, // LEFT 64 | 108: 81, // DOWN 65 | 103: 82, // UP 66 | 59: 58, // F1 67 | 60: 59, // F2 68 | 61: 60, // F3 69 | 62: 61, // F4 70 | 63: 62, // F5 71 | 64: 63, // F6 72 | 65: 64, // F7 73 | 66: 65, // F8 74 | 67: 66, // F9 75 | 68: 67, // F10 76 | 69: 68, // F11 77 | 70: 69, // F12 78 | 12: 45, // - 79 | 13: 46, // = 80 | 15: 43, // TAB 81 | 26: 47, // { 82 | 27: 48, // ] 83 | 39: 51, // : 84 | 40: 52, // " 85 | 51: 54, // < 86 | 52: 55, // > 87 | 53: 56, // ? 88 | 41: 50, // // 89 | 43: 49, // \ 90 | 30: 4, // a 91 | 48: 5, // b 92 | 46: 6, // c 93 | 32: 7, // d 94 | 18: 8, // e 95 | 33: 9, // f 96 | 34: 10, // g 97 | 35: 11, // h 98 | 23: 12, // i 99 | 36: 13, // j 100 | 37: 14, // k 101 | 38: 15, // l 102 | 50: 16, // m 103 | 49: 17, // n 104 | 24: 18, // o 105 | 25: 19, // p 106 | 16: 20, // q 107 | 19: 21, // r 108 | 31: 22, // s 109 | 20: 23, // t 110 | 22: 24, // u 111 | 47: 25, // v 112 | 17: 26, // w 113 | 45: 27, // x 114 | 21: 28, // y 115 | 44: 29, // z 116 | 86: 49, // | & \ 117 | 104: 75, // PgUp 118 | 109: 78, // PgDn 119 | 102: 74, // Home 120 | 107: 77, // End 121 | 110: 73, // Insert 122 | 119: 72, // Pause 123 | //70: 71, // ScrLk 124 | 99: 70, // PrtSc 125 | 87: 68, // F11 126 | 88: 69, // F12 127 | 113: 127, // Mute 128 | 114: 129, // VolDn 129 | 115: 128, // VolUp 130 | 58: 57, // CapsLock (non-locking) 131 | 158: 122, // "Undo" (Thinkpad special key) 132 | 159: 121, // "Again" (Thinkpad special key) 133 | 29: 224, // Left-Ctrl 134 | 125: 227, // Left-Cmd 135 | 42: 225, // Left-Shift 136 | 56: 226, // Left-Alt 137 | 100: 230, // AltGr (Right-Alt) 138 | 127: 231, // Right-Cmd 139 | 97: 228, // Right-Ctrl 140 | 54: 229, // Right-Shift 141 | 111: 76, // Delete 142 | 164: 232, // Play-Pause 143 | 165: 234, // Previous-Track 144 | 163: 233, // Next-Track 145 | } 146 | 147 | const ( 148 | RIGHT_META = 1 << 7 149 | RIGHT_ALT = 1 << 6 150 | RIGHT_SHIFT = 1 << 5 151 | RIGHT_CONTROL = 1 << 4 152 | LEFT_META = 1 << 3 153 | LEFT_ALT = 1 << 2 154 | LEFT_SHIFT = 1 << 1 155 | LEFT_CONTROL = 1 << 0 156 | 157 | BUTTON_LEFT = 1 << 0 158 | BUTTON_RIGHT = 1 << 1 159 | BUTTON_MIDDLE = 1 << 2 160 | ) 161 | 162 | func SetupUSBGadget() { 163 | var paths = []string{ 164 | "/sys/kernel/config/usb_gadget/piproxy", 165 | "/sys/kernel/config/usb_gadget/piproxy/strings/0x409", 166 | "/sys/kernel/config/usb_gadget/piproxy/configs/c.1/strings/0x409", 167 | "/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb0", 168 | "/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb1", 169 | } 170 | filesStr := orderedmap.New() 171 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/idVendor", "0x1d6b") 172 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/idProduct", "0x0104") 173 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/bcdDevice", "0x0100") 174 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/bcdUSB", "0x0200") 175 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/strings/0x409/serialnumber", "fedcba9876543210") 176 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/strings/0x409/manufacturer", "Raspberry Pi") 177 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/strings/0x409/product", "pizero keyboard Device") 178 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/configs/c.1/strings/0x409/configuration", "Config 1: ECM network") 179 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/configs/c.1/MaxPower", "250") 180 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb0/protocol", "1") 181 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb0/subclass", "1") 182 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb0/report_length", "8") 183 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb1/protocol", "2") 184 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb1/subclass", "1") 185 | filesStr.Set("/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb1/report_length", "4") 186 | var filesBytes = map[string][]byte{ 187 | "/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb0/report_desc": []byte{0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x03, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x03, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xc0}, 188 | "/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb1/report_desc": []byte{0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0}, 189 | } 190 | var symlinks = map[string]string{ 191 | "/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb0": "/sys/kernel/config/usb_gadget/piproxy/configs/c.1/hid.usb0", 192 | "/sys/kernel/config/usb_gadget/piproxy/functions/hid.usb1": "/sys/kernel/config/usb_gadget/piproxy/configs/c.1/hid.usb1", 193 | } 194 | 195 | for _, path := range paths { 196 | if _, err := os.Stat(path); os.IsNotExist(err) { 197 | log.Debugf("Creating directory: %s", path) 198 | err := os.MkdirAll(path, os.ModeDir) 199 | if err != nil { 200 | log.Fatalf("Failed to create directory path: %s", path) 201 | } 202 | } 203 | } 204 | 205 | for pair := filesStr.Oldest(); pair != nil; pair = pair.Next() { 206 | content, err := ioutil.ReadFile(pair.Key.(string)) 207 | if err == nil { 208 | if bytes.Compare(content[0:len(content)-1], []byte(pair.Value.(string))) == 0 { 209 | continue 210 | } 211 | } 212 | 213 | log.Debugf("Writing file: %s", pair.Key.(string)) 214 | err = ioutil.WriteFile(pair.Key.(string), []byte(pair.Value.(string)), os.FileMode(0644)) 215 | if err != nil { 216 | log.Warnf("Failed to write file: %s (maybe already set up)", pair.Key.(string)) 217 | } 218 | } 219 | 220 | for file, contents := range filesBytes { 221 | content, err := ioutil.ReadFile(file) 222 | if err == nil { 223 | if bytes.Compare(content, contents) == 0 { 224 | continue 225 | } 226 | } 227 | log.Debugf("Writing file: %s", file) 228 | err = ioutil.WriteFile(file, contents, os.FileMode(0644)) 229 | if err != nil { 230 | log.Warnf("Failed to create file: %s (maybe already set up)", file) 231 | } 232 | } 233 | 234 | for source, target := range symlinks { 235 | if _, err := os.Stat(target); os.IsNotExist(err) { 236 | log.Debugf("Creating symlink from %s to: %s", source, target) 237 | err := os.Symlink(source, target) 238 | if err != nil { 239 | log.Fatalf("Failed to create symlink %s -> %s", source, target) 240 | } 241 | } 242 | } 243 | 244 | time.Sleep(1000 * time.Millisecond) 245 | 246 | matches, err := filepath.Glob("/sys/class/udc/*") 247 | if err != nil { 248 | log.Fatalf("Failed to list files in /sys/class/udc: %s", err.Error()) 249 | } 250 | var udcFile string = "/sys/kernel/config/usb_gadget/piproxy/UDC" 251 | var udc string = "" 252 | for _, match := range matches { 253 | udc = udc + filepath.Base(match) + " " 254 | } 255 | content, err := ioutil.ReadFile(udcFile) 256 | if err == nil { 257 | if bytes.Compare(content[0:len(content)-1], []byte(strings.TrimSpace(udc))) != 0 { 258 | err = ioutil.WriteFile(udcFile, []byte(strings.TrimSpace(udc)), os.FileMode(0644)) 259 | if err != nil { 260 | log.Warnf("Failed to create file %s: %s: (%s)", udcFile, udc, err.Error()) 261 | } 262 | } 263 | } 264 | // Give it a second to settle 265 | time.Sleep(1000 * time.Millisecond) 266 | } 267 | 268 | func HandleKeyboard(output chan<- error, input chan<- InputMessage, close <-chan bool, rate uint, delay uint, dev evdev.InputDevice) error { 269 | keysDown := make([]uint16, 0) 270 | err := dev.Grab() 271 | if err != nil { 272 | log.Fatal(err) 273 | output <- err 274 | return err 275 | } 276 | defer dev.Release() 277 | 278 | log.Infof("Grabbed keyboard-like device: %s (%s)", dev.Name, dev.Fn) 279 | syscall.SetNonblock(int(dev.File.Fd()), true) 280 | 281 | log.Infof("Setting repeat rate to %d, delay %d for %s (%s)", rate, delay, dev.Name, dev.Fn) 282 | dev.SetRepeatRate(rate, delay) 283 | 284 | loop := 0 285 | for { 286 | err = dev.File.SetReadDeadline(time.Now().Add(250 * time.Millisecond)) 287 | if err != nil { 288 | log.Fatal(err) 289 | output <- err 290 | return err 291 | } 292 | 293 | event, err := dev.ReadOne() 294 | if err != nil && strings.Contains(err.Error(), "i/o timeout") { 295 | continue 296 | } 297 | if err != nil { 298 | log.Fatal(err) 299 | output <- err 300 | return err 301 | } 302 | log.Debugf("Keyboard input event: type=%d, code=%d, value=%d", event.Type, event.Code, event.Value) 303 | if event.Type == evdev.EV_KEY { 304 | keyEvent := evdev.NewKeyEvent(event) 305 | log.Debugf("Key event: scancode=%d, keycode=%d, state=%d", keyEvent.Scancode, keyEvent.Keycode, keyEvent.State) 306 | if keyCode, ok := Scancodes[keyEvent.Scancode]; ok { 307 | if keyEvent.State == 1 { // Key down 308 | keyIsDown := false 309 | for _, k := range keysDown { 310 | if k == keyCode { 311 | keyIsDown = true 312 | } 313 | } 314 | if !keyIsDown { 315 | keysDown = append(keysDown, keyCode) 316 | } 317 | } 318 | if keyEvent.State == 0 { // Key up 319 | newKeysDown := make([]uint16, 0) 320 | for _, k := range keysDown { 321 | if k != keyCode { 322 | newKeysDown = append(newKeysDown, k) 323 | } 324 | } 325 | keysDown = newKeysDown 326 | } 327 | 328 | var modifiers uint8 = 0 329 | keysToSend := make([]uint8, 0) 330 | for _, k := range keysDown { 331 | switch { 332 | case k == 224: // Left-Ctrl 333 | modifiers |= LEFT_CONTROL 334 | case k == 227: // Left-Cmd 335 | modifiers |= LEFT_META 336 | case k == 225: // Left-Shift 337 | modifiers |= LEFT_SHIFT 338 | case k == 226: // Left-Alt 339 | modifiers |= LEFT_ALT 340 | case k == 228: // Right-Ctrl 341 | modifiers |= RIGHT_CONTROL 342 | case k == 231: // Right-Cmd 343 | modifiers |= RIGHT_META 344 | case k == 229: // Right-Shift 345 | modifiers |= RIGHT_SHIFT 346 | case k == 230: // Right-Alt 347 | modifiers |= RIGHT_ALT 348 | default: 349 | keysToSend = append(keysToSend, uint8(k)) 350 | } 351 | } 352 | keysToSend = append([]uint8{modifiers, 0}, keysToSend...) 353 | if len(keysToSend) < 8 { 354 | for i := len(keysToSend); i < 8; i++ { 355 | keysToSend = append(keysToSend, uint8(0)) 356 | } 357 | } 358 | input <- InputMessage{ 359 | Timestamp: hrtime.Now(), 360 | Message: keysToSend, 361 | } 362 | 363 | log.Debugf("Key status (scancode %d, keycode %d): %v\n", keyEvent.Scancode, keyCode, keysToSend) 364 | } else { 365 | log.Warnf("Unknown scancode: %d\n", keyEvent.Scancode) 366 | } 367 | } 368 | loop += 1 369 | if loop > 3 { 370 | select { 371 | case _ = <-close: 372 | log.Infof("Stopping processing keyboard input from: %s (%s)", dev.Name, dev.Fn) 373 | output <- nil 374 | return nil 375 | default: 376 | } 377 | loop = 0 378 | } 379 | } 380 | 381 | output <- nil 382 | return nil 383 | } 384 | 385 | func HandleMouse(output chan<- error, input chan<- InputMessage, close <-chan bool, dev evdev.InputDevice) error { 386 | err := dev.Grab() 387 | if err != nil { 388 | log.Fatal(err) 389 | output <- err 390 | return err 391 | } 392 | defer dev.Release() 393 | 394 | log.Infof("Grabbed mouse-like device: %s (%s)", dev.Name, dev.Fn) 395 | syscall.SetNonblock(int(dev.File.Fd()), true) 396 | 397 | loop := 0 398 | var buttons uint8 = 0x0 399 | for { 400 | err = dev.File.SetReadDeadline(time.Now().Add(250 * time.Millisecond)) 401 | if err != nil { 402 | log.Fatal(err) 403 | output <- err 404 | return err 405 | } 406 | 407 | event, err := dev.ReadOne() 408 | if err != nil && strings.Contains(err.Error(), "i/o timeout") { 409 | continue 410 | } 411 | if err != nil { 412 | log.Fatal(err) 413 | output <- err 414 | return err 415 | } 416 | log.Debugf("Mouse input event: type=%d, code=%d, value=%d", event.Type, event.Code, event.Value) 417 | var buttonOp bool = false 418 | if event.Type == evdev.EV_KEY { 419 | if event.Code == 272 { 420 | if event.Value > 0 { 421 | buttons |= BUTTON_LEFT 422 | } else { 423 | buttons &= ^uint8(BUTTON_LEFT) 424 | } 425 | buttonOp = true 426 | } 427 | if event.Code == 273 { 428 | if event.Value > 0 { 429 | buttons |= BUTTON_RIGHT 430 | } else { 431 | buttons &= ^uint8(BUTTON_RIGHT) 432 | } 433 | buttonOp = true 434 | } 435 | if event.Code == 274 { 436 | if event.Value > 0 { 437 | buttons |= BUTTON_MIDDLE 438 | } else { 439 | buttons &= ^uint8(BUTTON_MIDDLE) 440 | } 441 | buttonOp = true 442 | } 443 | } 444 | if event.Type == evdev.EV_REL || buttonOp { 445 | mouseToSend := make([]uint8, 0) 446 | mouseToSend = append(mouseToSend, buttons) 447 | if event.Type == evdev.EV_REL { 448 | if event.Code == 0 { 449 | mouseToSend = append(mouseToSend, uint8(event.Value)) 450 | mouseToSend = append(mouseToSend, 0x00) 451 | mouseToSend = append(mouseToSend, 0x00) 452 | } 453 | if event.Code == 1 { 454 | mouseToSend = append(mouseToSend, 0x00) 455 | mouseToSend = append(mouseToSend, uint8(event.Value)) 456 | mouseToSend = append(mouseToSend, 0x00) 457 | } 458 | if event.Code == 11 { 459 | mouseToSend = append(mouseToSend, 0x00) 460 | mouseToSend = append(mouseToSend, 0x00) 461 | mouseToSend = append(mouseToSend, uint8(event.Value)) 462 | } 463 | } else { 464 | mouseToSend = append(mouseToSend, 0x00) 465 | mouseToSend = append(mouseToSend, 0x00) 466 | mouseToSend = append(mouseToSend, 0x00) 467 | } 468 | input <- InputMessage{ 469 | Timestamp: hrtime.Now(), 470 | Message: mouseToSend, 471 | } 472 | } 473 | loop += 1 474 | if loop > 3 { 475 | select { 476 | case _ = <-close: 477 | log.Infof("Stopping processing mouse input from: %s (%s)", dev.Name, dev.Fn) 478 | output <- nil 479 | return nil 480 | default: 481 | } 482 | loop = 0 483 | } 484 | } 485 | 486 | output <- nil 487 | return nil 488 | 489 | } 490 | 491 | func SendKeyboardReports(input <-chan InputMessage) error { 492 | log.Info("Opening keyboard /dev/hidg0 for writing...") 493 | file, err := os.OpenFile("/dev/hidg0", os.O_APPEND|os.O_WRONLY, 0600) 494 | if err != nil { 495 | log.Warn("Error opening /dev/hidg0, are you running as root?") 496 | log.Fatal(err) 497 | return err 498 | } 499 | defer file.Close() 500 | 501 | var avg, min, max, loop int64 = 0, 0, 0, 0 502 | for { 503 | msg := <-input 504 | bytesWritten, err := file.Write(msg.Message) 505 | if err != nil { 506 | log.Fatal(err) 507 | return err 508 | } 509 | latency := hrtime.Since(msg.Timestamp).Nanoseconds() 510 | if latency < min { 511 | min = latency 512 | } 513 | if latency > max { 514 | max = latency 515 | } 516 | avg = (avg + latency) / 2 517 | loop += 1 518 | if loop > 50 { 519 | log.Debugf("Latency: now=%d, avg=%d, min=%d, max=%d μs", latency/1000, avg/1000, min/1000, max/1000) 520 | loop = 0 521 | } 522 | 523 | log.Debugf("Wrote %d bytes to /dev/hidg0 (%v)", bytesWritten, msg) 524 | } 525 | return nil 526 | } 527 | 528 | func SendMouseReports(input <-chan InputMessage) error { 529 | log.Info("Opening keyboard /dev/hidg1 for writing...") 530 | file, err := os.OpenFile("/dev/hidg1", os.O_APPEND|os.O_WRONLY, 0600) 531 | if err != nil { 532 | log.Warn("Error opening /dev/hidg1, are you running as root?") 533 | log.Fatal(err) 534 | return err 535 | } 536 | defer file.Close() 537 | 538 | var avg, min, max, loop int64 = 0, 0, 0, 0 539 | for { 540 | msg := <-input 541 | bytesWritten, err := file.Write(msg.Message) 542 | if err != nil { 543 | log.Fatal(err) 544 | return err 545 | } 546 | log.Debugf("Wrote %d bytes to /dev/hidg1 (%v)", bytesWritten, msg) 547 | latency := hrtime.Since(msg.Timestamp).Nanoseconds() 548 | if latency < min { 549 | min = latency 550 | } 551 | if latency > max { 552 | max = latency 553 | } 554 | avg = (avg + latency) / 2 555 | loop += 1 556 | if loop > 100 { 557 | log.Debugf("Latency: now=%d, avg=%d, min=%d, max=%d μs", latency/1000, avg/1000, min/1000, max/1000) 558 | loop = 0 559 | } 560 | } 561 | return nil 562 | } 563 | 564 | func GetDisconnectedDevices(adapterId string) ([]string, error) { 565 | log.Debugf("Getting adapter: %s", adapterId) 566 | a, err := adapter.GetAdapter(adapterId) 567 | if err != nil { 568 | return nil, err 569 | } 570 | 571 | log.Debugf("Getting devices from adapter: %s", adapterId) 572 | devices, err := a.GetDevices() 573 | if err != nil { 574 | return nil, err 575 | } 576 | 577 | disconnected := make([]string, 0) 578 | connected := make([]string, 0) 579 | for _, dev := range devices { 580 | address, err := dev.GetAddress() 581 | if err != nil { 582 | continue 583 | } 584 | name, err := dev.GetName() 585 | if err != nil { 586 | name = "?" 587 | } 588 | 589 | log.Infof("Checking if device %s (%s) is connected...", name, address) 590 | deviceConnected, err := dev.GetConnected() 591 | if err == nil { 592 | if !deviceConnected { 593 | log.Infof("Device %s is disconnected.", name) 594 | disconnected = append(disconnected, name) 595 | } else { 596 | log.Infof("Device %s is still connected.", name) 597 | connected = append(connected, name) 598 | } 599 | } 600 | } 601 | results := make([]string, 0) 602 | for _, dname := range disconnected { 603 | ok := true 604 | for _, cname := range connected { 605 | if cname == dname { 606 | ok = false 607 | break 608 | } 609 | } 610 | if ok { 611 | inResults := false 612 | for _, rname := range results { 613 | if rname == dname { 614 | inResults = true 615 | } 616 | } 617 | if !inResults { 618 | results = append(results, dname) 619 | } 620 | } 621 | } 622 | return results, nil 623 | } 624 | 625 | func Start(config Config) { 626 | var wg sync.WaitGroup 627 | 628 | log.SetLevel(config.LogLevel) 629 | 630 | if config.SetupHid { 631 | log.Info("Setting up HID files...") 632 | SetupUSBGadget() 633 | } 634 | 635 | keyboardInput := make(chan InputMessage, 10) 636 | mouseInput := make(chan InputMessage, 100) 637 | output := make(map[InputDevice]chan error, 0) 638 | close := make(map[InputDevice]chan bool, 0) 639 | 640 | var udevCh <-chan *udev.Device 641 | var cancel context.CancelFunc 642 | var ctx context.Context 643 | 644 | defer api.Exit() 645 | u := udev.Udev{} 646 | if config.MonitorUdev { 647 | log.Info("Starting udev monitoring for Bluetooth devices") 648 | m := u.NewMonitorFromNetlink("udev") 649 | m.FilterAddMatchSubsystem("bluetooth") 650 | 651 | ctx, cancel = context.WithCancel(context.Background()) 652 | udevCh, _ = m.DeviceChan(ctx) 653 | } 654 | 655 | go SendKeyboardReports(keyboardInput) 656 | go SendMouseReports(mouseInput) 657 | wg.Add(1) 658 | for { 659 | select { 660 | case d := <-udevCh: 661 | if d.Action() == "add" || d.Action() == "remove" { 662 | disconnected, err := GetDisconnectedDevices(config.AdapterId) 663 | if err != nil { 664 | log.Errorf("Error checking disconnected devices: %s", err.Error()) 665 | } else { 666 | for _, device := range disconnected { 667 | for devId, _ := range output { 668 | if strings.HasPrefix(devId.Name, device) { 669 | log.Infof("Disconnected device, stopping listening to: %s (%s)", devId.Name, devId.Device) 670 | select { 671 | case close[devId] <- true: 672 | log.Infof("Sent stop signal to: %s (%s)", devId.Name, devId.Device) 673 | default: 674 | } 675 | 676 | } 677 | } 678 | } 679 | } 680 | } 681 | default: 682 | } 683 | 684 | log.Info("Polling for new devices in /dev/input\n") 685 | devices, _ := evdev.ListInputDevices() 686 | for _, dev := range devices { 687 | isMouse := false 688 | isKeyboard := false 689 | for k := range dev.Capabilities { 690 | if k.Name == "EV_REL" { 691 | isMouse = true 692 | } 693 | if k.Name == "EV_KEY" { 694 | isKeyboard = true 695 | } 696 | } 697 | log.Debugf("Device %s (%s), capabilities: %v (mouse=%t, kbd=%t)", dev.Name, dev.Fn, dev.Capabilities, isMouse, isKeyboard) 698 | if isKeyboard || isMouse { 699 | devId := InputDevice{ 700 | Device: dev.Fn, 701 | Name: dev.Name, 702 | } 703 | if _, ok := output[devId]; !ok { 704 | output[devId] = make(chan error, 10) 705 | close[devId] = make(chan bool, 10) 706 | if isKeyboard && !isMouse && config.SetupKeyboard { 707 | go HandleKeyboard(output[devId], keyboardInput, close[devId], uint(config.KbdRepeat), uint(config.KbdDelay), *dev) 708 | wg.Add(1) 709 | } 710 | log.Debugf("isKeyboard: %t, isMouse: %t, setupMouse: %t", !isKeyboard, isMouse, config.SetupMouse) 711 | if isMouse && config.SetupMouse { 712 | go HandleMouse(output[devId], mouseInput, close[devId], *dev) 713 | wg.Add(1) 714 | } 715 | } 716 | } 717 | } 718 | time.Sleep(1000 * time.Millisecond) 719 | for id, eventOutput := range output { 720 | select { 721 | case msg := <-eventOutput: 722 | if msg == nil { 723 | log.Warnf("Event handler quit: %s", id.Device) 724 | } else { 725 | log.Errorf("Received error from %s: %s", id.Device, msg.Error()) 726 | } 727 | delete(output, id) 728 | wg.Done() 729 | default: 730 | } 731 | } 732 | } 733 | cancel() 734 | } 735 | -------------------------------------------------------------------------------- /hidproxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=HID proxy for Bluetooth 3 | After=bluetooth.target 4 | 5 | [Service] 6 | ExecStartPre=/usr/bin/sleep 20 7 | ExecStart=/usr/sbin/go-hidproxy 8 | Restart=always 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | --------------------------------------------------------------------------------