├── .gitignore ├── usbmon-setperms ├── NEWS ├── udev └── 42-logitech-unify-permissions.rules ├── Makefile ├── README.txt ├── shell ├── usbmon.awk ├── keyboard.txt ├── read-dev-usbmon.c ├── hidpp20.c ├── hidraw.c ├── notes.txt ├── registers.txt └── ltunify.c /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw* 2 | read-dev-usbmon 3 | hidraw 4 | ltunify 5 | -------------------------------------------------------------------------------- /usbmon-setperms: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # helper for allowing the developer to read usbmon as non-root. 3 | 4 | if [ "$USER" = "root" ]; then 5 | echo "Please run as regular user or set USER" 6 | exit 1 7 | fi 8 | 9 | GROUP="$(id -g)" 10 | if [ $GROUP = 0 ]; then 11 | GROUP="$(id -g "$USER")" 12 | fi 13 | 14 | if [ ! -e /dev/usbmon0 ]; then 15 | echo "Attempting to modprobe usbmon..." 16 | sudo modprobe -v usbmon 17 | if [ ! -e /dev/usbmon0 ]; then 18 | echo "Cannot locate 'usbmon' kernel module" 19 | exit 1 20 | fi 21 | fi 22 | 23 | usbmons=$(lsusb -d046d: | cut -c7 | sed 's,^,/dev/usbmon,' | sort -u) 24 | 25 | echo Found devices: $usbmons 26 | 27 | usbmonsw= 28 | for dev in $usbmons; do 29 | if [ ! -r "$dev" ]; then 30 | usbmonsw="$usbmonsw $dev" 31 | fi 32 | done 33 | 34 | if [ -n "$usbmonsw" ]; then 35 | echo "Attempting to make $usbmonsw readable" 36 | sudo chgrp -v $GROUP $usbmonsw 37 | sudo chmod -v g+r $usbmonsw 38 | else 39 | echo "No devices found" 40 | fi 41 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | Version 0.3 - 14 June 2020 2 | 3 | * ltunify: fix device version reporting for HID++ 1.0 devices. 4 | * ltunify: ignore DJ reports that resulted in messages such as "Dev conn notif 5 | is expected to be short, got 0x20 instead". 6 | * ltunify: report some more HID++ 2.0 feature names. 7 | * ltunify: fix failures when another event (such as touchpad motions) occur 8 | between sending the command and reading the response. 9 | * ltunify: fix crash on unrecognised command-line parameter. 10 | * ltunify: add --version option. 11 | * ltunify: add support for Nano Receiver c534 as used by the MK270 combo. 12 | 13 | Version 0.2 - 23 July 2013 14 | 15 | * ltunify: display HID++ version and supported HID++ 2.0 features for `info` 16 | option. 17 | * ltunify: print HID++ version of receiver. 18 | * ltunify: support c52f Nano receiver. 19 | * udev: use uaccess to allow seated users to login without joining a group. 20 | * Add various installation targets and variables to Makefile. 21 | * Updated registers.txt and monitoring tools with battery and keyboard 22 | information. 23 | 24 | Version 0.1 - 25 April 2013 25 | 26 | * Initial release. 27 | -------------------------------------------------------------------------------- /udev/42-logitech-unify-permissions.rules: -------------------------------------------------------------------------------- 1 | # Allows non-root users to have raw access the Logitech Unifying USB Receiver 2 | # device. For development purposes, allowing users to write to the receiver is 3 | # potentially dangerous (e.g. perform firmware updates). 4 | 5 | # skip actual unified devices, only consider the receiver 6 | DRIVERS=="logitech-djdevice", GOTO="not_unify_recv" 7 | 8 | # official Unifying receivers 9 | SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52b", GOTO="unify_dev" 10 | SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c532", GOTO="unify_dev" 11 | 12 | # "Unifying Ready" Nano receiver 13 | SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52f", GOTO="unify_dev" 14 | 15 | # Nano receiver for devices such as the MK270 mouse and keyboard combo 16 | SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c534", GOTO="unify_dev" 17 | 18 | GOTO="not_unify_recv" 19 | 20 | LABEL="unify_dev" 21 | # Allow any seated user to access the receiver. 22 | # uaccess: modern ACL-enabled udev 23 | # udev-acl: for Ubuntu 12.10 and older 24 | TAG+="uaccess", TAG+="udev-acl" 25 | 26 | # Grant members of the "plugdev" group access to receiver (useful for SSH users) 27 | #MODE="0660", GROUP="plugdev" 28 | 29 | LABEL="not_unify_recv" 30 | # vim: ft=udevrules 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS ?= -g -O2 -Wall -Wextra -D_FORTIFY_SOURCE=2 -fstack-protector --param ssp-buffer-size=4 2 | # for install-home 3 | BINDIR ?= $(HOME)/bin 4 | 5 | # for install and uninstall 6 | DESTDIR ?= 7 | bindir ?= /usr/local/bin 8 | udevrulesdir ?= /etc/udev/rules.d 9 | 10 | udevrule = 42-logitech-unify-permissions.rules 11 | 12 | PACKAGE_VERSION ?= $(shell git describe --dirty 2>/dev/null | sed s/^v//) 13 | ifeq (PACKAGE_VERSION, "") 14 | LTUNIFY_DEFINES := 15 | else 16 | LTUNIFY_DEFINES := -DPACKAGE_VERSION=\"$(PACKAGE_VERSION)\" 17 | endif 18 | 19 | %: %.c 20 | $(CC) $(CFLAGS) -o $(OUTDIR)$@ $< 21 | 22 | all: ltunify read-dev-usbmon 23 | 24 | read-dev-usbmon: read-dev-usbmon.c hidraw.c 25 | 26 | ltunify: ltunify.c hidpp20.c 27 | $(CC) $(CFLAGS) -o $(OUTDIR)$@ $< -lrt $(LTUNIFY_DEFINES) 28 | 29 | .PHONY: all clean install-home install install-udevrule uninstall 30 | clean: 31 | rm -f ltunify read-dev-usbmon hidraw 32 | 33 | install-home: ltunify 34 | install -m755 -D ltunify $(BINDIR)/ltunify 35 | 36 | # System-wide installation 37 | install: ltunify install-udevrule 38 | install -m755 -D ltunify $(DESTDIR)$(bindir)/ltunify 39 | 40 | install-udevrule: udev/$(udevrule) 41 | install -m644 -D udev/$(udevrule) $(DESTDIR)$(udevrulesdir)/$(udevrule) 42 | 43 | uninstall: 44 | $(RM) $(DESTDIR)$(bindir)/ltunify $(DESTDIR)$(udevrulesdir)/$(udevrule) 45 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Logitech Unifying tool for Linux 2 | 3 | See also the article on 4 | 5 | Logitech documents 6 | 7 | I have learned a bit from the kernel source code hid-logitech-dj, but the 8 | "official" Logitech specification (HID++ 1.0) was much more useful. These 9 | documents can be found on . 10 | 11 | 12 | Debuggers 13 | usbmon.awk - initial debugging tool used for tapping usbmon from debugfs 14 | hidraw.c - successor of usbmon.awk that can parse packets of usb payload. 15 | read-dev-usbmon.c - Reads data from /dev/usbmonX and show interpreted data in a 16 | more human-readable way. 17 | 18 | Note: as a quick-n-dirty hack, I included hidraw.c at some point into the 19 | read-dev-usbmon program. Otherwise, I had no way to show the difference between 20 | a send or receive packet without adding to the same stdout stream. If I included 21 | it in the stderr pipe, then it would be interleaved with stdout in an 22 | unpredictable manner. This means that hidraw.c is currently unusable, it does 23 | not process data correctly. 24 | 25 | Usage of USB debugger: 26 | 1. Use `lsusb -d 046d:c52b` to determine the bus number. If the output is "Bus 27 | 001 ..", your usb monitor device is at /dev/usbmon1. 28 | 2. sudo chgrp $USER /dev/usbmon1 29 | 3. sudo chmod g+r /dev/usbmon1 30 | 4. ./read-dev-usbmon /dev/usbmon1 31 | 5. Profit! 32 | 33 | 34 | Pairing tool (ltunify) 35 | ltunify allows you to pair new devices, unpair existing devices or view 36 | information for those devices. In order to build the ltunify binary and install 37 | it to `$HOME/bin/ltunify`: 38 | 39 | make ltunify 40 | make install-home 41 | 42 | If you intend to package ltunify or otherwise install it system-wide with a 43 | udevrule, you can use: 44 | 45 | make ltunify 46 | make install DESTDIR=$pkgdir bindir=/usr/bin udevrulesdir=/lib/udev/rules.d 47 | 48 | Once installed, run `ltunify --help` for available options. 49 | 50 | Usage of the pairing tool is pretty straight-forward. Example session: 51 | 52 | $ ./ltunify list 53 | /dev/hidraw0: Permission denied 54 | Logitech Unifying Receiver device is not accessible. 55 | Try running this program as root or enable read/write permissions 56 | for /dev/hidraw0 57 | $ sudo chgrp $USER /dev/hidraw0 && sudo chmod g+rw /dev/hidraw0 58 | $ ./ltunify list 59 | Devices count: 1 60 | Connected devices: 61 | idx=1 Mouse M525 62 | $ ./ltunify info 1 63 | Device index 1 64 | Mouse 65 | Name: M525 66 | Wireless Product ID: 4013 67 | Serial number: DAFA335E 68 | Device was unavailable, version information not available. 69 | $ ./ltunify unpair 1 70 | Device 0x01 Mouse successfully unpaired 71 | $ ./ltunify list 72 | Devices count: 0 73 | Connected devices: 74 | $ ./ltunify pair 75 | Please turn your wireless device off and on to start pairing. 76 | Found new device, id=0x01 Mouse 77 | $ ./ltunify list 78 | Devices count: 1 79 | Connected devices: 80 | idx=1 Mouse M525 81 | 82 | TODO 83 | - organize code in multiple files 84 | - simplify code 85 | - HID++ 2.0 debugging (transparent if possible) 86 | 87 | ~ Peter Wu 88 | -------------------------------------------------------------------------------- /shell: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Utilities for HID++ accesses. 3 | # 4 | # Author: Peter Wu 5 | 6 | hidw() { 7 | local hiddev arg bytes fillchar length oIFS actualArgs 8 | hiddev=$1; shift 9 | bytes=() 10 | 11 | case $hiddev in 12 | ''|-h|--help|-?) 13 | cat < $length" 93 | return 1 94 | fi 95 | fi 96 | 97 | echo "${bytes[@]}" | xxd -ps -r > "$hiddev" 98 | } 99 | 100 | _hidpp_main() { 101 | local cmd 102 | local cmds=(hidw) 103 | cmd=$1; shift 104 | 105 | for c in "${cmds[@]}"; do 106 | if [[ $cmd == $c ]]; then 107 | $cmd "$@" 108 | return 109 | fi 110 | done 111 | 112 | case $cmd in 113 | ''|-h|--help|-?) 114 | cat < 7 | # Date: 2013-04-04 8 | 9 | BEGIN { 10 | OFS=" "; 11 | # Taken from Linux source, drivers/hid/hid-logitech-dj.h 12 | # Catgegories are taken from patent description of US8386651 13 | # 0x00 - 0x3F HID reports 14 | types["01"] = "KEYBOARD"; 15 | types["02"] = "MOUSE"; 16 | types["03"] = "CONSUMER_CONTROL"; 17 | types["04"] = "SYSTEM_CONTROL"; 18 | 19 | types["08"] = "MEDIA_CENTER"; 20 | 21 | types["0E"] = "LEDS"; 22 | 23 | # 0x40 - 0x7F enumerator notifications 24 | types["40"] = "NOTIF_DEVICE_UNPAIRED"; 25 | types["41"] = "NOTIF_DEVICE_PAIRED"; 26 | types["42"] = "NOTIF_CONNECTION_STATUS"; 27 | types["4A"] = "NOTIF_RECV_LOCK_CHANGED"; # 0100 0000 = ready for connections, 0010 0000 = new connections disabled 28 | types["4B"] = "?NOTIF_PAIR_ACCEPTED"; # 0100 0000 29 | 30 | types["7F"] = "NOTIF_ERROR"; 31 | 32 | # 0x80 - 0xFF enumerator commands; Register Access 33 | types["80"] = "SET_REG"; # CMD_SWITCH 34 | types["81"] = "GET_REG"; # CMD_GET_PAIRED_DEVICES 35 | types["82"] = "SET_LONG_REG"; 36 | types["83"] = "GET_LONG_REG"; 37 | types["8F"] = "_ERROR_MSG"; 38 | # Align type name 39 | maxlen=0; 40 | for (i in types) { 41 | if (maxlen < length(types[i])) { 42 | maxlen = length(types[i]); 43 | } 44 | } 45 | 46 | # error messages for type=8F (ERROR_MSG) 47 | errmsgs["01"] = "SUCCESS"; 48 | errmsgs["02"] = "INVALID_SUBID"; 49 | errmsgs["03"] = "INVALID_ADDRESS"; 50 | errmsgs["04"] = "INVALID_VALUE"; 51 | errmsgs["05"] = "CONNECT_FAIL"; 52 | errmsgs["06"] = "TOO_MANY_DEVICES"; 53 | errmsgs["07"] = "ALREADY_EXISTS"; 54 | errmsgs["08"] = "BUSY"; 55 | errmsgs["09"] = "UNKNOWN_DEVICE"; 56 | errmsgs["0a"] = "RESOURCE_ERROR"; 57 | errmsgs["0b"] = "REQUEST_UNAVAILABLE"; 58 | errmsgs["0c"] = "INVALID_PARAM_VALUE"; 59 | errmsgs["0d"] = "WRONG_PIN_CODE"; 60 | 61 | regs["00"] = "ENABLED_NOTIFS"; 62 | regs["02"] = "CONNECTION_STATE"; 63 | regs["b2"] = "DEVICE_PAIRING"; 64 | regs["b3"] = "DEVICE_ACTIVITY"; 65 | regs["b5"] = "PAIRING_INFO"; 66 | } # end of BEGIN 67 | function colorize(col, s) { 68 | return "\033[" col "m" s "\033[m"; 69 | } 70 | # global color 71 | function c(s) { 72 | return colorize(color, s); 73 | } 74 | function endPoint(ep) { 75 | if (ep == "0") return "output"; 76 | if (ep == "1") return " input"; 77 | if (ep == "2") return colorize("1;33", "enumIf"); 78 | # if (ep == "3") return " ???"; # seen in the output of usbmon 79 | return sprintf("%6s", "ep" ep); 80 | } 81 | function dev(hex) { 82 | if (hex == "ff") { 83 | return "RECV"; 84 | } 85 | if (int(hex) >= 1 && int(hex) <= 6) { 86 | return "DEV" int(hex) 87 | } 88 | return " "; 89 | } 90 | function typeStr(hex) { 91 | return sprintf("%-" maxlen "s", types[toupper(hex)]); 92 | } 93 | function payload(type, p) { 94 | v1 = substr(p, 1, 2); 95 | if (type == "8f") { # error 96 | er=substr(p, 5, 2); 97 | reg=substr(p, 3, 2); 98 | parms = "SubID=" v1 99 | parms = parms ", Reg=" c(reg) " " regs[reg]; 100 | parms = parms ", er=" c(er); 101 | parms = parms " " errmsgs[er]; 102 | } else if (type == "80" || type == "81" || type == "82" || type == "83") { 103 | parms = "reg=" c(v1) " " regs[v1]; 104 | parms = parms " parms=" c(substr(p, 3)); 105 | } else { 106 | parms = "parms=" c(p); 107 | } 108 | return parms; 109 | } 110 | 111 | { 112 | if (match($0, /.*?:[0-9]+:[0-9]{3,}:([0-9]+) .*? = (..)(..)(..)(..) (.*)/, a)) { 113 | # length 85 is ok for most, but not when starting logitech program 114 | if (length($0) > 100) { 115 | print $0; 116 | $0 = ""; 117 | } 118 | printf("%-100s", $0); 119 | color = "1;32"; 120 | # sending data instead of receiving data 121 | if ($0 ~ " s ") color = "1;31"; 122 | 123 | print " " endPoint(a[1]), 124 | "report_id=" c(a[2]), 125 | "dev_idx=" c(a[3]) " " dev(a[3]), 126 | "type=" c(a[4]) " " typeStr(a[4]), 127 | payload(a[4], a[5] a[6]); 128 | } else { 129 | print colorize("1;30", $0); 130 | } 131 | fflush(); 132 | } 133 | -------------------------------------------------------------------------------- /keyboard.txt: -------------------------------------------------------------------------------- 1 | Keyboard - Fn keys customization 2 | ================================ 3 | This document describes observations with the K800 keyboard which features 4 | customizable Fn keys and a HID++ 1.0 protocol. Its observations may apply to 5 | other HID++ 1.0 keyboards too. 6 | 7 | See registers.txt for HID++ details. Important knowledge from that file: 8 | - Setting Notifications flags makes the keyboard emit different HID reports that 9 | can be captured by the software to generate custom events. 10 | - The functionality Fx and Fn + Fx can be swapped (e.g. pressing F1 generates a 11 | "Web" event, Fn + F1 generates the regular "F1" event). 12 | 13 | 14 | The Consumer Control (3) and System Control (4) descriptors have the following 15 | descriptors: 16 | INPUT(3)[INPUT] 17 | Field(0) 18 | Application(Consumer.0001) 19 | Usage(652) 20 | Consumer.0001 21 | Consumer.0002 22 | ... (a lot Consumer.xxxx omitted) ... 23 | Consumer.028b 24 | Consumer.028c 25 | Logical Minimum(1) 26 | Logical Maximum(652) 27 | Report Size(16) 28 | Report Count(2) 29 | Report Offset(0) 30 | Flags( Array Absolute ) 31 | INPUT(4)[INPUT] 32 | Field(0) 33 | Application(GenericDesktop.SystemControl) 34 | Usage(3) 35 | GenericDesktop.SystemSleep 36 | GenericDesktop.SystemPowerDown 37 | GenericDesktop.SystemWakeUp 38 | Logical Minimum(1) 39 | Logical Maximum(3) 40 | Report Size(2) 41 | Report Count(1) 42 | Report Offset(0) 43 | Flags( Array Absolute NoPreferredState NullState ) 44 | 45 | /* At most two simultaneous key presses can be registered. If a button is not 46 | * pressed, the value for that "button" is 00 00. The HID layer keeps a state of 47 | * which keys are pressed, reports simply change that state. 48 | * 49 | * 92 01 00 00 - Pressed "Calculator" (0192) 50 | * 92 01 b5 00 - Pressed "Forward" (00b5) 51 | * b5 00 00 00 - Released "Calculator" (0192 has gone) 52 | * 00 00 00 00 - Released "Forward" (00b5 has gone) 53 | */ 54 | struct consumer_control_data { 55 | uint16_t button1; 56 | uint16_t button2; 57 | } 58 | 59 | /* The button is a number in the range 1 - 3 (0 means released) 60 | */ 61 | struct system_control_data { 62 | char button : 2; /* two right-most bits, Big Endian */ 63 | }; 64 | 65 | The 20 ix yy dd.. ... messages below are described as follows: 66 | - 20: Report ID for Logitech Vendor DJ collection (messages are not processed by 67 | the hid-logitech-dj driver but are passed through to the HID layer.) 68 | - yy: descriptor type (system control, consumer control, keyboard, etc.) 69 | - dd..: data, length is dependent on descriptor type. 70 | - Remaining bytes is garbage/padding and can be ignored (confirmed by Nestor 71 | from Logitech). 72 | 73 | 74 | The following describes what events are generated when a certain flag is toggled 75 | in the notification register 00. 76 | flag 1, bit 1 - controls "System Control" events? 77 | Format: 78 | (disabled bit) 20 ix 04 XX ... (other 11 bytes is padding) 79 | (enabled bit) 10 ix 04 XX 00 00 00 80 | Values for XX: 81 | - 01: Sleep Button (Fn + F8) 82 | 83 | 84 | flag 1, bit 0 - controls "Consumer Control" events? 85 | Format: 86 | (disabled bit) 20 ix 03 XX xx YY yy ... (other 8 bytes is padding) 87 | (enabled bit) 10 ix 03 XX xx YY yy 88 | XX xx and YY yy are two buttons that are pressed according to the keyboard (see 89 | also struct consumer_control_data above). 90 | Known keys (XX xx are shown as xxXX): 91 | - 0223: Web (Fn + F1) 92 | - 018a: Email (Fn + F2) 93 | - 0221: Search (Fn + F3) 94 | - 0183: Music (Fn + F9) 95 | - 00b6: Previous (Fn + F10) 96 | - 00cd: Play/Pause (Fn + F10) 97 | - 00b5: Next (Fn + F10) 98 | - 00e2: Mute 99 | - 00ea: Volume down 100 | - 00e9: Volume up 101 | - 0192: Calculator 102 | - 102c: "Fn" button 103 | Note: when Fn keys are not swapped, pressing Fn + F1 will still generate 102c: 104 | vv vv-------- Fn (102c) 105 | 10 ix 03 2c 10 23 02 106 | ^^ ^^-- Web (0223) 107 | 108 | Remaining unmappable keys: 109 | - Application Switcher (Fn + F4) 110 | - Illumination brightnesss down (Fn + F5) 111 | - Illumination brightnesss up (Fn + F6) 112 | - Battery status (Fn + F7) 113 | -------------------------------------------------------------------------------- /read-dev-usbmon.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Tool for reading usbmon messages and writing non-empty data to stdout. 3 | * Because of limitations of a single output stream, there is currently a hack 4 | * that directly includes hidraw.c. 5 | * 6 | * Copyright (C) 2013 Peter Wu 7 | * 8 | * This program is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with this program. If not, see . 20 | */ 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include /* getenv */ 29 | #include 30 | #include /* gettimeofday */ 31 | #include /* localtime */ 32 | 33 | typedef uint16_t u16; 34 | typedef int32_t s32; 35 | typedef uint64_t u64; 36 | typedef int64_t s64; 37 | #define SETUP_LEN 8 38 | 39 | /* taken from Linux, Documentation/usb/usbmon.txt */ 40 | struct usbmon_packet { 41 | u64 id; /* 0: URB ID - from submission to callback */ 42 | unsigned char type; /* 8: Same as text; extensible. */ 43 | unsigned char xfer_type; /* ISO (0), Intr, Control, Bulk (3) */ 44 | unsigned char epnum; /* Endpoint number and transfer direction */ 45 | unsigned char devnum; /* Device address */ 46 | u16 busnum; /* 12: Bus number */ 47 | char flag_setup; /* 14: Same as text */ 48 | char flag_data; /* 15: Same as text; Binary zero is OK. */ 49 | s64 ts_sec; /* 16: gettimeofday */ 50 | s32 ts_usec; /* 24: gettimeofday */ 51 | int status; /* 28: */ 52 | unsigned int length; /* 32: Length of data (submitted or actual) */ 53 | unsigned int len_cap; /* 36: Delivered length */ 54 | union { /* 40: */ 55 | unsigned char setup[SETUP_LEN]; /* Only for Control S-type */ 56 | struct iso_rec { /* Only for ISO */ 57 | int error_count; 58 | int numdesc; 59 | } iso; 60 | } s; 61 | int interval; /* 48: Only for Interrupt and ISO */ 62 | int start_frame; /* 52: For ISO */ 63 | unsigned int xfer_flags; /* 56: copy of URB's transfer_flags */ 64 | unsigned int ndesc; /* 60: Actual number of ISO descriptors */ 65 | }; 66 | 67 | struct mon_get_arg { 68 | struct usbmon_packet *hdr; 69 | void *data; 70 | size_t alloc; /* Length of data (can be zero) */ 71 | }; 72 | 73 | #define MON_IOC_MAGIC 0x92 74 | #define MON_IOCQ_URB_LEN _IO(MON_IOC_MAGIC, 1) 75 | #define MON_IOCX_GET _IOW(MON_IOC_MAGIC, 6, struct mon_get_arg) 76 | 77 | #define NO_MAIN 78 | // HACK - otherwise there is no easy wat to tell whether a packet is read or 79 | // written from the usbmon 80 | #include "hidraw.c" 81 | #undef NO_MAIN 82 | 83 | void print_time(void) { 84 | struct timeval tval; 85 | struct tm *tm; 86 | 87 | if (gettimeofday(&tval, NULL)) { 88 | perror("gettimeofday"); 89 | return; 90 | } 91 | tm = localtime(&tval.tv_sec); 92 | printf("%02d:%02d:%02d.%03ld ", 93 | tm->tm_hour, tm->tm_min, tm->tm_sec, 94 | tval.tv_usec / 1000); 95 | } 96 | 97 | int main(int argc, char ** argv) { 98 | unsigned char data[1024]; 99 | struct usbmon_packet hdr; 100 | struct mon_get_arg event; 101 | int fd, r; 102 | 103 | if (argc < 2) { 104 | fprintf(stderr, "Usage: %s /dev/usbmonX\n", argv[0]); 105 | return 1; 106 | } 107 | 108 | fd = open(argv[1], O_RDONLY); 109 | if (fd < 0) { 110 | perror(argv[1]); 111 | return 1; 112 | } 113 | 114 | memset(&hdr, 0, sizeof hdr); 115 | event.hdr = &hdr; // hopefully it is OK to use stack for this 116 | event.data = &data; 117 | event.alloc = sizeof data; 118 | 119 | //r = ioctl(fd, MON_IOCQ_URB_LEN); 120 | //printf("%i\n", r); 121 | for (;;) { 122 | memset(&data, 0xCC, sizeof data); // for debugging purposes 123 | r = ioctl(fd, MON_IOCX_GET, &event); 124 | if (r == -1 && errno == EINTR) { 125 | continue; 126 | } 127 | if (r < 0) { 128 | perror("ioctl"); 129 | break; 130 | } 131 | 132 | // ignore non-data packets 133 | if (hdr.len_cap) { 134 | if (getenv("HEX")) { 135 | unsigned int i; 136 | printf("Type=%c\n", hdr.type); 137 | for (i=0; i sizeof (struct report)) { 142 | fprintf(stderr, "Discarding too large packet of length %u!\n", hdr.len_cap); 143 | } else { 144 | struct report *report = (struct report *)&data; 145 | if (hdr.len_cap < 3) { 146 | fprintf(stderr, "Short data len: %i\n", hdr.len_cap); 147 | continue; 148 | } 149 | #define COLOR(c, cstr) "\033[" c "m" cstr "\033[m" 150 | print_time(); 151 | if (hdr.type == 'C') { 152 | printf(COLOR("1;32", "Recv\t")); 153 | } else if (hdr.type == 'S') { 154 | printf(COLOR("1;31", "Send\t")); 155 | } else { 156 | printf(COLOR("1;35", "Type=%c\t") "\n", hdr.type); 157 | } 158 | process_msg(report, hdr.len_cap); 159 | fflush(NULL); 160 | #if 0 161 | if (write(STDOUT_FILENO, &data, hdr.len_cap) < 0) { 162 | perror("write"); 163 | break; 164 | } 165 | #endif 166 | } 167 | } 168 | } 169 | 170 | close(fd); 171 | 172 | return 0; 173 | } 174 | -------------------------------------------------------------------------------- /hidpp20.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define SHORT_MESSAGE 0x10 8 | #define LONG_MESSAGE 0x11 9 | 10 | struct hidpp2_message { 11 | u8 report_id; 12 | u8 device_index; 13 | u8 feature_index; 14 | #define HIDPP_SET_FUNC(msg, func) ((msg)->func_swId |= ((func) << 4)) 15 | u8 func_swId; 16 | u8 params[16]; // 3 or 16 params 17 | } __attribute__((__packed__)); 18 | 19 | struct feature { 20 | uint16_t featureId; 21 | #define FEAT_TYPE_MASK 0xe0 22 | #define FEAT_TYPE_OBSOLETE 0x80 23 | #define FEAT_TYPE_SWHIDDEN 0x40 24 | #define FEAT_TYPE_RSVD_INTERNAL 0x20 25 | u8 featureType; 26 | }; 27 | 28 | #define SOFTWARE_ID 0x04 /* getRandom() -- guaranteed to be random. */ 29 | 30 | #define FEATURE_INDEX_IROOT 0x00 31 | 32 | #define FID_IFEATURESET 0x0001 33 | static 34 | const char * 35 | get_feature_name(uint16_t featureId) { 36 | /* With '?' prefix are taken from SetPointP/KEMUI.xml */ 37 | switch (featureId) { 38 | case 0x0000: return "Root"; 39 | case 0x0001: return "FeatureSet"; 40 | case 0x0002: return "FeatureInfo"; 41 | case 0x0003: return "DeviceFwVersion"; 42 | case 0x0005: return "DeviceName"; 43 | case 0x0006: return "DeviceGroups"; 44 | case 0x00C0: return "Dfucontrol"; /* Firmware Update */ 45 | case 0x1000: return "BatteryStatus"; 46 | case 0x1900: return "?SoundNotif"; /* Sound Notification */ 47 | case 0x1920: return "?AudioControls"; /* Audio Controls */ 48 | case 0x1940: return "?VOIP"; /* Internet Telephony */ 49 | case 0x1960: return "?VideoCalling"; 50 | case 0x1980: return "?Backlighting"; 51 | case 0x1981: return "Backlight"; 52 | case 0x1B00: return "ReprogControls"; 53 | case 0x1B01: return "ReprogControlsV2"; 54 | case 0x1B03: return "ReprogControlsV3"; 55 | case 0x1D4B: return "WirelessDeviceStatus"; /* Wireless Update */ 56 | case 0x2000: return "?MouseFeature"; /* Pointing Device Feature */ 57 | case 0x2001: return "LeftRightSwap"; 58 | case 0x2100: return "VerticalScrolling"; 59 | case 0x2120: return "HiResScrolling"; 60 | case 0x2200: return "MousePointer"; 61 | case 0x2510: return "?ProfileMgmt"; /* Profile Management */ 62 | case 0x4000: return "?KeyboardFeature"; 63 | case 0x40A0: return "FnInversion"; 64 | case 0x40A2: return "NewFnInversion"; 65 | case 0x4100: return "Encryption"; 66 | case 0x4301: return "SolarDashboard"; 67 | case 0x4400: return "?DisplayFeature"; 68 | case 0x4520: return "KeyboardLayout"; /* Inactive key */ 69 | case 0x5500: return "?SliderControls"; 70 | case 0x6000: return "?TouchpadFeature"; 71 | case 0x6010: return "TouchpadFwItems"; /* Basic Touchpad settings */ 72 | case 0x6011: return "TouchpadSwItems"; /* Enhanced Touchpad settings */ 73 | case 0x6012: return "TouchpadWin8FwItems"; 74 | case 0x6020: return "?TouchpadTapSelect"; /* Tap to select */ 75 | case 0x6030: return "?DisablePointerAccel"; /* Disable pointer acceleration */ 76 | case 0x6100: return "TouchpadRawXy"; 77 | case 0x6110: return "TouchmouseRawPoints"; 78 | case 0x6120: return "Touchmouse6120"; 79 | case 0x6300: return "?HandwritingRecog"; /* Handwriting recognition" */ 80 | default: return "unknown"; 81 | } 82 | } 83 | 84 | /** 85 | * Initialize common values of a HID++ 2.0 message: report_id, device_index and 86 | * software ID. Remaining fields: feature_index, func, params. 87 | */ 88 | #define INIT_HIDPP_SHORT(msg, dev_index) \ 89 | memset((msg), 0, sizeof *(msg)); \ 90 | (msg)->report_id = SHORT_MESSAGE; \ 91 | (msg)->device_index = dev_index; \ 92 | (msg)->func_swId = SOFTWARE_ID 93 | 94 | static 95 | bool 96 | do_hidpp2_request(int fd, struct hidpp2_message *msg) { 97 | struct hidpp_message *hid10_message = (struct hidpp_message *) msg; 98 | 99 | if (!do_write(fd, hid10_message)) { 100 | return false; 101 | } 102 | 103 | // use do_read_skippy in case there are interleaving notifications 104 | // sub id is feature index in HID++ 2.0 105 | if (!do_read_skippy(fd, hid10_message, 0x11, hid10_message->sub_id)) { 106 | puts("WTF"); 107 | return false; 108 | } 109 | 110 | return true; 111 | } 112 | 113 | // Returns feature index for featureId 114 | static 115 | u8 116 | get_feature(int fd, u8 device_index, uint16_t featureId) { 117 | struct hidpp2_message msg; 118 | INIT_HIDPP_SHORT(&msg, device_index); 119 | msg.feature_index = FEATURE_INDEX_IROOT; 120 | HIDPP_SET_FUNC(&msg, 0); // GetFeature(featureId) 121 | msg.params[0] = featureId >> 8; 122 | msg.params[1] = featureId & 0xFF; 123 | if (!do_hidpp2_request(fd, &msg)) { 124 | return 0; 125 | } 126 | return msg.params[0]; 127 | } 128 | 129 | // Returns number of features or 0 on error. 130 | static 131 | u8 132 | get_feature_count(int fd, u8 device_index, u8 ifeatIndex) { 133 | struct hidpp2_message msg; 134 | INIT_HIDPP_SHORT(&msg, device_index); 135 | // XXX: is this variable? Can't it be hard-coded to 0x01? 136 | msg.feature_index = ifeatIndex; 137 | HIDPP_SET_FUNC(&msg, 0); // GetCount() 138 | if (!do_hidpp2_request(fd, &msg)) { 139 | fprintf(stderr, "Failed to request features count\n"); 140 | return 0; 141 | } 142 | return msg.params[0]; 143 | } 144 | 145 | // Get featureId and type for a given featureIndex. 146 | static 147 | bool 148 | get_featureId(int fd, u8 device_index, u8 ifeatIndex, u8 featureIndex, struct feature *feat) { 149 | struct hidpp2_message msg; 150 | INIT_HIDPP_SHORT(&msg, device_index); 151 | // XXX: is this variable? Can't it be hard-coded to 0x01? 152 | msg.feature_index = ifeatIndex; 153 | HIDPP_SET_FUNC(&msg, 1); // GetFeatureId(featureIndex) 154 | msg.params[0] = featureIndex; 155 | if (!do_hidpp2_request(fd, &msg)) { 156 | return false; 157 | } 158 | feat->featureId = (msg.params[0] << 8) | msg.params[1]; 159 | feat->featureType = msg.params[2]; 160 | return true; 161 | } 162 | 163 | void 164 | hidpp20_print_features(int fd, u8 device_index) { 165 | u8 i, count, ifeatIndex; 166 | 167 | ifeatIndex = get_feature(fd, device_index, FID_IFEATURESET); 168 | if (!ifeatIndex) { 169 | fprintf(stderr, "Failed to get feature information\n"); 170 | return; 171 | } 172 | count = get_feature_count(fd, device_index, ifeatIndex); 173 | 174 | printf("Total number of HID++ 2.0 features: %i\n", count); 175 | for (i = 0; i <= count; i++) { 176 | struct feature feat; 177 | if (get_featureId(fd, device_index, ifeatIndex, i, &feat)) { 178 | printf(" %2i: [%04X] %c%c%c %s\n", i, feat.featureId, 179 | feat.featureType & FEAT_TYPE_OBSOLETE ? 'O' : ' ', 180 | feat.featureType & FEAT_TYPE_SWHIDDEN ? 'H' : ' ', 181 | feat.featureType & FEAT_TYPE_RSVD_INTERNAL ? 'I' : ' ', 182 | get_feature_name(feat.featureId)); 183 | if (feat.featureType & ~FEAT_TYPE_MASK) { 184 | printf("Warning: unrecognized feature flags: %#04x\n", 185 | feat.featureType & ~FEAT_TYPE_MASK); 186 | } 187 | } else { 188 | fprintf(stderr, "Failed to get feature, is device connected?\n"); 189 | } 190 | } 191 | puts("(O = obsolete feature; H = SW hidden feature;\n" 192 | " I = reserved for internal use)"); 193 | } 194 | -------------------------------------------------------------------------------- /hidraw.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Displays a more human-readable interpretation of the USB data payload 3 | * for Logitech Unifying Receiver. 4 | * 5 | * Example usage: read-dev-usbmon /dev/usbmon0 | hidraw 6 | * 7 | * Copyright (C) 2013 Peter Wu 8 | * 9 | * This program is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * This program is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with this program. If not, see . 21 | */ 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | typedef unsigned char u8; 30 | 31 | #define SHORT_MSG 0x10 32 | #define SHORT_MSG_LEN 7 33 | #define DJ_SHORT 0x20 34 | #define DJ_SHORT_LEN 15 35 | #define DJ_LONG 0x21 36 | #define DJ_LONG_LEN 32 37 | #define LONG_MSG 0x11 38 | #define LONG_MSG_LEN 20 39 | 40 | struct payload_short { 41 | u8 address; 42 | u8 value[3]; 43 | }; 44 | struct payload_long { 45 | u8 address; 46 | u8 str[16]; 47 | }; 48 | struct report { 49 | u8 report_id; 50 | u8 device_index; 51 | u8 sub_id; 52 | union { 53 | struct payload_long l; 54 | struct payload_short s; 55 | }; 56 | } __attribute__((__packed__)); 57 | 58 | /* types for HID++ report IDs 0x10 and 0x11 */ 59 | static const char * report_types[0x100] = { 60 | [0x00] = "_HIDPP20", // fake type 61 | // 0x00 - 0x3F HID reports 62 | [0x01] = "KEYBOARD", 63 | [0x02] = "MOUSE", 64 | [0x03] = "CONSUMER_CONTROL", 65 | [0x04] = "SYSTEM_CONTROL", 66 | 67 | [0x08] = "MEDIA_CENTER", 68 | 69 | [0x0E] = "LEDS", 70 | 71 | // 0x40 - 0x7F enumerator notifications 72 | [0x40] = "NOTIF_DEVICE_UNPAIRED", 73 | [0x41] = "NOTIF_DEVICE_PAIRED", 74 | [0x42] = "NOTIF_CONNECTION_STATUS", 75 | [0x4A] = "NOTIF_RECV_LOCK_CHANGED", 76 | [0x4B] = "?NOTIF_PAIR_ACCEPTED", 77 | 78 | [0x7F] = "NOTIF_ERROR", 79 | 80 | // 0x80 - 0xFF enumerator commands; Register Access 81 | [0x80] = "SET_REG", // was CMD_SWITCH 82 | [0x81] = "GET_REG", // was CMD_GET_PAIRED_DEVICES 83 | [0x82] = "SET_LONG_REG", 84 | [0x83] = "GET_LONG_REG", 85 | [0x8F] = "_ERROR_MSG", 86 | [0xFF] = "_HIDPP20_ERROR_MSG", 87 | }; 88 | 89 | /* types for DJ report IDS 0x20 and 0x21 */ 90 | static const char *dj_report_types[0x100] = { 91 | /* 0x00 - 0x3F: RF reports */ 92 | [0x01] = "KEYBOARD", 93 | [0x02] = "MOUSE", 94 | [0x03] = "CONSUMER_CONTROL", 95 | [0x04] = "SYSTEM_CONTROL", 96 | [0x08] = "MEDIA_CENTER", 97 | [0x0E] = "KEYBOARD_LEDS", 98 | 99 | /* 0x40 - 0x7F: DJ notifications */ 100 | [0x40] = "NOTIF_DEVICE_UNPAIRED", 101 | [0x41] = "NOTIF_DEVICE_PAIRED", 102 | [0x42] = "NOTIF_CONNECTION_STATUS", 103 | 104 | [0x7F] = "NOTIF_ERROR", 105 | 106 | /* 0x80 - 0xFF: DJ commands */ 107 | [0x80] = "CMD_SWITCH_N_KEEPALIVE", 108 | [0x81] = "CMD_GET_PAIRED_DEVICES" 109 | }; 110 | 111 | static const char * error_messages[0x100] = { 112 | // error messages for type=8F (ERROR_MSG) 113 | [0x00] = "SUCCESS", 114 | [0x01] = "INVALID_SUBID", 115 | [0x02] = "INVALID_ADDRESS", 116 | [0x03] = "INVALID_VALUE", 117 | [0x04] = "CONNECT_FAIL", 118 | [0x05] = "TOO_MANY_DEVICES", 119 | [0x06] = "ALREADY_EXISTS", 120 | [0x07] = "BUSY", 121 | [0x08] = "UNKNOWN_DEVICE", 122 | [0x09] = "RESOURCE_ERROR", 123 | [0x0A] = "REQUEST_UNAVAILABLE", 124 | [0x0B] = "INVALID_PARAM_VALUE", 125 | [0x0C] = "WRONG_PIN_CODE", 126 | }; 127 | 128 | // I don't know the upper bound, perhaps 0x10 is enough 129 | static const char * error_messages_hidpp20[0x100] = { 130 | [0x00] = "NoError", 131 | [0x01] = "Unknown", 132 | [0x02] = "InvalidArgument", 133 | [0x03] = "OutOfRange", 134 | [0x04] = "HWError", 135 | [0x05] = "Logitech internal", 136 | [0x06] = "INVALID_FEATURE_INDEX", 137 | [0x07] = "INVALID_FUNCTION_ID", 138 | [0x08] = "Busy", 139 | [0x09] = "Unsupported", 140 | }; 141 | 142 | // everything with a '?' is guessed 143 | static const char * registers[0x100] = { 144 | [0x00] = "ENABLED_NOTIFS", 145 | [0x01] = "KBD_HAND_DETECT?", 146 | [0x02] = "CONNECTION_STATE", 147 | [0x07] = "BATTERY?", 148 | [0x09] = "FN_KEY_SWAP?", 149 | [0x17] = "ILLUMINATION_INFO?", 150 | [0xb2] = "DEVICE_PAIRING", 151 | [0xb3] = "DEVICE_ACTIVITY", 152 | [0xb5] = "PAIRING_INFO", 153 | [0xf1] = "VERSION_INFO?", 154 | }; 155 | 156 | bool report_type_is_hidpp(u8 report_id) { 157 | return report_id == SHORT_MSG || report_id == LONG_MSG; 158 | } 159 | bool report_type_is_dj(u8 report_id) { 160 | return report_id == DJ_SHORT || report_id == DJ_LONG; 161 | } 162 | 163 | const char * report_type_str(u8 report_id, u8 type) { 164 | const char *str = NULL; 165 | if (report_type_is_hidpp(report_id)) 166 | str = report_types[type]; 167 | else if (report_type_is_dj(report_id)) 168 | str = dj_report_types[type]; 169 | return str ? str : ""; 170 | } 171 | const char * device_type_str(u8 type) { 172 | switch (type) { 173 | case 0x01: return "DEV1"; 174 | case 0x02: return "DEV2"; 175 | case 0x03: return "DEV3"; 176 | case 0x04: return "DEV4"; 177 | case 0x05: return "DEV5"; 178 | case 0x06: return "DEV6"; 179 | case 0xFF: return "RECV"; 180 | default: return ""; 181 | } 182 | } 183 | const char *error_str(u8 er) { 184 | const char * str = error_messages[er]; 185 | return str ? str : ""; 186 | } 187 | const char *error_str_hidpp20(u8 er) { 188 | const char * str = error_messages_hidpp20[er]; 189 | return str ? str : ""; 190 | } 191 | const char *register_str(u8 reg) { 192 | const char * str = registers[reg]; 193 | return str ? str : ""; 194 | } 195 | 196 | void process_msg_payload(struct report *r, u8 data_len) { 197 | u8 pos, i; 198 | u8 * bytes = (u8 *) &r->s; 199 | 200 | pos = 0; // nothing has been processed 201 | 202 | if (report_type_is_hidpp(r->report_id)) 203 | switch (r->sub_id) { 204 | case 0x00: // assume HID++ 2.0 request/response for feature IRoot 205 | if (data_len == 4 || data_len == 17) { 206 | printf("func=%X ", bytes[0] >> 4); 207 | printf("swId=%X ", bytes[0] & 0xF); 208 | pos = 1; 209 | } 210 | break; 211 | case 0xFF: // assume HID++ 2.0 error 212 | if (data_len == 17) { 213 | printf("feat=%X ", bytes[0]); 214 | printf("func=%X ", bytes[1] >> 4); 215 | printf("swId=%X ", bytes[1] & 0xF); 216 | printf("err=%02X %s ", bytes[2], error_str_hidpp20(bytes[2])); 217 | pos = 3; 218 | } 219 | break; 220 | case 0x8F: // error 221 | // TODO: length check 222 | printf("SubID=%02X %s ", bytes[0], report_type_str(r->report_id, bytes[0])); 223 | printf("reg=%02X %s ", bytes[1], register_str(bytes[1])); 224 | printf("err=%02X %s ", bytes[2], error_str(bytes[2])); 225 | pos = 4; // everything is processed 226 | break; 227 | case 0x80: 228 | case 0x81: 229 | case 0x82: /* long */ 230 | case 0x83: /* long */ 231 | printf("reg=%02X %s ", bytes[0], register_str(bytes[0])); 232 | pos = 1; 233 | break; 234 | } 235 | 236 | if (pos < data_len) { 237 | printf("params="); 238 | //printf("params(len=%02X)=", data_len); 239 | } 240 | for (i = 0; pos < data_len; pos++, i++) { 241 | printf("%02X ", bytes[pos]); 242 | if (i % 4 == 3 && pos + 1 < data_len) { 243 | putchar(' '); 244 | } 245 | } 246 | } 247 | 248 | void process_msg(struct report *report, ssize_t size) { 249 | const char * report_type; 250 | 251 | switch (report->report_id) { 252 | case SHORT_MSG: 253 | report_type = "short"; 254 | if (size != SHORT_MSG_LEN) { 255 | fprintf(stderr, "Invalid short msg len %zi\n", size); 256 | return; 257 | } 258 | break; 259 | case LONG_MSG: 260 | report_type = "long"; 261 | if (size != LONG_MSG_LEN) { 262 | fprintf(stderr, "Invalid long msg len %zi\n", size); 263 | return; 264 | } 265 | break; 266 | case DJ_SHORT: 267 | report_type = "dj_s"; 268 | if (size != DJ_SHORT_LEN) { 269 | fprintf(stderr, "Invalid DJ short msg len %zi\n", size); 270 | return; 271 | } 272 | break; 273 | case DJ_LONG: 274 | report_type = "dj_l"; 275 | if (size != DJ_LONG_LEN) { 276 | fprintf(stderr, "Invalid DJ long msg len %zi\n", size); 277 | return; 278 | } 279 | break; 280 | default: 281 | report_type = "unkn"; 282 | //fprintf(stderr, "Unknown report ID %02x, len=%zi\n", report->report_id, size); 283 | if (size < 3) { 284 | return; 285 | } 286 | break; 287 | } 288 | 289 | printf("report_id=%02X %-5s ", report->report_id, report_type); 290 | printf("device=%02X %-4s ", report->device_index, 291 | device_type_str(report->device_index)); 292 | printf("type=%02X %-23s ", report->sub_id, 293 | report_type_str(report->report_id, report->sub_id)); 294 | 295 | if (size > 3) { 296 | process_msg_payload(report, size - 3); 297 | } 298 | putchar('\n'); 299 | } 300 | 301 | #ifndef NO_MAIN 302 | int main(int argc, char ** argv) { 303 | int fd = STDIN_FILENO; 304 | ssize_t r; 305 | struct report report; 306 | 307 | if (argc >= 2 && (fd = open(argv[1], O_RDONLY)) < 0) { 308 | perror(argv[1]); 309 | return 1; 310 | } 311 | 312 | do { 313 | memset(&report, 0xCC, sizeof report); // for debugging purposes 314 | r = read(fd, &report, sizeof report); 315 | if (r > 0) { 316 | process_msg(&report, r); 317 | } 318 | } while (r >= 0); 319 | 320 | if (r < 0) { 321 | perror("read"); 322 | } 323 | 324 | close(fd); 325 | 326 | return 0; 327 | } 328 | #endif /* ! NO_MAIN */ 329 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | // vim:syntax=c: 2 | // docs at https://drive.google.com/?tab=mo&pli=1&authuser=0#folders/0BxbRzx7vEV7eWmgwazJ3NUFfQ28 (found at http://code.google.com/p/chromium/issues/detail?id=175572) 3 | struct dj_report { 4 | u8 report_id; 5 | u8 device_index; 6 | u8 report_type; 7 | u8 report_params[DJREPORT_SHORT_LENGTH - 3]; 8 | }; 9 | 10 | // char magic_sequence[] = {0x10, 0xFF, 0x80, 0xB2, 0x01, 0x00, 0x00}; 11 | 12 | #define REPORT_TYPE_KEYBOARD 0x01 13 | #define REPORT_TYPE_MOUSE 0x02 14 | #define REPORT_TYPE_CONSUMER_CONTROL 0x03 15 | #define REPORT_TYPE_SYSTEM_CONTROL 0x04 16 | 17 | #define REPORT_TYPE_MEDIA_CENTER 0x08 18 | 19 | #define REPORT_TYPE_LEDS 0x0E 20 | 21 | #define REPORT_TYPE_NOTIF_DEVICE_UNPAIRED 0x40 22 | #define REPORT_TYPE_NOTIF_DEVICE_PAIRED 0x41 23 | #define REPORT_TYPE_NOTIF_CONNECTION_STATUS 0x42 24 | 25 | #define REPORT_TYPE_NOTIF_ERROR 0x7F 26 | 27 | #define REPORT_TYPE_CMD_SWITCH 0x80 28 | #define REPORT_TYPE_CMD_GET_PAIRED_DEVICES 0x81 29 | 30 | dj_report->report_id = REPORT_ID_DJ_SHORT; 31 | dj_report->device_index = 0xFF; 32 | dj_report->report_type = REPORT_TYPE_CMD_GET_PAIRED_DEVICES; 33 | retval = logi_dj_recv_send_report(djrcv_dev, dj_report); 34 | 35 | report_id = 0x10; 36 | device_index = 0xFF; 37 | report_type = 0x80; // REPORT_TYPE_CMD_SWITCH 38 | report_params = {0xB2, 0x01, 0x00, 0x00}; 39 | // { 0xB2 , Connect Devices, Device Number, Open Lock Timeout } 40 | // = {0xb2, 0x01, 0x50, 0x3c} 41 | // observation: S b203 0100 perform unpair 42 | // S b201 533c when discovery is enabled (with no paired devices) 43 | // R b200 0000 when discovery is enabled (no paired devices, waiting; also recvd when unpaired while in Advanced mode) 44 | // S b202 5394 when discovery is disabled (both with 1 paired kbd and no paired kbd; explicitly issued when closing pair program) 45 | // 0:1 1 address 46 | // 1:3 3 value (0 is returned on succesfully setting register) 47 | // Related to type 0x4A 48 | 49 | // observations + guesses 50 | // issued after 10 ff CMD_SWITCH b2 .. .. .. 51 | report_id = 0x10 52 | device_index = 0xFF 53 | report_type = 0x4A // receiver status - open for new devices? 54 | // R 0100 0000 discovery enabled 55 | // R 0001 0000 discovery disabled (issued after timeout) 56 | // R 0000 0000 discovery disabled (after CMD_SWITCH b200 0000; succesful pair) 57 | 58 | // 00000000 00000000 is sent when device is turned off, "null report"? 59 | 60 | // No paired devices, just started program: 61 | // output report_id=10 dev_idx=ff RECV type=83 parms=b5030000 62 | // ep3 report_id=11 dev_idx=ff RECV type=83 parms=b503af4f95 ea150609 00000000 00000000 63 | // No paired devices (same for unpaired kbd off/on), just started program: 64 | // output report_id=10 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b5030000 65 | // ep3 report_id=11 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b503af4f95 ea15060a 00000000 00000000 66 | // Press "Advanced", unpaired (same for unpaired kbd on/off): 67 | // output report_id=10 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b3000000 68 | // ep3 report_id=11 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b308000000 00000000 00000000 00000000 69 | 70 | /* 71 | report_id=10; dev_id=01 for Sent0+Recv0 and Sent1+Recv1, but ff for Sent1+Recv2 72 | Sent0 Recv0 73 | 00120100* 74 | 0d000000 810d0200* 75 | 07000000 07050000 76 | 77 | Sent1 Recv1 Recv2 78 | f1010000 f1012201 f1011201 79 | f1020000 f1020019 f1020019 80 | f1030000 f1030007 81f10300* 81 | f1040000 f1040201 f1040214 82 | 83 | *) type=8f instead of 81 CMD_GET_PAIRED_DEVICES 84 | Order = Sent0+Recv0, Sent1+Recv1, Sent1+Recv2 (+ = interleaved) 85 | */ 86 | 87 | 88 | // Discover? (click Advanced and get a lot of this spam) 89 | // send: report_id=10 dev_idx=ff type=83 parms=b3000000 # report yourself guys? 90 | // 91 | // recv: report_id=11 dev_idx=ff type=83 parms=b3a1000000 00000000 00000000 00000000 92 | // ep3 report_id=11 dev_idx=ff type=83 parms=b32a000000 00000000 00000000 00000000 # byte 2 is channel/encrypt key??? 93 | // ep3 report_id=11 dev_idx=ff type=83 parms=b331000000 00000000 00000000 00000000 # No devices turned on 94 | // ep3 report_id=11 dev_idx=ff type=83 parms=b334000000 00000000 00000000 00000000 # Device pair step 1 (after Sent0+Recv0) 95 | // ep3 report_id=11 dev_idx=ff type=83 parms=b336000000 00000000 00000000 00000000 # Device pair step 2 (after Sent1+Recv1) 96 | // ep3 report_id=11 dev_idx=ff type=83 parms=b338000000 00000000 00000000 00000000 # Device just paired (after Sent1+Recv2) 97 | 98 | 99 | // Unpair: 100 | // send: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b2030100 # R: Unpair device request 101 | // 102 | // recv: report_id=03 dev_idx=00 type=00 parms=0000 103 | // recv: report_id=10 dev_idx=01 type=40 NOTIF_DEVICE_UNPAIRED parms=02000000 104 | // recv: report_id=00 dev_idx=00 type=00 parms=0000000000 105 | // 106 | // recv: report_id=20 dev_idx=01 type=40 NOTIF_DEVICE_UNPAIRED parms=0000000000 00000000 000000 107 | // 108 | // recv: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b2000000 # K: Ready to accept other receiver? 109 | 110 | // Prepare switch? 111 | // send: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b201533c # R: Looking for devices? 112 | // recv: 113 | // recv: report_id=10 dev_idx=ff type=4a parms=01000000 # K: Hi I am a device 114 | // recv: 115 | // recv: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b2000000 # K: I want to pair with you 116 | // Switch timeout? (+/- 60 seconds) 117 | // recv: report_id=10 dev_idx=ff type=4a parms=00010000 # K: Nevermind, nobody responded 118 | 119 | // Turn on kbd while pair program is waiting for recv 120 | // recv: report_id=10 dev_idx=01 type=41 NOTIF_DEVICE_PAIRED parms=04611020 # L: I just turned myself on 121 | // 122 | // send: report_id=10 dev_idx=ff type=83 parms=b5400000 # R: Hey, wanna join me? 123 | // recv: report_id=20 dev_idx=01 type=41 NOTIF_DEVICE_PAIRED parms=0010201a40 00000000 000000 124 | // 125 | // recv: report_id=10 dev_idx=ff type=4a parms=00000000 126 | // 127 | // recv: report_id=11 dev_idx=ff type=83 parms=b540044b38 30300000 00000000 00000000 128 | // 129 | // send: report_id=10 dev_idx=ff type=83 parms=b5300000 130 | // 131 | // recv: report_id=11 dev_idx=ff type=83 parms=b530fb841b 861a4000 00070000 00000000 132 | // b530 133 | // fb841b 86 Serial No 134 | // 1a4000 00070000 00000000 ??? 135 | // 136 | // recv: report_id=10 dev_idx=01 type=41 NOTIF_DEVICE_PAIRED parms=04a11020 # I am already paired? (0x6_ -> 0xa_, 0110 -> 1010); Also sent when turning on paired kbd 137 | // type=41 Device Connection 138 | // 04 0000 0100 - Unifying protocol 139 | // a1 1010 0001 - DeviceType=Keyboard; Link is encrypted; Link is established; Packet with payload 140 | // 10 0001 0000 - Wireless PID LSB 141 | // 20 0010 0000 - Wireless PID MSB 142 | 143 | // continued switch. 144 | // send: report_id=10 dev_idx=01 type=00 parms=12283f94 145 | // : report_id=10 dev_idx=01 type=4b parms=01000000 146 | // 147 | // : report_id=10 dev_idx=01 type=8f parms=00120100 148 | // 149 | // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=0d000000 150 | // 151 | // : report_id=10 dev_idx=01 type=8f parms=810d0200 152 | // 153 | // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=07000000 154 | // 155 | // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=07050000 156 | // 157 | // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1010000 158 | // 159 | // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1012201 160 | // 161 | // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1020000 162 | // 163 | // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1020019 164 | // 165 | // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1030000 166 | // 167 | // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1030007 168 | // 169 | // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1040000 170 | // 171 | // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1040201 172 | // 173 | // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1010000 174 | // : report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1011201 175 | // 176 | // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1020000 177 | // : report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1020019 178 | // 179 | // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1030000 180 | // : 181 | // : report_id=10 dev_idx=ff type=8f parms=81f10300 182 | // : 183 | // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1040000 184 | // : report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1040214 185 | 186 | 187 | // http://tequals0.wordpress.com/2011/11/01/reverse-engineering-logitech-unifying-usb-protocol/ 188 | /* 189 | T: Bus=05 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 3 Spd=12 MxCh= 0 190 | D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 191 | P: Vendor=046d ProdID=c52b Rev=12.01 192 | S: Manufacturer=Logitech 193 | S: Product=USB Receiver 194 | C:* #Ifs= 3 Cfg#= 1 Atr=a0 MxPwr= 98mA 195 | I:* If#= 0 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=01 Driver=usbhid 196 | E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=8ms 197 | I:* If#= 1 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=02 Driver=usbhid 198 | E: Ad=82(I) Atr=03(Int.) MxPS= 8 Ivl=2ms 199 | I:* If#= 2 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid 200 | E: Ad=83(I) Atr=03(Int.) MxPS= 32 Ivl=2ms 201 | 202 | b=$'\e[1;32m';e=$'\e[m';sudo cat /sys/kernel/debug/usb/usbmon/5u | sed -ur "s/= (..)(..)(..)(..) (..)(....)(..)/= report_id=$b\1$e dev_idx=$b\2$e type=$b\3$e parms=$b\4\5 \6 \7$e/" 203 | 204 | see lt/usbmon.awk 205 | awk -vOFS=' ' 'function l(s){return "\033[1;32m" s "\033[m"}{if(match($0,/(.*? = )(..)(..)(..)(..) (.*)/,a)){printf("%-85s",$0);print "report_id=" l(a[2]), "dev_idx=" l(a[3]), "type=" l(a[4]), "parms=" l(a[5] a[6])}else print "\033[1;30m" $0 "\033[m"}' 206 | 207 | */ 208 | -------------------------------------------------------------------------------- /registers.txt: -------------------------------------------------------------------------------- 1 | Overview 2 | 3 | USB VID 0x046d 4 | USB PID 0xc52b 5 | 6 | Message Sub ID 7 | 0x80 SET_REGISTER 8 | 0x81 GET_REGISTER 9 | 0x82 SET_LONG_REGISTER 10 | 0x83 GET_LONG_REGISTER 11 | 0x8F ERROR_MSG 12 | 13 | Notifications 14 | 0x40 Device Disconnection 15 | 0x41 Device Connection 16 | 0x4A Unifying Receiver Locking Change information 17 | 18 | Registers 19 | 0x00 Enable HID++ Notifications 20 | 0x02 Connection State 21 | 0xB2 Device Connection and Disconnection (Pairing) 22 | 0xB3 Device Activity 23 | 0xB5 Pairing Information 24 | (more undocumented below) 25 | 26 | Not documented: 27 | Long register B5 (Pairing Information) - nn=03 "Receiver information?" 28 | Send: 10 FF 83 B5 03 00 00 00 - Short (10) message for receiver (FF) to retrieve 29 | long register (83) PairingInfo (B5) with special param 03 00 00 00. 30 | Recv: 11 FF 83 B5 03 AF 4F 95 EA 05 06 0E 00 00 00 00 00 00 00 00 31 | 11 - Long message 32 | FF - Receiver target 33 | 83 - LONG_REGISTER_RESPONSE 34 | B5 - Pairing Info 35 | 03 - "Receiver information"? 36 | AF 4F 95 EA - Serial Number of receiver 37 | 05 - dunno, also seen: 02, 18 38 | 06 - Max Device Capability? (not sure, but it is six) 39 | 0E - dunno, also seen: 32, 47 40 | 00 00 00 00 00 00 00 00 - padding? 41 | Remaining information: 42 | - Wireless Status (0x03) 43 | - ModelId (0x46d c52b) 44 | - Handle: 0xff000001 (0xff is device ID, 01 is internal to the software, ordered) 45 | - Dfu Status (0x1) 46 | - Is Dfu Cancellable (yes) 47 | 48 | DFU = Device Firmware Upgrade 49 | 0x00 - Up-to-date 50 | 0x01 - Unknown 51 | 0x02 - Available 52 | 0x03 - Initiated 53 | 0x04 - Failed 54 | 0x05 - Succeeded 55 | 0x06 - Newer software needed 56 | 0x06 - Unrecoverable 57 | 58 | Short register F1 - "Version information" (guessed) 59 | Header: 10 FF 81 (short msg, receiver is target, GET_REGISTER) 60 | Data: F1 nn xx yy (xx yy is empty for request and contains version for response) 61 | nn=01 xx,yy=12 01 62 | nn=02 xx,yy=00 19 63 | nn=03 error 03 (Invalid address) (but returns 00 07 for keyboard) 64 | nn=04 xx,yy=02 14 65 | Displayed firmware version: 012.001.00019 (x1 . y1 . x2 y2) 66 | Displayed bootloader version: BL.002.014 (BL . x4 . y4) 67 | 68 | More undocumented short regs **for K800 keyboard**: 69 | 17 rw Illumination info 70 | get 00 00 00 - Retrieve illimunation status? 71 | rsp: 3C 00 02 - (illimunation is disabled) 72 | set 3C 00 01 - Activate illumination only when I start typing 73 | set 3C 00 02 - Disable keyboard illumination 74 | Notif [10 ix 17] 3C 00 00 0n received when the keyboard illumination level is 75 | changed. (n = 1..5; 1=off; 5=full brightness) (NOTE: needs notif flag 00 02 00) 76 | 77 | 01 rw "Keyboard hand detection?" 78 | get 00 00 00 - retrieve keyboard hand detection status? 79 | rsp: 00 00 20 - (hand detection is disabled) 80 | set 00 00 00 - Enable hand detection 81 | set 00 00 20 - Disable hand detection 82 | 83 | 09 rw "F key functions" 84 | get 00 00 00 - Retrieve F key function state 85 | rsp: 00 00 00 - (F key functions are not swapped) 86 | set 00 01 00 - Swap F key functions 87 | set 00 00 00 - Do not swap F key functions 88 | 89 | 00 rw ENABLED_NOTIFS, 10 02 00, 10 is Battery info, buy what is 02? 90 | After writing FF FF FF, reading the register shows 13 02 00 91 | .1 .. .. - If enabled, keys like Web and Mute are passed over the HID protocol 92 | with short message type=03. This presumably allows the user to customize 93 | key bindings. 94 | .2 .. .. - If enabled, Fn+F8 (Sleep button) generates a HID++ short message 95 | instead of a key event that is captured by the Linux input layer 96 | (10 ix 04 0s 00 00 00 where s=1 when pressed and s=0 when released). 97 | 10:53:54.914 Recv report_id=10 short device=01 DEV1 type=04 SYSTEM_CONTROL 98 | params=01 00 00 00 99 | 1. .. .. - battery status (documented) (see also below, register/type 07) 100 | .. .2 .. - backlight changes (pressing Fn+F[56]) 101 | (more details about bits 0 and 1 of flag 1 in keyboard.txt) 102 | 103 | 07 r Likely the battery status of the kbd, not observed for M525 mouse 104 | get 00 00 00 105 | rsp: 07 00 00 - (Battery full?) 106 | rsp: 01 00 00 - (Battery almost empty? Flashing red light on keyboard) 107 | Undocumented short type=07 for KBD dev: 10 02 07 01 00 00 00; sent when 108 | pressing the Fn+F7 key for battery information. 01 is presumably the battery 109 | level (red, danger, etc)) 110 | (on kbd enable, 10 02 07 01 00 00 00 is sent ONLY if notification is enabled 111 | after NOTIF_DEVICE_PAIRED (0x41) / ?NOTIF_PAIR_ACCEPTED (0x4B); not observed 112 | when battery is not near dead) 113 | 114 | Format: 10 DEVID 07 00 00 115 | (or: response to reading register, 10 DEVID 81 07 07 00) 116 | Battery level: 117 | 00 is not possible for battery level, when the keyboard went off because of zero 118 | power, 01 was still the reported value 119 | 01 - red zone 120 | 03 - one bar 121 | 05 - two bars 122 | 07 - three bars 123 | Some magic: 124 | 00 - after unplugging/when not connected 125 | 22 - fully charged 126 | 25 - charging 127 | 26 - notification when battery is fully charged [10 ix 07] 07 26 00 00 128 | NOTE: 10 ix 07 notifications are only available when r0bit4 (Battery status) of 129 | Enable Hid++ Notifications (type=0x00) is enabled. This notification is then 130 | sent when pressing the Fn+F7 (battery) key or when an event happens (battery 131 | full). 132 | 133 | Recv report_id=10 short device=02 DEV2 type=41 NOTIF_DEVICE_PAIRED params=04 A1 10 20 # Hello, I am a device! 134 | Recv report_id=20 unkn device=02 DEV2 type=42 NOTIF_CONNECTION_STATUS params=00 00 00 00 00 00 00 00 00 00 00 00 # wtf is this? 135 | Recv report_id=10 short device=02 DEV2 type=4B ?NOTIF_PAIR_ACCEPTED params=01 00 00 00 # Guessed: pair succeeded? 136 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=10 00 00 # Enable battery status notifs 137 | Recv report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 00 00 # succesfully set 138 | Recv report_id=10 short device=02 DEV2 type=07 params=01 00 00 00 # battery low notif! 139 | 140 | 17 rw ??? 141 | 142 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=00 00 00 143 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=3C 00 01 144 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=3C 00 01 145 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=00 00 00 146 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 147 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 148 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 149 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 150 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 151 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 152 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 153 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 154 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=00 00 00 155 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=07 00 00 156 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 157 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=10 02 00 158 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=10 02 00 159 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 00 00 160 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=00 00 00 161 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=3C 00 01 162 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=3C 00 01 163 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=00 00 00 164 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 165 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 166 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 167 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 168 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 169 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 170 | Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 171 | report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 172 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=00 00 00 173 | report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=07 00 00 174 | 175 | Enabled HID++ Notifications for receiver 176 | Serial number: 03AF4F95 (shipped with K800 keyboard) 177 | Firmware version: 012.001.00019 178 | Bootloader version: BL.002.014 179 | Supported notifs: 00 09 00 180 | 181 | Serial number: 08D89AA6 (shipped with M525 mouse) 182 | Firmware version: 024.000.00018 183 | Bootloader version: BL.000.006 184 | Supported notifs: 00 19 00 185 | ^--- besides Wireless notifs (r1bit1) and Software present 186 | (r1bit3) it also supports another flag: r1bit4 (unknown functionality) 187 | 188 | 189 | Documented in hidpp10: 190 | Allows for testing protocol version. 191 | Request: 10 DeviceIndex 00 1n 00 00 uu 192 | - n is SwId 193 | - UU is "ping data" defined by SW 194 | Responses: 195 | HID++ 1.0: 10 ix 8F 00 Fn 01 00 (ERR_INVALID_SUBID) 196 | HID++ 2.0: 10 ix 00 1n 02 00 UU 197 | HID++ X.Y: 10 ix 00 1n XX YY UU 198 | 199 | Discovery: 200 | 1. Get Reg 00 201 | 2. Set Reg 00 to value from (1) with bit 0 of the second byte enabled (v1[1] |= 1) 202 | 3. Send read CONNECTION_STATE register for total number of devices 203 | 4. Write CONNECTION_STATE 02 00 00 register 204 | 5. (4) triggers a 0x41 notification (Device Paired notification) for each 205 | device. Note, device index does not have to start at 1 (if device got 206 | unpaired before). 207 | 5. Got response for (3). 208 | 6. (disable discovery) Get Reg 00 209 | 7. Set Reg 00 to value from (6) with bit 0 of the second byte disabled (v1[1] &= ~1) 210 | 211 | Startup (all targeted at receiver, notifications come from device 1..6): 212 | 1. Get receiver details from pairing info register (serial number, etc) 213 | 2. Get firmware version (recv) 214 | 3. Get bootloader version (recv) 215 | 4. Get enabled notifications 216 | 5. Enable wireless notifications (set enabled notifs reg) 217 | 6. Read connection state register for number of machines 218 | for each machine from (6): 219 | 7. Write params to connection state register (02 00 00 was written) (read 220 | returns after (8)) (possibly a "trigger report all paired devices") 221 | 8. (7) immediately triggers a Device Paired notification for a previously paired dev 222 | 9. Send/Read request for paired device device name 223 | 10. Send/Read request for paired device extended info (serial, .., location of power switch) 224 | 11. Read fw+bootloader version information again (this seems useless?) 225 | 226 | A1. Pairing starts: write DEVICE_PAIRING register, enable pairing with timeout (read returns after (A2)) 227 | A2. (A1) triggers Receiver Lock Changed notification (subid=0x41) (reason: no error) 228 | A3. On timeout, Receiver Lock Changed notification is received (reason: timeout) 229 | A4. (close button) On close, DEVICE_PAIRING register is written to disable pairing discovery 230 | A5. Receiver Lock Changed notification is immediately received (reason: timeout) 231 | 232 | Advanced: 233 | B1. Advanced button is pressed. Polling starts, every x time, DEVICE_ACTIVITY register request is sent/read 234 | B2. Unpair device, Device Unpaired notification (subid=0x40) received (reason: 235 | device disconnected) (note, report id 10 and 20 received, ignore 20) 236 | 237 | Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=03 00 00 238 | report_id=11 long device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=03 AF 4F 95 EA 05 06 0E 00 00 00 00 00 00 00 00 239 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 240 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 12 01 241 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 242 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 243 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 244 | report_id=10 short device=FF RECV type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS 245 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 246 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 14 247 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 248 | report_id=10 short device=FF RECV type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 01 00 249 | Send report_id=10 short device=FF RECV type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 01 00 250 | report_id=10 short device=FF RECV type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 00 00 251 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=02 CONNECTION_STATE params=00 00 00 252 | report_id=10 short device=FF RECV type=81 GET_REG reg=02 CONNECTION_STATE params=00 01 00 253 | Send report_id=10 short device=FF RECV type=80 SET_REG reg=02 CONNECTION_STATE params=02 00 00 254 | report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=04 61 10 20 255 | report_id=10 short device=FF RECV type=80 SET_REG reg=02 CONNECTION_STATE params=00 00 00 256 | Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=40 00 00 257 | report_id=11 long device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=40 04 4B 38 30 30 00 00 00 00 00 00 00 00 00 00 258 | Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=30 00 00 259 | report_id=11 long device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=30 FB 84 1B 86 1A 40 00 00 07 00 00 00 00 00 00 260 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 261 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 12 01 262 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 263 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 264 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 265 | report_id=10 short device=FF RECV type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS 266 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 267 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 14 268 | 269 | No paired devices, start pairing: 270 | 1. Enable device pairing (response read returns after (2)) 271 | 2. Got receiver lock notification (locking open) 272 | 273 | 3. Enable device, got Device Paired notif (kbd, link encrypted, link not established) 274 | 4. Send read request for device name (response in (6)) 275 | (5. got report_id=20 for device paired notif) 276 | 5. Got Receiver Lock changed notification (lock closed) 277 | 6. Response for device name (sent in (4)), Send/Read request for paired device 278 | extended info (serial, .., location of power switch) 279 | 7. Got Device Paired notif (kbd, link encrypted, link established, link with payload) 280 | 8. (??) Send dev1 [header 10 01 00] 12 28 3F 94 281 | 9. Got notification ("Pair accepted"?) [header 10 01 4B] 01 00 00 00 282 | 10. Dev1 request (8) returned error message (err=01 SUCCESS) 283 | 11. Read dev1 register 0D, but it returns an error (err=02 INVALID_SUBID) 284 | 12. Request/read dev1 register 07 285 | 13. Read dev1 firmware, bootloader version 286 | 14. Read recv firmware, bootloader version 287 | 15. Write DEVICE_PAIRING register (close lock) 288 | 16. Got Receiver Lock changed notification (lock closed) (was already closed in (5) though) 289 | 290 | Send report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=01 53 3C 291 | report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED params=01 00 00 00 292 | report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=00 00 00 293 | 294 | report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=04 61 10 20 295 | Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=40 00 00 296 | report_id=20 unkn device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=00 10 20 1A 40 00 00 00 00 00 00 00 297 | report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED params=00 00 00 00 298 | report_id=11 long device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=40 04 4B 38 30 30 00 00 00 00 00 00 00 00 00 00 299 | Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=30 00 00 300 | report_id=11 long device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=30 FB 84 1B 86 1A 40 00 00 07 00 00 00 00 00 00 301 | report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=04 A1 10 20 302 | Send report_id=10 short device=01 DEV1 type=00 params=12 28 3F 94 303 | report_id=10 short device=01 DEV1 type=4B ?NOTIF_PAIR_ACCEPTED params=01 00 00 00 304 | report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=00 reg=12 err=01 SUCCESS 305 | Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=0D params=00 00 00 306 | report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=81 GET_REG reg=0D err=02 INVALID_SUBID 307 | Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=07 params=00 00 00 308 | report_id=10 short device=01 DEV1 type=81 GET_REG reg=07 params=07 00 00 309 | Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 310 | report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=01 22 01 311 | Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 312 | report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 313 | Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 314 | report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 07 315 | Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 316 | report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 01 317 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 318 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 12 01 319 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 320 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 321 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 322 | report_id=10 short device=FF RECV type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS 323 | Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 324 | report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 14 325 | 326 | Send report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=02 53 94 327 | report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=00 00 00 328 | report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED params=00 00 00 00 329 | 330 | Somewhere in the below stream is the battery condition of the M525 mouse (good). 331 | It could also be that there is no battery information at all and that SetPoint 332 | is faking the information. I have tried brand-new Duracell batteries and the 333 | output is still the same. 334 | Send report_id=10 short device=01 DEV1 type=00 params=05 00 03 00 335 | Recv report_id=11 long device=01 DEV1 type=00 params=05 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 336 | Send report_id=10 short device=01 DEV1 type=02 MOUSE params=05 00 00 00 337 | Recv report_id=11 long device=01 DEV1 type=02 MOUSE params=05 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 338 | Send report_id=10 short device=01 DEV1 type=02 MOUSE params=15 00 00 00 339 | Recv report_id=11 long device=01 DEV1 type=02 MOUSE params=15 00 52 51 4D 27 02 00 28 00 40 13 00 00 00 00 00 340 | Send report_id=10 short device=01 DEV1 type=04 SYSTEM_CONTROL params=05 00 00 00 341 | Recv report_id=11 long device=01 DEV1 type=04 SYSTEM_CONTROL params=05 5A 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 342 | Send report_id=10 short device=01 DEV1 type=0B params=15 01 00 00 343 | Recv report_id=11 long device=01 DEV1 type=0B params=15 01 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 344 | 345 | The mouse got some messages when SetPoint is closed: 346 | Send report_id=10 short device=01 DEV1 type=0B params=15 00 00 00 347 | Recv report_id=11 long device=01 DEV1 type=0B params=15 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 348 | Send report_id=10 short device=01 DEV1 type=04 SYSTEM_CONTROL params=05 00 00 00 349 | Recv report_id=11 long device=01 DEV1 type=04 SYSTEM_CONTROL params=05 5A 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 350 | 351 | Back to alive (very, very red battery for keyboard): 352 | Recv report_id=10 short device=02 DEV2 type=41 NOTIF_DEVICE_PAIRED params=04 A1 10 20 353 | Recv report_id=20 unkn device=02 DEV2 type=42 NOTIF_CONNECTION_STATUS params=00 00 00 00 00 00 00 00 00 00 00 00 354 | Send report_id=20 unkn device=02 DEV2 type=0E LEDS params=00 00 00 00 00 00 00 00 00 00 00 00 355 | 356 | All registers for K800 (HID++ 1.0): 357 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 358 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 359 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 KBD_HAND_DETECT? params=00 00 00 360 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 KBD_HAND_DETECT? params=00 00 00 361 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 BATTERY? params=00 00 00 362 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 BATTERY? params=03 00 00 363 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 FN_KEY_SWAP? params=00 00 00 364 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 FN_KEY_SWAP? params=00 00 00 365 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 ILLUMINATION_INFO? params=00 00 00 366 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 ILLUMINATION_INFO? params=3C 00 01 367 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=51 params=00 00 00 368 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=51 params=00 00 00 369 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=54 params=00 00 00 370 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=54 params=FF 00 00 371 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=D0 params=00 00 00 372 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=D0 params=00 00 00 373 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=F1 VERSION_INFO? params=00 00 00 374 | Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS 375 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=F2 params=00 00 00 376 | Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=81 GET_REG reg=F2 err=02 INVALID_SUBID 377 | Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=F3 params=00 00 00 378 | Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=F3 params=00 00 00 379 | Send report_id=10 short device=02 DEV2 type=83 GET_LONG_REG reg=0F params=00 00 00 380 | Recv report_id=11 long device=02 DEV2 type=83 GET_LONG_REG reg=0F params=FF FB 00 00 02 40 02 5C 00 00 00 00 0F F9 00 80 381 | 382 | Observations about reading regs: 383 | - The M525 HID++ 2.0 device does not reply with anything useful to any register 384 | request (GET_REG or GET_LONG_REG with 00 params). 385 | - eading an unknown register on K800 keyboard (HID++ 1.0) yields: 386 | Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=81 GET_REG reg=FE err=02 INVALID_SUBID 387 | Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=83 GET_LONG_REG reg=FE err=02 INVALID_SUBID 388 | - On a M525 mouse (HID++ 2.0), this shows SUCCESS instead: 389 | Recv report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=81 GET_REG reg=FE err=01 SUCCESS 390 | Recv report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=83 GET_LONG_REG reg=FE err=01 SUCCESS 391 | -------------------------------------------------------------------------------- /ltunify.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Pair, unpair or list information about wireless devices like keyboards and 3 | * mice that use the Logitech® Unifying receiver. 4 | * 5 | * Copyright (C) 2013 Peter Wu 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include /* strtoul */ 28 | #include /* uint16_t */ 29 | #include /* ntohs, ntohl */ 30 | #include /* for /dev/hidrawX discovery */ 31 | #include /* for getopt_long */ 32 | #include 33 | #include /* for basename, used during discovery */ 34 | #include /* needs -lrt, for clock_gettime as timeout helper */ 35 | #include 36 | 37 | #ifndef PACKAGE_VERSION 38 | # define PACKAGE_VERSION "0.3" 39 | #endif 40 | 41 | #define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a)) 42 | 43 | // pass -D option to print very verbose details like protocol communication 44 | static bool debug_enabled; 45 | #define DPRINTF(...) if (debug_enabled) { fprintf(stderr, __VA_ARGS__); } 46 | 47 | typedef unsigned char u8; 48 | 49 | #define VID_LOGITECH 0x046d 50 | #define PID_NANO_RECEIVER 0xc52f 51 | #define PID_NANO_RECEIVER_2 0xc534 52 | 53 | #define HEADER_SIZE 3 54 | #define SHORT_MESSAGE 0x10 55 | #define SHORT_MESSAGE_LEN 7 56 | #define LONG_MESSAGE 0x11 57 | #define LONG_MESSAGE_LEN 20 58 | 59 | #define DEVICE_RECEIVER 0xFF 60 | 61 | #define SUB_SET_REGISTER 0x80 62 | #define SUB_GET_REGISTER 0x81 63 | #define SUB_SET_LONG_REGISTER 0x82 64 | #define SUB_GET_LONG_REGISTER 0x83 65 | #define SUB_ERROR_MSG 0x8F 66 | 67 | #define NOTIF_DEV_DISCONNECT 0x40 /* Device Disconnection */ 68 | #define NOTIF_DEV_CONNECT 0x41 /* Device Connection */ 69 | #define NOTIF_RECV_LOCK_CHANGE 0x4A /* Unifying Receiver Locking Change information */ 70 | 71 | #define REG_ENABLED_NOTIFS 0x00 72 | #define REG_CONNECTION_STATE 0x02 73 | /* Device Connection and Disconnection (Pairing) */ 74 | #define REG_DEVICE_PAIRING 0xB2 75 | #define REG_DEVICE_ACTIVITY 0xB3 76 | #define REG_PAIRING_INFO 0xB5 77 | #define REG_VERSION_INFO 0xF1 /* undocumented */ 78 | 79 | // Used for: {GET,SET}_REGISTER_{REQ,RSP}, SET_LONG_REGISTER_RSP, GET_LONG_REGISTER_REQ 80 | struct msg_short { 81 | u8 address; 82 | u8 value[3]; 83 | }; 84 | // Used for: SET_LONG_REGISTER_REQ, GET_LONG_REGISTER_RSP 85 | struct msg_long { 86 | u8 address; 87 | u8 str[16]; 88 | }; 89 | // Used for: ERROR_MSG 90 | struct msg_error { 91 | u8 sub_id; 92 | u8 address; 93 | u8 error_code; 94 | u8 padding; /* set to 0 */ 95 | }; 96 | 97 | // 0x00 Enable HID++ Notifications 98 | struct msg_enable_notifs { 99 | u8 reporting_flags_devices; // bit 4 Battery status 100 | u8 reporting_flags_receiver; // bit 0 Wireless notifications, 3 Software Present 101 | u8 reporting_flags_receiver2; // (reserved) 102 | }; 103 | 104 | // long receiver resp - 0xB5 Pairing information, 0x03 - "Receiver information"? (undocumented) 105 | struct msg_receiver_info { 106 | u8 _dunno1; // always 0x03 for receiver? 107 | u8 serial_number[4]; 108 | u8 _dunno2; // 06 - Max Device Capability? (not sure, but it is six) 109 | u8 _dunno3; 110 | u8 padding[8]; // 00 00 00 00 00 00 00 00 - ?? 111 | }; 112 | 113 | // 0xB5 Pairing information, 0x20..0x2F - Unifying Device pairing information 114 | struct msg_dev_pair_info { 115 | u8 requested_field; // 0x20..0x25 116 | u8 dest_id; 117 | u8 report_interval; // ms 118 | u8 pid_msb; 119 | u8 pid_lsb; 120 | u8 _reserved1[2]; 121 | u8 device_type; 122 | u8 _reserved2[6]; 123 | }; 124 | // 0xB5 Pairing information, 0x30..0x3F - Unifying Device extended pairing info 125 | struct msg_dev_ext_pair_info { 126 | u8 requested_field; // 0x30..0x35 127 | u8 serial_number[4]; // index 0 is MSB 128 | u8 report_types[4]; // index 0 is MSB 129 | u8 usability_info; // bits 0..3 is location of power switch 130 | }; 131 | // 0xB5 Pairing information, 0x40..0x4F - Unifying Device name 132 | #define DEVICE_NAME_MAXLEN 14 133 | struct msg_dev_name { 134 | u8 requested_field; // 0x40..0x45 135 | u8 length; 136 | char str[DEVICE_NAME_MAXLEN]; // UTF-8 encoding 137 | }; 138 | 139 | struct notif_devcon { 140 | #define DEVCON_PROT_UNIFYING 0x04 141 | #define DEVCON_PROT_NANO_LITE 0x0a 142 | u8 prot_type; // bits 0..2 is protocol type (4 for unifying), 3..7 is reserved 143 | #define DEVCON_DEV_TYPE_MASK 0x0f 144 | // Link status: 0 is established (in range), 1 is not established (out of range) 145 | #define DEVCON_LINK_STATUS_FLAG 0x40 146 | u8 device_info; 147 | // wireless product id: 148 | u8 pid_lsb; 149 | u8 pid_msb; 150 | }; 151 | 152 | // Register 0x02 Connection State 153 | struct val_reg_connection_state { 154 | // 0x02 triggers a 0x41 notification for all known devices 155 | #define CONSTATE_ACTION_LIST_DEVICES 0x02 156 | u8 action; // always 0 for read 157 | u8 connected_devices_count; 158 | u8 _undocumented2; 159 | }; 160 | 161 | // Register 0xB2 Device Connection and Disconnection (Pairing) 162 | struct val_reg_devpair { 163 | #define DEVPAIR_KEEP_LOCK 0 164 | #define DEVPAIR_OPEN_LOCK 1 165 | #define DEVPAIR_CLOSE_LOCK 2 166 | #define DEVPAIR_DISCONNECT 3 167 | u8 action; 168 | u8 device_number; // same as device index from 0x41 notif 169 | u8 open_lock_timeout; // timeout in seconds, 0 = default (30s) 170 | }; 171 | 172 | // Register 0xF1 Version Info (undocumented) 173 | struct val_reg_version { 174 | // v1.v2.xxx 175 | #define VERSION_FIRMWARE 1 176 | // x.x.v1v2 177 | #define VERSION_FW_BUILD 2 178 | // value 3 is invalid for receiver, but returns 00 07 for keyboard 179 | // BL.v1.v2 180 | #define VERSION_BOOTLOADER 4 181 | u8 select_field; 182 | u8 v1; 183 | u8 v2; 184 | }; 185 | 186 | struct hidpp_message { 187 | u8 report_id; 188 | u8 device_index; 189 | u8 sub_id; 190 | union { 191 | struct msg_short msg_short; 192 | struct msg_long msg_long; 193 | struct msg_error msg_error; 194 | }; 195 | }; 196 | 197 | struct hidpp_version { 198 | u8 major; 199 | u8 minor; 200 | }; 201 | 202 | struct version { 203 | u8 fw_major; 204 | u8 fw_minor; 205 | uint16_t fw_build; 206 | u8 bl_major; 207 | u8 bl_minor; 208 | }; 209 | 210 | #define DEVICES_MAX 6u 211 | struct device { 212 | bool device_present; // whether the device is paired 213 | bool device_available; // whether the device is connected 214 | u8 device_type; 215 | uint16_t wireless_pid; 216 | char name[DEVICE_NAME_MAXLEN + 1]; // include NUL byte 217 | uint32_t serial_number; 218 | u8 power_switch_location; 219 | struct hidpp_version hidpp_version; 220 | struct version version; 221 | }; 222 | struct device devices[DEVICES_MAX]; 223 | 224 | struct receiver_info { 225 | uint32_t serial_number; 226 | struct version version; 227 | }; 228 | 229 | struct receiver_info receiver; 230 | 231 | // error messages for type=8F (ERROR_MSG) 232 | static const char * error_messages[0x100] = { 233 | [0x00] = "SUCCESS", 234 | [0x01] = "INVALID_SUBID", 235 | [0x02] = "INVALID_ADDRESS", 236 | [0x03] = "INVALID_VALUE", 237 | [0x04] = "CONNECT_FAIL", 238 | [0x05] = "TOO_MANY_DEVICES", 239 | [0x06] = "ALREADY_EXISTS", 240 | [0x07] = "BUSY", 241 | [0x08] = "UNKNOWN_DEVICE", 242 | [0x09] = "RESOURCE_ERROR", 243 | [0x0A] = "REQUEST_UNAVAILABLE", 244 | [0x0B] = "INVALID_PARAM_VALUE", 245 | [0x0C] = "WRONG_PIN_CODE", 246 | }; 247 | 248 | static const char * device_type[0x10] = { 249 | [0x00] = "Unknown", 250 | [0x01] = "Keyboard", 251 | [0x02] = "Mouse", 252 | [0x03] = "Numpad", 253 | [0x04] = "Presenter", 254 | // 0x05..0x07 Reserved for future 255 | [0x08] = "Trackball", 256 | [0x09] = "Touchpad", 257 | // 0x0A..0x0F Reserved 258 | }; 259 | 260 | const char *device_type_str(u8 type) { 261 | if (type > 0x0F) { 262 | return "(invalid)"; 263 | } 264 | if (device_type[type]) { 265 | return device_type[type]; 266 | } 267 | return "(reserved)"; 268 | } 269 | 270 | // returns device type index or -1 if the string is invalid 271 | int device_type_from_str(const char *str) { 272 | unsigned i; 273 | 274 | // skip "Unknown" type 275 | for (i = 1; i < ARRAY_SIZE(device_type); i++) { 276 | if (device_type[i] && !strcasecmp(device_type[i], str)) { 277 | return i; 278 | } 279 | } 280 | 281 | return -1; 282 | } 283 | static void print_device_types(void) { 284 | unsigned i; 285 | 286 | // skip "Unknown" type 287 | for (i = 1; i < ARRAY_SIZE(device_type); i++) { 288 | if (device_type[i]) { 289 | fprintf(stderr, " %s", device_type[i]); 290 | } 291 | } 292 | putchar('\n'); 293 | } 294 | 295 | static void dump_msg(struct hidpp_message *msg, size_t payload_size, const char *tag) { 296 | size_t i; 297 | 298 | if (!debug_enabled) { 299 | return; 300 | } 301 | 302 | // HACK: do not mess with stderr colors 303 | fflush(NULL); 304 | printf("\033[34m"); 305 | printf("%s: ", tag); 306 | for (i=0; i 321 | static ssize_t do_read(int fd, struct hidpp_message *msg, u8 expected_report_id, int timeout) { 322 | ssize_t r; 323 | size_t payload_size = LONG_MESSAGE_LEN; 324 | long long unsigned begin_ms, end_ms; 325 | 326 | if (expected_report_id == SHORT_MESSAGE) { 327 | payload_size = SHORT_MESSAGE_LEN; 328 | } 329 | 330 | begin_ms = get_timestamp_ms(); 331 | 332 | while (timeout > 0) { 333 | struct pollfd pollfd; 334 | pollfd.fd = fd; 335 | pollfd.events = POLLIN; 336 | 337 | r = poll(&pollfd, 1, timeout); 338 | if (r < 0) { 339 | perror("poll"); 340 | return 0; 341 | } else if (r == 0) { 342 | DPRINTF("poll timeout reached!\n"); 343 | // timeout 344 | return 0; 345 | } 346 | 347 | memset(msg, 0, payload_size); 348 | r = read(fd, msg, payload_size); 349 | if (r < 0) { 350 | perror("read"); 351 | return 0; 352 | } else if (r > 0) { 353 | dump_msg(msg, r, "rd"); 354 | if (msg->report_id == expected_report_id) { 355 | return r; 356 | } else if (expected_report_id == 0 && 357 | (msg->report_id == SHORT_MESSAGE || 358 | msg->report_id == LONG_MESSAGE)) { 359 | /* HACK: ping response for HID++ 2.0 is a LONG 360 | * message, but for HID++ 1.0 it is a SHORT one. */ 361 | return r; 362 | } else { 363 | DPRINTF("Skipping unexpected report ID %#x (want %#x)\n", 364 | msg->report_id, expected_report_id); 365 | } 366 | } 367 | 368 | /* unexpected message, try again with updated timeout */ 369 | end_ms = get_timestamp_ms(); 370 | timeout -= end_ms - begin_ms; 371 | begin_ms = end_ms; 372 | } 373 | 374 | /* timeout expired, no report found unfortunately */ 375 | return 0; 376 | } 377 | static ssize_t do_write(int fd, struct hidpp_message *msg) { 378 | ssize_t r, payload_size = SHORT_MESSAGE_LEN; 379 | 380 | if (msg->report_id == LONG_MESSAGE) { 381 | payload_size = LONG_MESSAGE_LEN; 382 | } 383 | 384 | dump_msg(msg, payload_size, "wr"); 385 | r = write(fd, msg, payload_size); 386 | if (r < 0) { 387 | perror("write"); 388 | } 389 | 390 | return payload_size == r ? payload_size : 0; 391 | } 392 | 393 | const char *get_report_id_str(u8 report_type) { 394 | switch (report_type) { 395 | case SHORT_MESSAGE: 396 | return "short"; 397 | case LONG_MESSAGE: 398 | return "long"; 399 | default: 400 | return "unkn"; 401 | } 402 | } 403 | 404 | bool process_notif_dev_connect(struct hidpp_message *msg, u8 *device_index, 405 | bool *is_new_device) { 406 | u8 dev_idx = msg->device_index; 407 | struct notif_devcon *dcon = (struct notif_devcon *) &msg->msg_short; 408 | struct device *dev; 409 | if (msg->sub_id != NOTIF_DEV_CONNECT) { 410 | fprintf(stderr, "Invalid msg type %#0x, expected dev conn notif\n", 411 | msg->sub_id); 412 | return false; 413 | } 414 | if (msg->report_id != SHORT_MESSAGE) { 415 | fprintf(stderr, "Dev conn notif is expected to be short, got " 416 | "%#04x instead\n", msg->report_id); 417 | return false; 418 | } 419 | if (dcon->prot_type != DEVCON_PROT_UNIFYING && 420 | dcon->prot_type != DEVCON_PROT_NANO_LITE) { 421 | fprintf(stderr, "Unknown protocol %#04x in devcon notif\n", 422 | dcon->prot_type); 423 | return false; 424 | } 425 | if (dev_idx < 1 || dev_idx > DEVICES_MAX) { 426 | fprintf(stderr, "Disallowed device index %#04x\n", dev_idx); 427 | return false; 428 | } 429 | 430 | dev = &devices[dev_idx - 1]; 431 | if (device_index) *device_index = dev_idx; 432 | if (is_new_device) *is_new_device = !dev->device_present; 433 | 434 | memset(dev, 0, sizeof *dev); 435 | dev->device_type = dcon->device_info & DEVCON_DEV_TYPE_MASK; 436 | dev->wireless_pid = (dcon->pid_msb << 8) | dcon->pid_lsb; 437 | dev->device_present = true; 438 | dev->device_available = !(dcon->device_info & DEVCON_LINK_STATUS_FLAG); 439 | return true; 440 | } 441 | 442 | // use for reading registers and skipping notifications from return 443 | static bool do_read_skippy(int fd, struct hidpp_message *msg, 444 | u8 exp_report_id, u8 exp_sub_id) { 445 | for (;;) { 446 | if (!do_read(fd, msg, exp_report_id, 2000)) { 447 | return false; 448 | } 449 | if (msg->report_id == exp_report_id && msg->sub_id == exp_sub_id) { 450 | return true; 451 | } 452 | 453 | /* ignore non-HID++ reports (e.g. DJ reports) */ 454 | if (msg->report_id != SHORT_MESSAGE && msg->report_id != LONG_MESSAGE) { 455 | continue; 456 | } 457 | 458 | // guess: 0xFF is error message in HID++ 2.0? 459 | if (msg->report_id == LONG_MESSAGE && msg->sub_id == 0xFF) { 460 | if (debug_enabled) { 461 | fprintf(stderr, "HID++ 2.0 error %#04x\n", 462 | msg->msg_long.str[2]); 463 | } 464 | return false; 465 | } 466 | if (msg->report_id == SHORT_MESSAGE && msg->sub_id == SUB_ERROR_MSG 467 | && msg->msg_error.sub_id == exp_sub_id) { 468 | struct msg_error *error = &msg->msg_error; 469 | // TODO: consider address (register)? 470 | u8 error_code = error->error_code; 471 | const char *err_str = error_messages[error_code]; 472 | if (debug_enabled) { 473 | fprintf(stderr, "Received error for subid=%#04x," 474 | " reg=%#04x: %#04x (%s)\n", error->sub_id, 475 | error->address, error_code, err_str); 476 | } 477 | return false; 478 | } 479 | if (msg->sub_id == NOTIF_DEV_CONNECT) { 480 | process_notif_dev_connect(msg, NULL, NULL); 481 | continue; 482 | } else if (msg->sub_id == NOTIF_DEV_DISCONNECT) { 483 | u8 device_index = msg->device_index; 484 | u8 disconnect_type = *(u8 *) &msg->msg_short; 485 | if (device_index < 1 || device_index > DEVICES_MAX) { 486 | fprintf(stderr, "Invalid device index %#04x\n", device_index); 487 | } else if (disconnect_type & 0x02) { 488 | memset(&devices[device_index - 1], 0, sizeof *devices); 489 | } else { 490 | fprintf(stderr, "Unexpected disconnection type %#04x\n", disconnect_type); 491 | } 492 | } else if (msg->sub_id == NOTIF_RECV_LOCK_CHANGE) { 493 | if (msg->report_id != SHORT_MESSAGE || msg->device_index != DEVICE_RECEIVER) { 494 | fprintf(stderr, "Received invalid Unifying Receiver Locking Change notification (0x4A)\n"); 495 | continue; 496 | } 497 | // TODO: this can be used to make an application aware 498 | // that pairing is (not) possible (anymore) 499 | if (debug_enabled) { 500 | fprintf(stderr, "Receiver lock state is now %s\n", 501 | (*(u8 *)&msg->msg_short) & 1 ? "open" : "closed"); 502 | } 503 | } 504 | if (debug_enabled && 505 | (msg->report_id != exp_report_id || msg->sub_id != exp_sub_id)) { 506 | fprintf(stderr, "Expected %s msg %#02x, got %s msg %#02x\n", 507 | get_report_id_str(exp_report_id), exp_sub_id, 508 | get_report_id_str(msg->report_id), msg->sub_id); 509 | } 510 | // let's try reading another message... 511 | } 512 | return true; 513 | } 514 | 515 | // TODO: separate files 516 | #include "hidpp20.c" 517 | 518 | static bool set_register(int fd, u8 device_index, u8 address, 519 | u8 *params, struct hidpp_message *res, bool is_long_req) { 520 | u8 exp_sub_id; 521 | struct hidpp_message msg; 522 | 523 | msg.device_index = device_index; 524 | if (is_long_req) { 525 | msg.report_id = LONG_MESSAGE; 526 | exp_sub_id = SUB_SET_LONG_REGISTER; 527 | msg.msg_long.address = address; 528 | memcpy(&msg.msg_long.str, params, sizeof msg.msg_long.str); 529 | } else { 530 | msg.report_id = SHORT_MESSAGE; 531 | exp_sub_id = SUB_SET_REGISTER; 532 | msg.msg_short.address = address; 533 | memcpy(&msg.msg_short.value, params, sizeof msg.msg_short.value); 534 | } 535 | msg.sub_id = exp_sub_id; 536 | 537 | if (!do_write(fd, &msg)) { 538 | return false; 539 | } 540 | 541 | msg.report_id = SHORT_MESSAGE; 542 | if (!do_read_skippy(fd, &msg, SHORT_MESSAGE, exp_sub_id)) { 543 | return false; 544 | } 545 | memcpy(res, &msg, sizeof msg); 546 | return true; 547 | } 548 | 549 | bool set_short_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *res) { 550 | return set_register(fd, device_index, address, params, res, false); 551 | } 552 | bool set_long_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *res) { 553 | return set_register(fd, device_index, address, params, res, true); 554 | } 555 | 556 | static bool get_register(int fd, u8 device_index, u8 address, 557 | struct hidpp_message *out, u8 *params, bool is_long_resp) { 558 | u8 exp_report_id, exp_sub_id; 559 | struct hidpp_message msg; 560 | 561 | exp_report_id = is_long_resp ? LONG_MESSAGE : SHORT_MESSAGE; 562 | exp_sub_id = is_long_resp ? SUB_GET_LONG_REGISTER : SUB_GET_REGISTER; 563 | 564 | msg.report_id = SHORT_MESSAGE; 565 | msg.device_index = device_index; 566 | msg.sub_id = exp_sub_id; 567 | memset(msg.msg_short.value, 0, sizeof msg.msg_short.value); 568 | msg.msg_short.address = address; 569 | if (params) { 570 | memcpy(&msg.msg_short.value, params, sizeof msg.msg_short.value); 571 | } 572 | 573 | if (!do_write(fd, &msg)) { 574 | return false; 575 | } 576 | 577 | if (!do_read_skippy(fd, &msg, exp_report_id, exp_sub_id)) { 578 | return false; 579 | } 580 | memcpy(out, &msg, sizeof msg); 581 | return true; 582 | } 583 | 584 | bool get_short_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *out) { 585 | return get_register(fd, device_index, address, out, params, false); 586 | } 587 | bool get_long_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *out) { 588 | return get_register(fd, device_index, address, out, params, true); 589 | } 590 | 591 | bool get_info(int fd, struct hidpp_message *msg) { 592 | if (!get_long_register(fd, DEVICE_RECEIVER, REG_DEVICE_ACTIVITY, NULL, msg)) { 593 | return false; 594 | } 595 | return true; 596 | } 597 | 598 | // begin directly-usable functions 599 | bool get_notifications(int fd, u8 device_index, struct msg_enable_notifs *params) { 600 | struct hidpp_message msg; 601 | if (!get_short_register(fd, device_index, REG_ENABLED_NOTIFS, NULL, &msg)) { 602 | return false; 603 | } 604 | memcpy((u8 *) params, &msg.msg_short.value, sizeof *params); 605 | return true; 606 | } 607 | bool set_notifications(int fd, u8 device_index, struct msg_enable_notifs *params) { 608 | struct hidpp_message msg; 609 | if (!set_short_register(fd, device_index, REG_ENABLED_NOTIFS, (u8 *) params, &msg)) { 610 | return false; 611 | } 612 | return true; 613 | } 614 | bool get_and_print_notifications(int fd, u8 device_index, struct msg_enable_notifs *notifsp) { 615 | putchar('\n'); 616 | if (get_notifications(fd, device_index, notifsp)) { 617 | u8 flags = notifsp->reporting_flags_receiver; 618 | printf("Reporting Flags (Receiver) = %02x\n", flags & 0xFF); 619 | printf("Wireless notifications = %s\n", flags & 1 ? "yes" : "no"); 620 | printf("Software Present = %s\n", flags & 4 ? "yes" : "no"); 621 | return true; 622 | } else { 623 | fprintf(stderr, "Failed to get HID++ Notification status\n"); 624 | return false; 625 | } 626 | } 627 | 628 | bool get_connected_devices(int fd, u8 *devices_count) { 629 | struct hidpp_message msg; 630 | struct val_reg_connection_state *cval; 631 | if (!get_short_register(fd, DEVICE_RECEIVER, REG_CONNECTION_STATE, NULL, &msg)) { 632 | return false; 633 | } 634 | cval = (struct val_reg_connection_state *) msg.msg_short.value; 635 | *devices_count = cval->connected_devices_count; 636 | return true; 637 | } 638 | 639 | bool pair_start(int fd, u8 timeout) { 640 | struct hidpp_message msg; 641 | struct val_reg_devpair cmd; 642 | cmd.action = DEVPAIR_OPEN_LOCK; 643 | // device_index is 1..6 for a specific device, 0x53 is seen for "any 644 | // device". Not sure if this is a special value or randomly chosen 645 | cmd.device_number = 0; 646 | cmd.open_lock_timeout = timeout; 647 | if (!set_short_register(fd, DEVICE_RECEIVER, REG_DEVICE_PAIRING, (u8 *) &cmd, &msg)) { 648 | return false; 649 | } 650 | return true; 651 | } 652 | 653 | bool pair_cancel(int fd) { 654 | struct hidpp_message msg; 655 | struct val_reg_devpair cmd; 656 | cmd.action = DEVPAIR_CLOSE_LOCK; 657 | // see discussion at pair_start, why did logitech use 0x53? Confusion? 658 | cmd.device_number = 0; 659 | // timeout applies to open lock, not sure why I saw 0x94 (148 sec) 660 | cmd.open_lock_timeout = 0; 661 | if (!set_short_register(fd, DEVICE_RECEIVER, REG_DEVICE_PAIRING, (u8 *) &cmd, &msg)) { 662 | return false; 663 | } 664 | return true; 665 | } 666 | 667 | bool device_unpair(int fd, u8 device_index) { 668 | struct hidpp_message msg; 669 | struct val_reg_devpair cmd; 670 | cmd.action = DEVPAIR_DISCONNECT; 671 | cmd.device_number = device_index; 672 | cmd.open_lock_timeout = 0; 673 | if (!set_short_register(fd, DEVICE_RECEIVER, REG_DEVICE_PAIRING, (u8 *) &cmd, &msg)) { 674 | return false; 675 | } 676 | return true; 677 | } 678 | 679 | void perform_pair(int fd, u8 timeout) { 680 | struct hidpp_message msg; 681 | if (timeout == 0) { 682 | timeout = 30; 683 | } 684 | if (!pair_start(fd, timeout)) { 685 | fprintf(stderr, "Failed to send pair request\n"); 686 | return; 687 | } 688 | puts("Please turn your wireless device off and on to start pairing."); 689 | // WARNING: mess ahead. I knew it would become messy before writing it. 690 | for (;;) { 691 | if (!do_read(fd, &msg, SHORT_MESSAGE, timeout * 1000 + 2000)) { 692 | fprintf(stderr, "Failed to read short message\n"); 693 | break; 694 | } 695 | if (msg.sub_id == NOTIF_RECV_LOCK_CHANGE) { 696 | u8 *bytes = (u8 *) &msg.msg_short; 697 | if (msg.report_id != SHORT_MESSAGE || msg.device_index != DEVICE_RECEIVER) { 698 | fprintf(stderr, "Received invalid Unifying Receiver Locking Change notification (0x4A)\n"); 699 | continue; 700 | } 701 | if (bytes[0] & 1) { // locking open 702 | continue; 703 | } else { // locking closed 704 | const char *result; 705 | switch (bytes[1]) { 706 | case 0x00: 707 | result = "Success"; 708 | break; 709 | case 0x01: 710 | result = "Timeout"; 711 | break; 712 | default: 713 | result = "Failure"; 714 | } 715 | printf("Pairing result: %s (%i)\n", result, bytes[1]); 716 | break; 717 | } 718 | } else if (msg.sub_id == NOTIF_DEV_CONNECT) { 719 | u8 device_index; 720 | bool is_new_dev; 721 | if (!process_notif_dev_connect(&msg, &device_index, &is_new_dev)) { 722 | // error message is already emitted 723 | continue; 724 | } 725 | 726 | if (device_index < 1 || device_index > DEVICES_MAX) { 727 | fprintf(stderr, "Invalid device index %#04x\n", device_index); 728 | } else if (is_new_dev) { 729 | u8 device_type = devices[device_index - 1].device_type; 730 | printf("Found new device, id=%#04x %s\n", 731 | device_index, 732 | device_type_str(device_type)); 733 | break; 734 | } else { 735 | printf("Ignoring existent device id=%#04x\n", device_index); 736 | } 737 | } 738 | } 739 | // end of mess 740 | if (!pair_cancel(fd)) { 741 | fprintf(stderr, "Failed to cancel pair visibility\n"); 742 | } 743 | } 744 | void perform_unpair(int fd, u8 device_index) { 745 | struct device *dev = &devices[device_index - 1]; 746 | u8 dev_device_type = dev->device_type; // will be overwritten, therefore store it 747 | if (!dev->device_present) { 748 | printf("Device %#04x does not appear to be paired\n", device_index); 749 | return; 750 | } 751 | if (device_unpair(fd, device_index)) { 752 | if (!dev->device_present) { 753 | printf("Device %#04x %s successfully unpaired\n", device_index, 754 | device_type_str(dev_device_type)); 755 | } else { 756 | fprintf(stderr, "Unpairing of %#04x possibly failed\n", device_index); 757 | } 758 | } else { 759 | fprintf(stderr, "Failed to send unpair %#04x request\n", device_index); 760 | } 761 | } 762 | 763 | // triggers a notification that updates the list of paired devices 764 | bool get_all_devices(int fd) { 765 | struct hidpp_message msg; 766 | struct val_reg_connection_state cval; 767 | memset(&cval, 0, sizeof cval); 768 | cval.action = CONSTATE_ACTION_LIST_DEVICES; 769 | if (!set_short_register(fd, DEVICE_RECEIVER, REG_CONNECTION_STATE, (u8 *) &cval, &msg)) { 770 | return false; 771 | } 772 | return true; 773 | } 774 | bool get_receiver_info(int fd, struct receiver_info *rinfo) { 775 | struct hidpp_message msg; 776 | u8 params[3] = {0}; 777 | 778 | params[0] = 0x03; // undocumented 779 | if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { 780 | struct msg_receiver_info *info = (struct msg_receiver_info *) &msg.msg_long.str; 781 | uint32_t serial_number; 782 | 783 | memcpy(&serial_number, &info->serial_number, sizeof(serial_number)); 784 | rinfo->serial_number = ntohl(serial_number); 785 | return true; 786 | } 787 | return false; 788 | } 789 | bool get_device_pair_info(int fd, u8 device_index) { 790 | struct device *dev = &devices[device_index - 1]; 791 | struct hidpp_message msg; 792 | u8 params[3] = {0}; 793 | 794 | params[0] = 0x20 | (device_index - 1); // 0x20..0x2F Unifying Device pairing info 795 | if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { 796 | struct msg_dev_pair_info *info = (struct msg_dev_pair_info *) &msg.msg_long.str; 797 | 798 | dev->wireless_pid = (info->pid_msb << 8) | info->pid_lsb; 799 | dev->device_type = info->device_type; 800 | return true; 801 | } 802 | return false; 803 | } 804 | bool get_device_ext_pair_info(int fd, u8 device_index) { 805 | struct device *dev = &devices[device_index - 1]; 806 | struct hidpp_message msg; 807 | u8 params[3] = {0}; 808 | 809 | params[0] = 0x30 | (device_index - 1); // 0x30..0x3F Unifying Device extended pairing info 810 | if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { 811 | struct msg_dev_ext_pair_info *info; 812 | uint32_t serial_number; 813 | 814 | info = (struct msg_dev_ext_pair_info *) &msg.msg_long.str; 815 | memcpy(&serial_number, &info->serial_number, sizeof(serial_number)); 816 | dev->serial_number = ntohl(serial_number); 817 | dev->power_switch_location = info->usability_info & 0x0F; 818 | return true; 819 | } 820 | return false; 821 | } 822 | bool get_device_name(int fd, u8 device_index) { 823 | struct device *dev = &devices[device_index - 1]; 824 | struct hidpp_message msg; 825 | u8 params[3] = {0}; 826 | 827 | params[0] = 0x40 | (device_index - 1); // 0x40..0x4F Unifying Device Name 828 | if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { 829 | struct msg_dev_name *name = (struct msg_dev_name *) &msg.msg_long.str; 830 | if (name->length > DEVICE_NAME_MAXLEN) { 831 | fprintf(stderr, "Invalid name length %#04x for idx=%i\n", name->length, device_index); 832 | return false; 833 | } 834 | 835 | memcpy(&dev->name, name->str, name->length); 836 | dev->name[name->length] = 0; 837 | return true; 838 | } 839 | return false; 840 | } 841 | 842 | bool get_hidpp_version(int fd, u8 device_index, struct hidpp_version *version) { 843 | struct hidpp_message msg; 844 | struct msg_short *payload = &msg.msg_short; 845 | u8 softwareId = 0x04; // value 1..15 - random choice for 0x4 846 | u8 ping_data = 0x00; // can be any value 847 | 848 | msg.report_id = SHORT_MESSAGE; 849 | msg.device_index = device_index; 850 | msg.sub_id = 0x00; // Root feature index 851 | 852 | memset(payload->value, 0, sizeof payload->value); 853 | payload->address = 0x10 | softwareId; 854 | payload->value[2] = ping_data; 855 | if (!do_write(fd, &msg)) { 856 | return false; 857 | } 858 | for (;;) { 859 | if (!do_read(fd, &msg, 0, 3000)) { 860 | if (debug_enabled) { 861 | fprintf(stderr, "Failed to read HID++ version, device does not respond!\n"); 862 | } 863 | return false; 864 | } 865 | if (msg.sub_id == 0x8F) { 866 | struct msg_error *error = (struct msg_error *) &msg.msg_error; 867 | if (error->sub_id == 0x00 && error->address == (0x10 | softwareId)) { 868 | // if error is ERR_INVALID_SUBID (0x01), then HID++ 1.0 869 | if (error->error_code == 0x01) { 870 | version->major = 1; 871 | version->minor = 0; 872 | return true; 873 | } else if (debug_enabled) { 874 | const char *err_str = error_messages[error->error_code]; 875 | fprintf(stderr, "Failed to retrieve version: %#04x (%s)\n", 876 | error->error_code, err_str); 877 | } 878 | // fatal error - is device connected? 879 | return false; 880 | } 881 | // ignore other errors 882 | } 883 | if (msg.sub_id == 0x00 && (payload->address & 0xF) == softwareId && 884 | payload->value[2] == ping_data) { 885 | break; // I think we got a version 886 | } else if (debug_enabled) { 887 | fprintf(stderr, "Ignoring sub_id=%02x\n", msg.sub_id); 888 | } 889 | // ignore other messages 890 | } 891 | 892 | version->major = payload->value[0]; 893 | version->minor = payload->value[1]; 894 | return true; 895 | } 896 | 897 | // device_index can also be 0xFF for receiver 898 | bool get_device_version(int fd, u8 device_index, u8 version_type, struct val_reg_version *ver) { 899 | struct hidpp_message msg; 900 | // TODO: not 100% reliable for wireless devices, it may return MSG_ERR 901 | // (err=SUCCESS, wtf). Perhaps we need to send another msg type=00 902 | // (whatever the undocumented params are). 903 | 904 | memset(ver, 0, sizeof *ver); 905 | ver->select_field = version_type; 906 | if (get_short_register(fd, device_index, REG_VERSION_INFO, (u8 *) ver, &msg)) { 907 | memcpy(ver, msg.msg_short.value, sizeof *ver); 908 | return true; 909 | } 910 | return false; 911 | } 912 | 913 | bool get_device_versions(int fd, u8 device_index, struct version *version) { 914 | struct val_reg_version ver; 915 | 916 | memset(version, 0, sizeof *version); 917 | 918 | if (get_device_version(fd, device_index, VERSION_FIRMWARE, &ver)) { 919 | version->fw_major = ver.v1; 920 | version->fw_minor = ver.v2; 921 | } else { 922 | // assume that other versions will fail too 923 | return false; 924 | } 925 | if (get_device_version(fd, device_index, VERSION_FW_BUILD, &ver)) { 926 | version->fw_build = (ver.v1 << 8) | ver.v2; 927 | } 928 | //if (get_device_version(fd, device_index, 3, &ver)) puts("No idea what this is useful for"); 929 | if (get_device_version(fd, device_index, VERSION_BOOTLOADER, &ver)) { 930 | version->bl_major = ver.v1; 931 | version->bl_minor = ver.v2; 932 | } 933 | return true; 934 | } 935 | 936 | // device index is 1..6 937 | void gather_device_info(int fd, u8 device_index) { 938 | if (get_device_pair_info(fd, device_index)) { 939 | struct device *dev = &devices[device_index - 1]; 940 | 941 | dev->device_present = true; 942 | 943 | get_hidpp_version(fd, device_index, &dev->hidpp_version); 944 | get_device_ext_pair_info(fd, device_index); 945 | get_device_name(fd, device_index); 946 | if (dev->hidpp_version.major == 1 && dev->hidpp_version.minor == 0) { 947 | if (get_device_versions(fd, device_index, &dev->version)) { 948 | dev->device_available = true; 949 | } 950 | } else { 951 | // TODO: hid++20 support 952 | } 953 | } else { 954 | // retrieve some information from notifier 955 | get_all_devices(fd); 956 | } 957 | } 958 | 959 | void print_versions(struct version *ver) { 960 | // versions are shown as hex. Probably a mistake given that the length 961 | // is 3 which can fit 255 instead of FF (and 65535 instead of FF) 962 | printf("Firmware version: %03x.%03x.%05x\n", 963 | ver->fw_major, ver->fw_minor, ver->fw_build); 964 | printf("Bootloader version: BL.%03x.%03x\n", 965 | ver->bl_major, ver->bl_minor); 966 | } 967 | 968 | void get_and_print_recv_info(int fd) { 969 | if (get_receiver_info(fd, &receiver)) { 970 | printf("Serial number: %08X\n", receiver.serial_number); 971 | } 972 | if (get_device_versions(fd, DEVICE_RECEIVER, &receiver.version)) { 973 | print_versions(&receiver.version); 974 | } 975 | } 976 | 977 | void print_detailed_device(u8 device_index) { 978 | struct device *dev = &devices[device_index - 1]; 979 | 980 | if (!dev->device_present) { 981 | printf("Device %i is not paired\n", device_index); 982 | return; 983 | } 984 | 985 | if (dev->hidpp_version.major) { 986 | printf("HID++ version: %i.%i\n", dev->hidpp_version.major, dev->hidpp_version.minor); 987 | } else { 988 | puts("HID++ version: unknown"); 989 | } 990 | printf("Device index %i\n", device_index); 991 | printf("%s\n", device_type_str(dev->device_type)); 992 | printf("Name: %s\n", dev->name); 993 | printf("Wireless Product ID: %04X\n", dev->wireless_pid); 994 | printf("Serial number: %08X\n", dev->serial_number); 995 | if (dev->device_available) { 996 | print_versions(&dev->version); 997 | } else { 998 | puts("Device was unavailable, version information not available."); 999 | } 1000 | } 1001 | void get_device_names(int fd) { 1002 | u8 i; 1003 | 1004 | for (i=0; idevice_present) { 1007 | continue; 1008 | } 1009 | 1010 | if (!get_device_name(fd, i + 1)) { 1011 | fprintf(stderr, "Failed to read device name for idx=%i\n", i + 1); 1012 | } 1013 | } 1014 | } 1015 | 1016 | static void print_version(void) { 1017 | fprintf(stderr, 1018 | "Logitech Unifying tool version " PACKAGE_VERSION "\n" 1019 | "Copyright (C) 2013 Peter Wu \n"); 1020 | } 1021 | 1022 | void print_all_devices(void) { 1023 | unsigned i; 1024 | puts("Connected devices:"); 1025 | for (i=0; idevice_present) { 1028 | continue; 1029 | } 1030 | 1031 | printf("idx=%i\t%s\t%s\n", i + 1, 1032 | device_type_str(dev->device_type), dev->name); 1033 | } 1034 | } 1035 | 1036 | static void print_usage(const char *program_name) { 1037 | fprintf(stderr, "Usage: %s [options] cmd [cmd options]\n", 1038 | program_name); 1039 | print_version(); 1040 | fprintf(stderr, 1041 | "\n" 1042 | "Generic options:\n" 1043 | " -d, --device path Bypass detection, specify custom hidraw device.\n" 1044 | " -D Print debugging information\n" 1045 | " -h, --help Show this help message\n" 1046 | "\n" 1047 | "Commands:\n" 1048 | " list - show all paired devices\n" 1049 | " pair [timeout] - Try to pair within \"timeout\" seconds (1 to 255,\n" 1050 | " default 0 which is an alias for 30s)\n" 1051 | " unpair idx - Unpair device\n" 1052 | " info idx - Show more detailed information for a device\n" 1053 | " receiver-info - Show information about the receiver\n" 1054 | "In the above lines, \"idx\" refers to the device number shown in the\n" 1055 | " first column of the list command (between 1 and 6). Alternatively, you\n" 1056 | " can use the following names (case-insensitive):\n"); 1057 | print_device_types(); 1058 | } 1059 | 1060 | static bool is_numeric_device_index(const char *str) { 1061 | char *end; 1062 | unsigned long device_index = strtoul(str, &end, 0); 1063 | 1064 | // if a number was found, there must be no other characters thereafter 1065 | return !*end && 1066 | device_index >= 1 && device_index <= DEVICES_MAX; 1067 | } 1068 | 1069 | // Return number of commands and command arguments, -1 on error. If the program 1070 | // should not run (--help), then 0 is returned and args is NULL. 1071 | static int validate_args(int argc, char **argv, char ***argsp, char **hidraw_path) { 1072 | int args_count; 1073 | char *cmd; 1074 | int opt; 1075 | char **args; 1076 | struct option longopts[] = { 1077 | { "device", 1, NULL, 'd' }, 1078 | { "help", 0, NULL, 'h' }, 1079 | { "version", 0, NULL, 'V' }, 1080 | { 0, 0, 0, 0 }, 1081 | }; 1082 | 1083 | *argsp = NULL; 1084 | 1085 | while ((opt = getopt_long(argc, argv, "+Dd:hV", longopts, NULL)) != -1) { 1086 | switch (opt) { 1087 | case 'D': 1088 | debug_enabled = true; 1089 | break; 1090 | case 'd': 1091 | *hidraw_path = optarg; 1092 | break; 1093 | case 'V': 1094 | print_version(); 1095 | return 0; 1096 | case 'h': 1097 | print_usage(*argv); 1098 | return 0; 1099 | default: 1100 | return -1; 1101 | } 1102 | } 1103 | 1104 | if (optind >= argc) { 1105 | // missing command 1106 | print_usage(*argv); 1107 | return -1; 1108 | } 1109 | *argsp = args = &argv[optind]; 1110 | args_count = argc - optind - 1; 1111 | 1112 | cmd = args[0]; 1113 | if (!strcmp(cmd, "list") || !strcmp(cmd, "receiver-info")) { 1114 | /* nothing to check */ 1115 | } else if (!strcmp(cmd, "pair")) { 1116 | if (args_count >= 1) { 1117 | char *end; 1118 | unsigned long int n; 1119 | n = strtoul(args[1], &end, 0); 1120 | if (*end != '\0' || n > 0xFF) { 1121 | fprintf(stderr, "Timeout must be a number between 0 and 255\n"); 1122 | return -1; 1123 | } 1124 | } 1125 | } else if (!strcmp(cmd, "unpair") || !strcmp(cmd, "info")) { 1126 | if (args_count < 1) { 1127 | fprintf(stderr, "%s requires a device index\n", cmd); 1128 | return -1; 1129 | } 1130 | if (!is_numeric_device_index(args[1]) && 1131 | device_type_from_str(args[1]) == -1) { 1132 | fprintf(stderr, "Invalid device type, must be a numeric index or:\n"); 1133 | print_device_types(); 1134 | return -1; 1135 | } 1136 | } else { 1137 | fprintf(stderr, "Unrecognized command: %s\n", cmd); 1138 | return -1; 1139 | } 1140 | return args_count; 1141 | } 1142 | 1143 | #ifdef __GNUC__ 1144 | static FILE *fopen_format(const char *format, ...) 1145 | __attribute__((format(printf, 1, 2))); 1146 | #endif 1147 | 1148 | static FILE *fopen_format(const char *format, ...) { 1149 | char buf[1024]; 1150 | va_list ap; 1151 | 1152 | va_start(ap, format); 1153 | vsnprintf(buf, sizeof buf, format, ap); 1154 | va_end(ap); 1155 | return fopen(buf, "r"); 1156 | } 1157 | 1158 | #define RECEIVER_NAME "logitech-djreceiver" 1159 | int open_hidraw(void) { 1160 | int fd = -1; 1161 | glob_t matches; 1162 | char hiddev_name[32] = {0}; 1163 | 1164 | if (!glob("/sys/class/hidraw/hidraw*/device/driver", 0, NULL, &matches)) { 1165 | size_t i; 1166 | char buf[1024]; 1167 | for (i = 0; i < matches.gl_pathc; i++) { 1168 | ssize_t r; 1169 | char *name = matches.gl_pathv[i]; 1170 | const char *last_comp; 1171 | char *dev_name; 1172 | FILE *fp; 1173 | uint32_t vid = 0, pid = 0; 1174 | 1175 | r = readlink(name, buf, (sizeof buf) - 1); 1176 | if (r < 0) { 1177 | perror(name); 1178 | continue; 1179 | } 1180 | 1181 | buf[r] = 0; /* readlink does not NUL-terminate */ 1182 | last_comp = basename(buf); 1183 | 1184 | /* retrieve 'hidrawX' name */ 1185 | dev_name = name + sizeof "/sys/class/hidraw"; 1186 | *(strchr(dev_name, '/')) = 0; 1187 | 1188 | // Assume that the first match is the receiver. Devices bound to the 1189 | // same receiver may have the same modalias. 1190 | if ((fp = fopen_format("/sys/class/hidraw/%s/device/modalias", dev_name))) { 1191 | int m = fscanf(fp, "hid:b%*04Xg%*04Xv%08Xp%08X", &vid, &pid); 1192 | if (m != 2) { 1193 | pid = 0; 1194 | } 1195 | fclose(fp); 1196 | } 1197 | if (vid != VID_LOGITECH) { 1198 | continue; 1199 | } 1200 | 1201 | if (!strcmp(last_comp, RECEIVER_NAME)) { 1202 | /* Logitech receiver c52b and c532 - pass. 1203 | * 1204 | * Logitech Nano receiver c534 however has 1205 | * multiple hidraw devices, but the first one is 1206 | * only used for keyboard events and should be 1207 | * ignored. The second one is for the mouse, and 1208 | * that interface has a vendor-specific HID page 1209 | * for HID++. 1210 | * Parsing .../device/report_descriptor is much 1211 | * more complicated, so stick with knowledge 1212 | * about device-specific interfaces for now. 1213 | */ 1214 | if (pid == PID_NANO_RECEIVER_2) { 1215 | int iface = -1; 1216 | if ((fp = fopen_format("/sys/class/hidraw/%s/device/../bInterfaceNumber", dev_name))) { 1217 | int m = fscanf(fp, "%02x", &iface); 1218 | if (m != 1) { 1219 | iface = -1; 1220 | } 1221 | fclose(fp); 1222 | } 1223 | if (iface == 0) { 1224 | /* Skip first interface. */ 1225 | continue; 1226 | } 1227 | } 1228 | } else if (!strcmp(last_comp, "hid-generic")) { 1229 | /* need to test for older nano receiver c52f */ 1230 | if (pid != PID_NANO_RECEIVER) { 1231 | continue; 1232 | } 1233 | } else { /* unknown driver */ 1234 | continue; 1235 | } 1236 | 1237 | snprintf(hiddev_name, sizeof hiddev_name, "/dev/%s", dev_name); 1238 | fd = open(hiddev_name, O_RDWR); 1239 | if (fd < 0) { 1240 | perror(hiddev_name); 1241 | } else { 1242 | break; 1243 | } 1244 | } 1245 | } 1246 | 1247 | if (fd < 0) { 1248 | if (*hiddev_name) { 1249 | fprintf(stderr, "Logitech Unifying Receiver device is not accessible.\n" 1250 | "Try running this program as root or enable read/write permissions\n" 1251 | "for %s\n", hiddev_name); 1252 | } else { 1253 | fprintf(stderr, "No Logitech Unifying Receiver device found\n"); 1254 | if (access("/sys/class/hidraw", R_OK)) { 1255 | fputs("The kernel must have CONFIG_HIDRAW enabled.\n", 1256 | stderr); 1257 | } 1258 | if (access("/sys/module/hid_logitech_dj", F_OK)) { 1259 | fprintf(stderr, "Driver is not loaded, try:" 1260 | " sudo modprobe hid-logitech-dj\n"); 1261 | } 1262 | } 1263 | } 1264 | globfree(&matches); 1265 | 1266 | return fd; 1267 | } 1268 | 1269 | // returns device index starting at 1 or 0 on failure 1270 | static u8 find_device_index_for_type(int fd, const char *str, bool *fetched_devices) { 1271 | char *end; 1272 | u8 device_index; 1273 | 1274 | device_index = strtoul(str, &end, 0); 1275 | if (*end == '\0') { 1276 | return device_index; 1277 | } 1278 | 1279 | if (get_all_devices(fd)) { 1280 | u8 i; 1281 | int device_type_n; 1282 | 1283 | device_type_n = device_type_from_str(str); 1284 | if (fetched_devices) { 1285 | *fetched_devices = true; 1286 | } 1287 | 1288 | for (i = 0; i < DEVICES_MAX; i++) { 1289 | if (devices[i].device_type == device_type_n) { 1290 | return i + 1; 1291 | } 1292 | } 1293 | } else { 1294 | fprintf(stderr, "Unable to request a list of paired devices"); 1295 | } 1296 | return 0; 1297 | } 1298 | 1299 | int main(int argc, char **argv) { 1300 | int fd; 1301 | struct msg_enable_notifs notifs; 1302 | char *cmd, **args; 1303 | int args_count; 1304 | char *hidraw_path = NULL; 1305 | bool disable_notifs = false; 1306 | 1307 | args_count = validate_args(argc, argv, &args, &hidraw_path); 1308 | if (args_count < 0) { 1309 | return 1; 1310 | } else if (args == NULL) { 1311 | return 0; 1312 | } 1313 | cmd = args[0]; 1314 | 1315 | if (hidraw_path) { 1316 | fd = open(hidraw_path, O_RDWR); 1317 | if (fd < 0) { 1318 | perror(hidraw_path); 1319 | } 1320 | } else { 1321 | fd = open_hidraw(); 1322 | } 1323 | if (fd < 0) { 1324 | return 1; 1325 | } 1326 | 1327 | if (debug_enabled) { 1328 | if (!get_and_print_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { 1329 | goto end_close; 1330 | } 1331 | } else { 1332 | if (!get_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { 1333 | fprintf(stderr, "Failed to retrieve notification state\n"); 1334 | goto end_close; 1335 | } 1336 | } 1337 | 1338 | if (!notifs.reporting_flags_receiver) { 1339 | disable_notifs = true; 1340 | notifs.reporting_flags_receiver |= 1; 1341 | if (set_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { 1342 | if (debug_enabled) { 1343 | puts("Successfully enabled notifications"); 1344 | } 1345 | } else { 1346 | fprintf(stderr, "Failed to set HID++ Notification status\n"); 1347 | } 1348 | } 1349 | 1350 | if (!strcmp(cmd, "pair")) { 1351 | u8 timeout = 0; 1352 | if (args_count >= 1) { 1353 | timeout = (u8) strtoul(args[1], NULL, 0); 1354 | } 1355 | perform_pair(fd, timeout); 1356 | } else if (!strcmp(cmd, "unpair")) { 1357 | bool fetched_devices = false; 1358 | u8 device_index; 1359 | device_index = find_device_index_for_type(fd, args[1], &fetched_devices); 1360 | if (!fetched_devices && !get_all_devices(fd)) { 1361 | fprintf(stderr, "Unable to request a list of paired devices\n"); 1362 | } 1363 | if (device_index) { 1364 | perform_unpair(fd, device_index); 1365 | } else { 1366 | fprintf(stderr, "Device %s not found\n", args[1]); 1367 | } 1368 | } else if (!strcmp(cmd, "list")) { 1369 | u8 device_count; 1370 | if (get_connected_devices(fd, &device_count)) { 1371 | printf("Devices count: %i\n", device_count); 1372 | } else { 1373 | fprintf(stderr, "Failed to get connected devices count\n"); 1374 | } 1375 | 1376 | if (get_all_devices(fd)) { 1377 | get_device_names(fd); 1378 | print_all_devices(); 1379 | } else { 1380 | fprintf(stderr, "Unable to request a list of paired devices\n"); 1381 | } 1382 | } else if (!strcmp(cmd, "info")) { 1383 | u8 device_index; 1384 | 1385 | device_index = find_device_index_for_type(fd, args[1], NULL); 1386 | if (device_index) { 1387 | struct device *dev = &devices[device_index - 1]; 1388 | gather_device_info(fd, device_index); 1389 | print_detailed_device(device_index); 1390 | if (dev->hidpp_version.major == 2 && dev->hidpp_version.minor == 0) { 1391 | // TODO: separate fetch/print 1392 | hidpp20_print_features(fd, device_index); 1393 | } 1394 | } else { 1395 | fprintf(stderr, "Device %s not found\n", args[1]); 1396 | } 1397 | } else if (!strcmp(cmd, "receiver-info")) { 1398 | get_and_print_recv_info(fd); 1399 | } else { 1400 | fprintf(stderr, "Unhandled command: %s\n", cmd); 1401 | } 1402 | 1403 | if (disable_notifs) { 1404 | notifs.reporting_flags_receiver &= ~1; 1405 | if (set_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { 1406 | if (debug_enabled) { 1407 | puts("Successfully disabled notifications"); 1408 | } 1409 | } else { 1410 | fprintf(stderr, "Failed to set HID++ Notification status\n"); 1411 | } 1412 | } 1413 | 1414 | if (debug_enabled) { 1415 | get_and_print_notifications(fd, DEVICE_RECEIVER, ¬ifs); 1416 | } 1417 | 1418 | end_close: 1419 | close(fd); 1420 | 1421 | return 0; 1422 | } 1423 | --------------------------------------------------------------------------------