pax_global_header00006660000000000000000000000064136715046750014530gustar00rootroot0000000000000052 comment=872a781e07c079b880e36afd5b35504aabd5f7b1 ltunify-0.3/000077500000000000000000000000001367150467500130645ustar00rootroot00000000000000ltunify-0.3/.gitignore000066400000000000000000000000461367150467500150540ustar00rootroot00000000000000.*.sw* read-dev-usbmon hidraw ltunify ltunify-0.3/Makefile000066400000000000000000000022251367150467500145250ustar00rootroot00000000000000CFLAGS ?= -g -O2 -Wall -Wextra -D_FORTIFY_SOURCE=2 -fstack-protector --param ssp-buffer-size=4 # for install-home BINDIR ?= $(HOME)/bin # for install and uninstall DESTDIR ?= bindir ?= /usr/local/bin udevrulesdir ?= /etc/udev/rules.d udevrule = 42-logitech-unify-permissions.rules PACKAGE_VERSION ?= $(shell git describe --dirty 2>/dev/null | sed s/^v//) ifeq (PACKAGE_VERSION, "") LTUNIFY_DEFINES := else LTUNIFY_DEFINES := -DPACKAGE_VERSION=\"$(PACKAGE_VERSION)\" endif %: %.c $(CC) $(CFLAGS) -o $(OUTDIR)$@ $< all: ltunify read-dev-usbmon read-dev-usbmon: read-dev-usbmon.c hidraw.c ltunify: ltunify.c hidpp20.c $(CC) $(CFLAGS) -o $(OUTDIR)$@ $< -lrt $(LTUNIFY_DEFINES) .PHONY: all clean install-home install install-udevrule uninstall clean: rm -f ltunify read-dev-usbmon hidraw install-home: ltunify install -m755 -D ltunify $(BINDIR)/ltunify # System-wide installation install: ltunify install-udevrule install -m755 -D ltunify $(DESTDIR)$(bindir)/ltunify install-udevrule: udev/$(udevrule) install -m644 -D udev/$(udevrule) $(DESTDIR)$(udevrulesdir)/$(udevrule) uninstall: $(RM) $(DESTDIR)$(bindir)/ltunify $(DESTDIR)$(udevrulesdir)/$(udevrule) ltunify-0.3/NEWS000066400000000000000000000020211367150467500135560ustar00rootroot00000000000000Version 0.3 - 14 June 2020 * ltunify: fix device version reporting for HID++ 1.0 devices. * ltunify: ignore DJ reports that resulted in messages such as "Dev conn notif is expected to be short, got 0x20 instead". * ltunify: report some more HID++ 2.0 feature names. * ltunify: fix failures when another event (such as touchpad motions) occur between sending the command and reading the response. * ltunify: fix crash on unrecognised command-line parameter. * ltunify: add --version option. * ltunify: add support for Nano Receiver c534 as used by the MK270 combo. Version 0.2 - 23 July 2013 * ltunify: display HID++ version and supported HID++ 2.0 features for `info` option. * ltunify: print HID++ version of receiver. * ltunify: support c52f Nano receiver. * udev: use uaccess to allow seated users to login without joining a group. * Add various installation targets and variables to Makefile. * Updated registers.txt and monitoring tools with battery and keyboard information. Version 0.1 - 25 April 2013 * Initial release. ltunify-0.3/README.txt000066400000000000000000000055271367150467500145730ustar00rootroot00000000000000Logitech Unifying tool for Linux See also the article on Logitech documents I have learned a bit from the kernel source code hid-logitech-dj, but the "official" Logitech specification (HID++ 1.0) was much more useful. These documents can be found on . Debuggers usbmon.awk - initial debugging tool used for tapping usbmon from debugfs hidraw.c - successor of usbmon.awk that can parse packets of usb payload. read-dev-usbmon.c - Reads data from /dev/usbmonX and show interpreted data in a more human-readable way. Note: as a quick-n-dirty hack, I included hidraw.c at some point into the read-dev-usbmon program. Otherwise, I had no way to show the difference between a send or receive packet without adding to the same stdout stream. If I included it in the stderr pipe, then it would be interleaved with stdout in an unpredictable manner. This means that hidraw.c is currently unusable, it does not process data correctly. Usage of USB debugger: 1. Use `lsusb -d 046d:c52b` to determine the bus number. If the output is "Bus 001 ..", your usb monitor device is at /dev/usbmon1. 2. sudo chgrp $USER /dev/usbmon1 3. sudo chmod g+r /dev/usbmon1 4. ./read-dev-usbmon /dev/usbmon1 5. Profit! Pairing tool (ltunify) ltunify allows you to pair new devices, unpair existing devices or view information for those devices. In order to build the ltunify binary and install it to `$HOME/bin/ltunify`: make ltunify make install-home If you intend to package ltunify or otherwise install it system-wide with a udevrule, you can use: make ltunify make install DESTDIR=$pkgdir bindir=/usr/bin udevrulesdir=/lib/udev/rules.d Once installed, run `ltunify --help` for available options. Usage of the pairing tool is pretty straight-forward. Example session: $ ./ltunify list /dev/hidraw0: Permission denied Logitech Unifying Receiver device is not accessible. Try running this program as root or enable read/write permissions for /dev/hidraw0 $ sudo chgrp $USER /dev/hidraw0 && sudo chmod g+rw /dev/hidraw0 $ ./ltunify list Devices count: 1 Connected devices: idx=1 Mouse M525 $ ./ltunify info 1 Device index 1 Mouse Name: M525 Wireless Product ID: 4013 Serial number: DAFA335E Device was unavailable, version information not available. $ ./ltunify unpair 1 Device 0x01 Mouse successfully unpaired $ ./ltunify list Devices count: 0 Connected devices: $ ./ltunify pair Please turn your wireless device off and on to start pairing. Found new device, id=0x01 Mouse $ ./ltunify list Devices count: 1 Connected devices: idx=1 Mouse M525 TODO - organize code in multiple files - simplify code - HID++ 2.0 debugging (transparent if possible) ~ Peter Wu ltunify-0.3/hidpp20.c000066400000000000000000000141561367150467500145050ustar00rootroot00000000000000#include #include #include #include #include #define SHORT_MESSAGE 0x10 #define LONG_MESSAGE 0x11 struct hidpp2_message { u8 report_id; u8 device_index; u8 feature_index; #define HIDPP_SET_FUNC(msg, func) ((msg)->func_swId |= ((func) << 4)) u8 func_swId; u8 params[16]; // 3 or 16 params } __attribute__((__packed__)); struct feature { uint16_t featureId; #define FEAT_TYPE_MASK 0xe0 #define FEAT_TYPE_OBSOLETE 0x80 #define FEAT_TYPE_SWHIDDEN 0x40 #define FEAT_TYPE_RSVD_INTERNAL 0x20 u8 featureType; }; #define SOFTWARE_ID 0x04 /* getRandom() -- guaranteed to be random. */ #define FEATURE_INDEX_IROOT 0x00 #define FID_IFEATURESET 0x0001 static const char * get_feature_name(uint16_t featureId) { /* With '?' prefix are taken from SetPointP/KEMUI.xml */ switch (featureId) { case 0x0000: return "Root"; case 0x0001: return "FeatureSet"; case 0x0002: return "FeatureInfo"; case 0x0003: return "DeviceFwVersion"; case 0x0005: return "DeviceName"; case 0x0006: return "DeviceGroups"; case 0x00C0: return "Dfucontrol"; /* Firmware Update */ case 0x1000: return "BatteryStatus"; case 0x1900: return "?SoundNotif"; /* Sound Notification */ case 0x1920: return "?AudioControls"; /* Audio Controls */ case 0x1940: return "?VOIP"; /* Internet Telephony */ case 0x1960: return "?VideoCalling"; case 0x1980: return "?Backlighting"; case 0x1981: return "Backlight"; case 0x1B00: return "ReprogControls"; case 0x1B01: return "ReprogControlsV2"; case 0x1B03: return "ReprogControlsV3"; case 0x1D4B: return "WirelessDeviceStatus"; /* Wireless Update */ case 0x2000: return "?MouseFeature"; /* Pointing Device Feature */ case 0x2001: return "LeftRightSwap"; case 0x2100: return "VerticalScrolling"; case 0x2120: return "HiResScrolling"; case 0x2200: return "MousePointer"; case 0x2510: return "?ProfileMgmt"; /* Profile Management */ case 0x4000: return "?KeyboardFeature"; case 0x40A0: return "FnInversion"; case 0x40A2: return "NewFnInversion"; case 0x4100: return "Encryption"; case 0x4301: return "SolarDashboard"; case 0x4400: return "?DisplayFeature"; case 0x4520: return "KeyboardLayout"; /* Inactive key */ case 0x5500: return "?SliderControls"; case 0x6000: return "?TouchpadFeature"; case 0x6010: return "TouchpadFwItems"; /* Basic Touchpad settings */ case 0x6011: return "TouchpadSwItems"; /* Enhanced Touchpad settings */ case 0x6012: return "TouchpadWin8FwItems"; case 0x6020: return "?TouchpadTapSelect"; /* Tap to select */ case 0x6030: return "?DisablePointerAccel"; /* Disable pointer acceleration */ case 0x6100: return "TouchpadRawXy"; case 0x6110: return "TouchmouseRawPoints"; case 0x6120: return "Touchmouse6120"; case 0x6300: return "?HandwritingRecog"; /* Handwriting recognition" */ default: return "unknown"; } } /** * Initialize common values of a HID++ 2.0 message: report_id, device_index and * software ID. Remaining fields: feature_index, func, params. */ #define INIT_HIDPP_SHORT(msg, dev_index) \ memset((msg), 0, sizeof *(msg)); \ (msg)->report_id = SHORT_MESSAGE; \ (msg)->device_index = dev_index; \ (msg)->func_swId = SOFTWARE_ID static bool do_hidpp2_request(int fd, struct hidpp2_message *msg) { struct hidpp_message *hid10_message = (struct hidpp_message *) msg; if (!do_write(fd, hid10_message)) { return false; } // use do_read_skippy in case there are interleaving notifications // sub id is feature index in HID++ 2.0 if (!do_read_skippy(fd, hid10_message, 0x11, hid10_message->sub_id)) { puts("WTF"); return false; } return true; } // Returns feature index for featureId static u8 get_feature(int fd, u8 device_index, uint16_t featureId) { struct hidpp2_message msg; INIT_HIDPP_SHORT(&msg, device_index); msg.feature_index = FEATURE_INDEX_IROOT; HIDPP_SET_FUNC(&msg, 0); // GetFeature(featureId) msg.params[0] = featureId >> 8; msg.params[1] = featureId & 0xFF; if (!do_hidpp2_request(fd, &msg)) { return 0; } return msg.params[0]; } // Returns number of features or 0 on error. static u8 get_feature_count(int fd, u8 device_index, u8 ifeatIndex) { struct hidpp2_message msg; INIT_HIDPP_SHORT(&msg, device_index); // XXX: is this variable? Can't it be hard-coded to 0x01? msg.feature_index = ifeatIndex; HIDPP_SET_FUNC(&msg, 0); // GetCount() if (!do_hidpp2_request(fd, &msg)) { fprintf(stderr, "Failed to request features count\n"); return 0; } return msg.params[0]; } // Get featureId and type for a given featureIndex. static bool get_featureId(int fd, u8 device_index, u8 ifeatIndex, u8 featureIndex, struct feature *feat) { struct hidpp2_message msg; INIT_HIDPP_SHORT(&msg, device_index); // XXX: is this variable? Can't it be hard-coded to 0x01? msg.feature_index = ifeatIndex; HIDPP_SET_FUNC(&msg, 1); // GetFeatureId(featureIndex) msg.params[0] = featureIndex; if (!do_hidpp2_request(fd, &msg)) { return false; } feat->featureId = (msg.params[0] << 8) | msg.params[1]; feat->featureType = msg.params[2]; return true; } void hidpp20_print_features(int fd, u8 device_index) { u8 i, count, ifeatIndex; ifeatIndex = get_feature(fd, device_index, FID_IFEATURESET); if (!ifeatIndex) { fprintf(stderr, "Failed to get feature information\n"); return; } count = get_feature_count(fd, device_index, ifeatIndex); printf("Total number of HID++ 2.0 features: %i\n", count); for (i = 0; i <= count; i++) { struct feature feat; if (get_featureId(fd, device_index, ifeatIndex, i, &feat)) { printf(" %2i: [%04X] %c%c%c %s\n", i, feat.featureId, feat.featureType & FEAT_TYPE_OBSOLETE ? 'O' : ' ', feat.featureType & FEAT_TYPE_SWHIDDEN ? 'H' : ' ', feat.featureType & FEAT_TYPE_RSVD_INTERNAL ? 'I' : ' ', get_feature_name(feat.featureId)); if (feat.featureType & ~FEAT_TYPE_MASK) { printf("Warning: unrecognized feature flags: %#04x\n", feat.featureType & ~FEAT_TYPE_MASK); } } else { fprintf(stderr, "Failed to get feature, is device connected?\n"); } } puts("(O = obsolete feature; H = SW hidden feature;\n" " I = reserved for internal use)"); } ltunify-0.3/hidraw.c000066400000000000000000000175701367150467500145200ustar00rootroot00000000000000/* * Displays a more human-readable interpretation of the USB data payload * for Logitech Unifying Receiver. * * Example usage: read-dev-usbmon /dev/usbmon0 | hidraw * * Copyright (C) 2013 Peter Wu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include typedef unsigned char u8; #define SHORT_MSG 0x10 #define SHORT_MSG_LEN 7 #define DJ_SHORT 0x20 #define DJ_SHORT_LEN 15 #define DJ_LONG 0x21 #define DJ_LONG_LEN 32 #define LONG_MSG 0x11 #define LONG_MSG_LEN 20 struct payload_short { u8 address; u8 value[3]; }; struct payload_long { u8 address; u8 str[16]; }; struct report { u8 report_id; u8 device_index; u8 sub_id; union { struct payload_long l; struct payload_short s; }; } __attribute__((__packed__)); /* types for HID++ report IDs 0x10 and 0x11 */ static const char * report_types[0x100] = { [0x00] = "_HIDPP20", // fake type // 0x00 - 0x3F HID reports [0x01] = "KEYBOARD", [0x02] = "MOUSE", [0x03] = "CONSUMER_CONTROL", [0x04] = "SYSTEM_CONTROL", [0x08] = "MEDIA_CENTER", [0x0E] = "LEDS", // 0x40 - 0x7F enumerator notifications [0x40] = "NOTIF_DEVICE_UNPAIRED", [0x41] = "NOTIF_DEVICE_PAIRED", [0x42] = "NOTIF_CONNECTION_STATUS", [0x4A] = "NOTIF_RECV_LOCK_CHANGED", [0x4B] = "?NOTIF_PAIR_ACCEPTED", [0x7F] = "NOTIF_ERROR", // 0x80 - 0xFF enumerator commands; Register Access [0x80] = "SET_REG", // was CMD_SWITCH [0x81] = "GET_REG", // was CMD_GET_PAIRED_DEVICES [0x82] = "SET_LONG_REG", [0x83] = "GET_LONG_REG", [0x8F] = "_ERROR_MSG", [0xFF] = "_HIDPP20_ERROR_MSG", }; /* types for DJ report IDS 0x20 and 0x21 */ static const char *dj_report_types[0x100] = { /* 0x00 - 0x3F: RF reports */ [0x01] = "KEYBOARD", [0x02] = "MOUSE", [0x03] = "CONSUMER_CONTROL", [0x04] = "SYSTEM_CONTROL", [0x08] = "MEDIA_CENTER", [0x0E] = "KEYBOARD_LEDS", /* 0x40 - 0x7F: DJ notifications */ [0x40] = "NOTIF_DEVICE_UNPAIRED", [0x41] = "NOTIF_DEVICE_PAIRED", [0x42] = "NOTIF_CONNECTION_STATUS", [0x7F] = "NOTIF_ERROR", /* 0x80 - 0xFF: DJ commands */ [0x80] = "CMD_SWITCH_N_KEEPALIVE", [0x81] = "CMD_GET_PAIRED_DEVICES" }; static const char * error_messages[0x100] = { // error messages for type=8F (ERROR_MSG) [0x00] = "SUCCESS", [0x01] = "INVALID_SUBID", [0x02] = "INVALID_ADDRESS", [0x03] = "INVALID_VALUE", [0x04] = "CONNECT_FAIL", [0x05] = "TOO_MANY_DEVICES", [0x06] = "ALREADY_EXISTS", [0x07] = "BUSY", [0x08] = "UNKNOWN_DEVICE", [0x09] = "RESOURCE_ERROR", [0x0A] = "REQUEST_UNAVAILABLE", [0x0B] = "INVALID_PARAM_VALUE", [0x0C] = "WRONG_PIN_CODE", }; // I don't know the upper bound, perhaps 0x10 is enough static const char * error_messages_hidpp20[0x100] = { [0x00] = "NoError", [0x01] = "Unknown", [0x02] = "InvalidArgument", [0x03] = "OutOfRange", [0x04] = "HWError", [0x05] = "Logitech internal", [0x06] = "INVALID_FEATURE_INDEX", [0x07] = "INVALID_FUNCTION_ID", [0x08] = "Busy", [0x09] = "Unsupported", }; // everything with a '?' is guessed static const char * registers[0x100] = { [0x00] = "ENABLED_NOTIFS", [0x01] = "KBD_HAND_DETECT?", [0x02] = "CONNECTION_STATE", [0x07] = "BATTERY?", [0x09] = "FN_KEY_SWAP?", [0x17] = "ILLUMINATION_INFO?", [0xb2] = "DEVICE_PAIRING", [0xb3] = "DEVICE_ACTIVITY", [0xb5] = "PAIRING_INFO", [0xf1] = "VERSION_INFO?", }; bool report_type_is_hidpp(u8 report_id) { return report_id == SHORT_MSG || report_id == LONG_MSG; } bool report_type_is_dj(u8 report_id) { return report_id == DJ_SHORT || report_id == DJ_LONG; } const char * report_type_str(u8 report_id, u8 type) { const char *str = NULL; if (report_type_is_hidpp(report_id)) str = report_types[type]; else if (report_type_is_dj(report_id)) str = dj_report_types[type]; return str ? str : ""; } const char * device_type_str(u8 type) { switch (type) { case 0x01: return "DEV1"; case 0x02: return "DEV2"; case 0x03: return "DEV3"; case 0x04: return "DEV4"; case 0x05: return "DEV5"; case 0x06: return "DEV6"; case 0xFF: return "RECV"; default: return ""; } } const char *error_str(u8 er) { const char * str = error_messages[er]; return str ? str : ""; } const char *error_str_hidpp20(u8 er) { const char * str = error_messages_hidpp20[er]; return str ? str : ""; } const char *register_str(u8 reg) { const char * str = registers[reg]; return str ? str : ""; } void process_msg_payload(struct report *r, u8 data_len) { u8 pos, i; u8 * bytes = (u8 *) &r->s; pos = 0; // nothing has been processed if (report_type_is_hidpp(r->report_id)) switch (r->sub_id) { case 0x00: // assume HID++ 2.0 request/response for feature IRoot if (data_len == 4 || data_len == 17) { printf("func=%X ", bytes[0] >> 4); printf("swId=%X ", bytes[0] & 0xF); pos = 1; } break; case 0xFF: // assume HID++ 2.0 error if (data_len == 17) { printf("feat=%X ", bytes[0]); printf("func=%X ", bytes[1] >> 4); printf("swId=%X ", bytes[1] & 0xF); printf("err=%02X %s ", bytes[2], error_str_hidpp20(bytes[2])); pos = 3; } break; case 0x8F: // error // TODO: length check printf("SubID=%02X %s ", bytes[0], report_type_str(r->report_id, bytes[0])); printf("reg=%02X %s ", bytes[1], register_str(bytes[1])); printf("err=%02X %s ", bytes[2], error_str(bytes[2])); pos = 4; // everything is processed break; case 0x80: case 0x81: case 0x82: /* long */ case 0x83: /* long */ printf("reg=%02X %s ", bytes[0], register_str(bytes[0])); pos = 1; break; } if (pos < data_len) { printf("params="); //printf("params(len=%02X)=", data_len); } for (i = 0; pos < data_len; pos++, i++) { printf("%02X ", bytes[pos]); if (i % 4 == 3 && pos + 1 < data_len) { putchar(' '); } } } void process_msg(struct report *report, ssize_t size) { const char * report_type; switch (report->report_id) { case SHORT_MSG: report_type = "short"; if (size != SHORT_MSG_LEN) { fprintf(stderr, "Invalid short msg len %zi\n", size); return; } break; case LONG_MSG: report_type = "long"; if (size != LONG_MSG_LEN) { fprintf(stderr, "Invalid long msg len %zi\n", size); return; } break; case DJ_SHORT: report_type = "dj_s"; if (size != DJ_SHORT_LEN) { fprintf(stderr, "Invalid DJ short msg len %zi\n", size); return; } break; case DJ_LONG: report_type = "dj_l"; if (size != DJ_LONG_LEN) { fprintf(stderr, "Invalid DJ long msg len %zi\n", size); return; } break; default: report_type = "unkn"; //fprintf(stderr, "Unknown report ID %02x, len=%zi\n", report->report_id, size); if (size < 3) { return; } break; } printf("report_id=%02X %-5s ", report->report_id, report_type); printf("device=%02X %-4s ", report->device_index, device_type_str(report->device_index)); printf("type=%02X %-23s ", report->sub_id, report_type_str(report->report_id, report->sub_id)); if (size > 3) { process_msg_payload(report, size - 3); } putchar('\n'); } #ifndef NO_MAIN int main(int argc, char ** argv) { int fd = STDIN_FILENO; ssize_t r; struct report report; if (argc >= 2 && (fd = open(argv[1], O_RDONLY)) < 0) { perror(argv[1]); return 1; } do { memset(&report, 0xCC, sizeof report); // for debugging purposes r = read(fd, &report, sizeof report); if (r > 0) { process_msg(&report, r); } } while (r >= 0); if (r < 0) { perror("read"); } close(fd); return 0; } #endif /* ! NO_MAIN */ ltunify-0.3/keyboard.txt000066400000000000000000000073251367150467500154340ustar00rootroot00000000000000Keyboard - Fn keys customization ================================ This document describes observations with the K800 keyboard which features customizable Fn keys and a HID++ 1.0 protocol. Its observations may apply to other HID++ 1.0 keyboards too. See registers.txt for HID++ details. Important knowledge from that file: - Setting Notifications flags makes the keyboard emit different HID reports that can be captured by the software to generate custom events. - The functionality Fx and Fn + Fx can be swapped (e.g. pressing F1 generates a "Web" event, Fn + F1 generates the regular "F1" event). The Consumer Control (3) and System Control (4) descriptors have the following descriptors: INPUT(3)[INPUT] Field(0) Application(Consumer.0001) Usage(652) Consumer.0001 Consumer.0002 ... (a lot Consumer.xxxx omitted) ... Consumer.028b Consumer.028c Logical Minimum(1) Logical Maximum(652) Report Size(16) Report Count(2) Report Offset(0) Flags( Array Absolute ) INPUT(4)[INPUT] Field(0) Application(GenericDesktop.SystemControl) Usage(3) GenericDesktop.SystemSleep GenericDesktop.SystemPowerDown GenericDesktop.SystemWakeUp Logical Minimum(1) Logical Maximum(3) Report Size(2) Report Count(1) Report Offset(0) Flags( Array Absolute NoPreferredState NullState ) /* At most two simultaneous key presses can be registered. If a button is not * pressed, the value for that "button" is 00 00. The HID layer keeps a state of * which keys are pressed, reports simply change that state. * * 92 01 00 00 - Pressed "Calculator" (0192) * 92 01 b5 00 - Pressed "Forward" (00b5) * b5 00 00 00 - Released "Calculator" (0192 has gone) * 00 00 00 00 - Released "Forward" (00b5 has gone) */ struct consumer_control_data { uint16_t button1; uint16_t button2; } /* The button is a number in the range 1 - 3 (0 means released) */ struct system_control_data { char button : 2; /* two right-most bits, Big Endian */ }; The 20 ix yy dd.. ... messages below are described as follows: - 20: Report ID for Logitech Vendor DJ collection (messages are not processed by the hid-logitech-dj driver but are passed through to the HID layer.) - yy: descriptor type (system control, consumer control, keyboard, etc.) - dd..: data, length is dependent on descriptor type. - Remaining bytes is garbage/padding and can be ignored (confirmed by Nestor from Logitech). The following describes what events are generated when a certain flag is toggled in the notification register 00. flag 1, bit 1 - controls "System Control" events? Format: (disabled bit) 20 ix 04 XX ... (other 11 bytes is padding) (enabled bit) 10 ix 04 XX 00 00 00 Values for XX: - 01: Sleep Button (Fn + F8) flag 1, bit 0 - controls "Consumer Control" events? Format: (disabled bit) 20 ix 03 XX xx YY yy ... (other 8 bytes is padding) (enabled bit) 10 ix 03 XX xx YY yy XX xx and YY yy are two buttons that are pressed according to the keyboard (see also struct consumer_control_data above). Known keys (XX xx are shown as xxXX): - 0223: Web (Fn + F1) - 018a: Email (Fn + F2) - 0221: Search (Fn + F3) - 0183: Music (Fn + F9) - 00b6: Previous (Fn + F10) - 00cd: Play/Pause (Fn + F10) - 00b5: Next (Fn + F10) - 00e2: Mute - 00ea: Volume down - 00e9: Volume up - 0192: Calculator - 102c: "Fn" button Note: when Fn keys are not swapped, pressing Fn + F1 will still generate 102c: vv vv-------- Fn (102c) 10 ix 03 2c 10 23 02 ^^ ^^-- Web (0223) Remaining unmappable keys: - Application Switcher (Fn + F4) - Illumination brightnesss down (Fn + F5) - Illumination brightnesss up (Fn + F6) - Battery status (Fn + F7) ltunify-0.3/ltunify.c000066400000000000000000001172751367150467500147370ustar00rootroot00000000000000/* * Pair, unpair or list information about wireless devices like keyboards and * mice that use the Logitech® Unifying receiver. * * Copyright (C) 2013 Peter Wu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include /* strtoul */ #include /* uint16_t */ #include /* ntohs, ntohl */ #include /* for /dev/hidrawX discovery */ #include /* for getopt_long */ #include #include /* for basename, used during discovery */ #include /* needs -lrt, for clock_gettime as timeout helper */ #include #ifndef PACKAGE_VERSION # define PACKAGE_VERSION "0.3" #endif #define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a)) // pass -D option to print very verbose details like protocol communication static bool debug_enabled; #define DPRINTF(...) if (debug_enabled) { fprintf(stderr, __VA_ARGS__); } typedef unsigned char u8; #define VID_LOGITECH 0x046d #define PID_NANO_RECEIVER 0xc52f #define PID_NANO_RECEIVER_2 0xc534 #define HEADER_SIZE 3 #define SHORT_MESSAGE 0x10 #define SHORT_MESSAGE_LEN 7 #define LONG_MESSAGE 0x11 #define LONG_MESSAGE_LEN 20 #define DEVICE_RECEIVER 0xFF #define SUB_SET_REGISTER 0x80 #define SUB_GET_REGISTER 0x81 #define SUB_SET_LONG_REGISTER 0x82 #define SUB_GET_LONG_REGISTER 0x83 #define SUB_ERROR_MSG 0x8F #define NOTIF_DEV_DISCONNECT 0x40 /* Device Disconnection */ #define NOTIF_DEV_CONNECT 0x41 /* Device Connection */ #define NOTIF_RECV_LOCK_CHANGE 0x4A /* Unifying Receiver Locking Change information */ #define REG_ENABLED_NOTIFS 0x00 #define REG_CONNECTION_STATE 0x02 /* Device Connection and Disconnection (Pairing) */ #define REG_DEVICE_PAIRING 0xB2 #define REG_DEVICE_ACTIVITY 0xB3 #define REG_PAIRING_INFO 0xB5 #define REG_VERSION_INFO 0xF1 /* undocumented */ // Used for: {GET,SET}_REGISTER_{REQ,RSP}, SET_LONG_REGISTER_RSP, GET_LONG_REGISTER_REQ struct msg_short { u8 address; u8 value[3]; }; // Used for: SET_LONG_REGISTER_REQ, GET_LONG_REGISTER_RSP struct msg_long { u8 address; u8 str[16]; }; // Used for: ERROR_MSG struct msg_error { u8 sub_id; u8 address; u8 error_code; u8 padding; /* set to 0 */ }; // 0x00 Enable HID++ Notifications struct msg_enable_notifs { u8 reporting_flags_devices; // bit 4 Battery status u8 reporting_flags_receiver; // bit 0 Wireless notifications, 3 Software Present u8 reporting_flags_receiver2; // (reserved) }; // long receiver resp - 0xB5 Pairing information, 0x03 - "Receiver information"? (undocumented) struct msg_receiver_info { u8 _dunno1; // always 0x03 for receiver? u8 serial_number[4]; u8 _dunno2; // 06 - Max Device Capability? (not sure, but it is six) u8 _dunno3; u8 padding[8]; // 00 00 00 00 00 00 00 00 - ?? }; // 0xB5 Pairing information, 0x20..0x2F - Unifying Device pairing information struct msg_dev_pair_info { u8 requested_field; // 0x20..0x25 u8 dest_id; u8 report_interval; // ms u8 pid_msb; u8 pid_lsb; u8 _reserved1[2]; u8 device_type; u8 _reserved2[6]; }; // 0xB5 Pairing information, 0x30..0x3F - Unifying Device extended pairing info struct msg_dev_ext_pair_info { u8 requested_field; // 0x30..0x35 u8 serial_number[4]; // index 0 is MSB u8 report_types[4]; // index 0 is MSB u8 usability_info; // bits 0..3 is location of power switch }; // 0xB5 Pairing information, 0x40..0x4F - Unifying Device name #define DEVICE_NAME_MAXLEN 14 struct msg_dev_name { u8 requested_field; // 0x40..0x45 u8 length; char str[DEVICE_NAME_MAXLEN]; // UTF-8 encoding }; struct notif_devcon { #define DEVCON_PROT_UNIFYING 0x04 #define DEVCON_PROT_NANO_LITE 0x0a u8 prot_type; // bits 0..2 is protocol type (4 for unifying), 3..7 is reserved #define DEVCON_DEV_TYPE_MASK 0x0f // Link status: 0 is established (in range), 1 is not established (out of range) #define DEVCON_LINK_STATUS_FLAG 0x40 u8 device_info; // wireless product id: u8 pid_lsb; u8 pid_msb; }; // Register 0x02 Connection State struct val_reg_connection_state { // 0x02 triggers a 0x41 notification for all known devices #define CONSTATE_ACTION_LIST_DEVICES 0x02 u8 action; // always 0 for read u8 connected_devices_count; u8 _undocumented2; }; // Register 0xB2 Device Connection and Disconnection (Pairing) struct val_reg_devpair { #define DEVPAIR_KEEP_LOCK 0 #define DEVPAIR_OPEN_LOCK 1 #define DEVPAIR_CLOSE_LOCK 2 #define DEVPAIR_DISCONNECT 3 u8 action; u8 device_number; // same as device index from 0x41 notif u8 open_lock_timeout; // timeout in seconds, 0 = default (30s) }; // Register 0xF1 Version Info (undocumented) struct val_reg_version { // v1.v2.xxx #define VERSION_FIRMWARE 1 // x.x.v1v2 #define VERSION_FW_BUILD 2 // value 3 is invalid for receiver, but returns 00 07 for keyboard // BL.v1.v2 #define VERSION_BOOTLOADER 4 u8 select_field; u8 v1; u8 v2; }; struct hidpp_message { u8 report_id; u8 device_index; u8 sub_id; union { struct msg_short msg_short; struct msg_long msg_long; struct msg_error msg_error; }; }; struct hidpp_version { u8 major; u8 minor; }; struct version { u8 fw_major; u8 fw_minor; uint16_t fw_build; u8 bl_major; u8 bl_minor; }; #define DEVICES_MAX 6u struct device { bool device_present; // whether the device is paired bool device_available; // whether the device is connected u8 device_type; uint16_t wireless_pid; char name[DEVICE_NAME_MAXLEN + 1]; // include NUL byte uint32_t serial_number; u8 power_switch_location; struct hidpp_version hidpp_version; struct version version; }; struct device devices[DEVICES_MAX]; struct receiver_info { uint32_t serial_number; struct version version; }; struct receiver_info receiver; // error messages for type=8F (ERROR_MSG) static const char * error_messages[0x100] = { [0x00] = "SUCCESS", [0x01] = "INVALID_SUBID", [0x02] = "INVALID_ADDRESS", [0x03] = "INVALID_VALUE", [0x04] = "CONNECT_FAIL", [0x05] = "TOO_MANY_DEVICES", [0x06] = "ALREADY_EXISTS", [0x07] = "BUSY", [0x08] = "UNKNOWN_DEVICE", [0x09] = "RESOURCE_ERROR", [0x0A] = "REQUEST_UNAVAILABLE", [0x0B] = "INVALID_PARAM_VALUE", [0x0C] = "WRONG_PIN_CODE", }; static const char * device_type[0x10] = { [0x00] = "Unknown", [0x01] = "Keyboard", [0x02] = "Mouse", [0x03] = "Numpad", [0x04] = "Presenter", // 0x05..0x07 Reserved for future [0x08] = "Trackball", [0x09] = "Touchpad", // 0x0A..0x0F Reserved }; const char *device_type_str(u8 type) { if (type > 0x0F) { return "(invalid)"; } if (device_type[type]) { return device_type[type]; } return "(reserved)"; } // returns device type index or -1 if the string is invalid int device_type_from_str(const char *str) { unsigned i; // skip "Unknown" type for (i = 1; i < ARRAY_SIZE(device_type); i++) { if (device_type[i] && !strcasecmp(device_type[i], str)) { return i; } } return -1; } static void print_device_types(void) { unsigned i; // skip "Unknown" type for (i = 1; i < ARRAY_SIZE(device_type); i++) { if (device_type[i]) { fprintf(stderr, " %s", device_type[i]); } } putchar('\n'); } static void dump_msg(struct hidpp_message *msg, size_t payload_size, const char *tag) { size_t i; if (!debug_enabled) { return; } // HACK: do not mess with stderr colors fflush(NULL); printf("\033[34m"); printf("%s: ", tag); for (i=0; i static ssize_t do_read(int fd, struct hidpp_message *msg, u8 expected_report_id, int timeout) { ssize_t r; size_t payload_size = LONG_MESSAGE_LEN; long long unsigned begin_ms, end_ms; if (expected_report_id == SHORT_MESSAGE) { payload_size = SHORT_MESSAGE_LEN; } begin_ms = get_timestamp_ms(); while (timeout > 0) { struct pollfd pollfd; pollfd.fd = fd; pollfd.events = POLLIN; r = poll(&pollfd, 1, timeout); if (r < 0) { perror("poll"); return 0; } else if (r == 0) { DPRINTF("poll timeout reached!\n"); // timeout return 0; } memset(msg, 0, payload_size); r = read(fd, msg, payload_size); if (r < 0) { perror("read"); return 0; } else if (r > 0) { dump_msg(msg, r, "rd"); if (msg->report_id == expected_report_id) { return r; } else if (expected_report_id == 0 && (msg->report_id == SHORT_MESSAGE || msg->report_id == LONG_MESSAGE)) { /* HACK: ping response for HID++ 2.0 is a LONG * message, but for HID++ 1.0 it is a SHORT one. */ return r; } else { DPRINTF("Skipping unexpected report ID %#x (want %#x)\n", msg->report_id, expected_report_id); } } /* unexpected message, try again with updated timeout */ end_ms = get_timestamp_ms(); timeout -= end_ms - begin_ms; begin_ms = end_ms; } /* timeout expired, no report found unfortunately */ return 0; } static ssize_t do_write(int fd, struct hidpp_message *msg) { ssize_t r, payload_size = SHORT_MESSAGE_LEN; if (msg->report_id == LONG_MESSAGE) { payload_size = LONG_MESSAGE_LEN; } dump_msg(msg, payload_size, "wr"); r = write(fd, msg, payload_size); if (r < 0) { perror("write"); } return payload_size == r ? payload_size : 0; } const char *get_report_id_str(u8 report_type) { switch (report_type) { case SHORT_MESSAGE: return "short"; case LONG_MESSAGE: return "long"; default: return "unkn"; } } bool process_notif_dev_connect(struct hidpp_message *msg, u8 *device_index, bool *is_new_device) { u8 dev_idx = msg->device_index; struct notif_devcon *dcon = (struct notif_devcon *) &msg->msg_short; struct device *dev; if (msg->sub_id != NOTIF_DEV_CONNECT) { fprintf(stderr, "Invalid msg type %#0x, expected dev conn notif\n", msg->sub_id); return false; } if (msg->report_id != SHORT_MESSAGE) { fprintf(stderr, "Dev conn notif is expected to be short, got " "%#04x instead\n", msg->report_id); return false; } if (dcon->prot_type != DEVCON_PROT_UNIFYING && dcon->prot_type != DEVCON_PROT_NANO_LITE) { fprintf(stderr, "Unknown protocol %#04x in devcon notif\n", dcon->prot_type); return false; } if (dev_idx < 1 || dev_idx > DEVICES_MAX) { fprintf(stderr, "Disallowed device index %#04x\n", dev_idx); return false; } dev = &devices[dev_idx - 1]; if (device_index) *device_index = dev_idx; if (is_new_device) *is_new_device = !dev->device_present; memset(dev, 0, sizeof *dev); dev->device_type = dcon->device_info & DEVCON_DEV_TYPE_MASK; dev->wireless_pid = (dcon->pid_msb << 8) | dcon->pid_lsb; dev->device_present = true; dev->device_available = !(dcon->device_info & DEVCON_LINK_STATUS_FLAG); return true; } // use for reading registers and skipping notifications from return static bool do_read_skippy(int fd, struct hidpp_message *msg, u8 exp_report_id, u8 exp_sub_id) { for (;;) { if (!do_read(fd, msg, exp_report_id, 2000)) { return false; } if (msg->report_id == exp_report_id && msg->sub_id == exp_sub_id) { return true; } /* ignore non-HID++ reports (e.g. DJ reports) */ if (msg->report_id != SHORT_MESSAGE && msg->report_id != LONG_MESSAGE) { continue; } // guess: 0xFF is error message in HID++ 2.0? if (msg->report_id == LONG_MESSAGE && msg->sub_id == 0xFF) { if (debug_enabled) { fprintf(stderr, "HID++ 2.0 error %#04x\n", msg->msg_long.str[2]); } return false; } if (msg->report_id == SHORT_MESSAGE && msg->sub_id == SUB_ERROR_MSG && msg->msg_error.sub_id == exp_sub_id) { struct msg_error *error = &msg->msg_error; // TODO: consider address (register)? u8 error_code = error->error_code; const char *err_str = error_messages[error_code]; if (debug_enabled) { fprintf(stderr, "Received error for subid=%#04x," " reg=%#04x: %#04x (%s)\n", error->sub_id, error->address, error_code, err_str); } return false; } if (msg->sub_id == NOTIF_DEV_CONNECT) { process_notif_dev_connect(msg, NULL, NULL); continue; } else if (msg->sub_id == NOTIF_DEV_DISCONNECT) { u8 device_index = msg->device_index; u8 disconnect_type = *(u8 *) &msg->msg_short; if (device_index < 1 || device_index > DEVICES_MAX) { fprintf(stderr, "Invalid device index %#04x\n", device_index); } else if (disconnect_type & 0x02) { memset(&devices[device_index - 1], 0, sizeof *devices); } else { fprintf(stderr, "Unexpected disconnection type %#04x\n", disconnect_type); } } else if (msg->sub_id == NOTIF_RECV_LOCK_CHANGE) { if (msg->report_id != SHORT_MESSAGE || msg->device_index != DEVICE_RECEIVER) { fprintf(stderr, "Received invalid Unifying Receiver Locking Change notification (0x4A)\n"); continue; } // TODO: this can be used to make an application aware // that pairing is (not) possible (anymore) if (debug_enabled) { fprintf(stderr, "Receiver lock state is now %s\n", (*(u8 *)&msg->msg_short) & 1 ? "open" : "closed"); } } if (debug_enabled && (msg->report_id != exp_report_id || msg->sub_id != exp_sub_id)) { fprintf(stderr, "Expected %s msg %#02x, got %s msg %#02x\n", get_report_id_str(exp_report_id), exp_sub_id, get_report_id_str(msg->report_id), msg->sub_id); } // let's try reading another message... } return true; } // TODO: separate files #include "hidpp20.c" static bool set_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *res, bool is_long_req) { u8 exp_sub_id; struct hidpp_message msg; msg.device_index = device_index; if (is_long_req) { msg.report_id = LONG_MESSAGE; exp_sub_id = SUB_SET_LONG_REGISTER; msg.msg_long.address = address; memcpy(&msg.msg_long.str, params, sizeof msg.msg_long.str); } else { msg.report_id = SHORT_MESSAGE; exp_sub_id = SUB_SET_REGISTER; msg.msg_short.address = address; memcpy(&msg.msg_short.value, params, sizeof msg.msg_short.value); } msg.sub_id = exp_sub_id; if (!do_write(fd, &msg)) { return false; } msg.report_id = SHORT_MESSAGE; if (!do_read_skippy(fd, &msg, SHORT_MESSAGE, exp_sub_id)) { return false; } memcpy(res, &msg, sizeof msg); return true; } bool set_short_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *res) { return set_register(fd, device_index, address, params, res, false); } bool set_long_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *res) { return set_register(fd, device_index, address, params, res, true); } static bool get_register(int fd, u8 device_index, u8 address, struct hidpp_message *out, u8 *params, bool is_long_resp) { u8 exp_report_id, exp_sub_id; struct hidpp_message msg; exp_report_id = is_long_resp ? LONG_MESSAGE : SHORT_MESSAGE; exp_sub_id = is_long_resp ? SUB_GET_LONG_REGISTER : SUB_GET_REGISTER; msg.report_id = SHORT_MESSAGE; msg.device_index = device_index; msg.sub_id = exp_sub_id; memset(msg.msg_short.value, 0, sizeof msg.msg_short.value); msg.msg_short.address = address; if (params) { memcpy(&msg.msg_short.value, params, sizeof msg.msg_short.value); } if (!do_write(fd, &msg)) { return false; } if (!do_read_skippy(fd, &msg, exp_report_id, exp_sub_id)) { return false; } memcpy(out, &msg, sizeof msg); return true; } bool get_short_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *out) { return get_register(fd, device_index, address, out, params, false); } bool get_long_register(int fd, u8 device_index, u8 address, u8 *params, struct hidpp_message *out) { return get_register(fd, device_index, address, out, params, true); } bool get_info(int fd, struct hidpp_message *msg) { if (!get_long_register(fd, DEVICE_RECEIVER, REG_DEVICE_ACTIVITY, NULL, msg)) { return false; } return true; } // begin directly-usable functions bool get_notifications(int fd, u8 device_index, struct msg_enable_notifs *params) { struct hidpp_message msg; if (!get_short_register(fd, device_index, REG_ENABLED_NOTIFS, NULL, &msg)) { return false; } memcpy((u8 *) params, &msg.msg_short.value, sizeof *params); return true; } bool set_notifications(int fd, u8 device_index, struct msg_enable_notifs *params) { struct hidpp_message msg; if (!set_short_register(fd, device_index, REG_ENABLED_NOTIFS, (u8 *) params, &msg)) { return false; } return true; } bool get_and_print_notifications(int fd, u8 device_index, struct msg_enable_notifs *notifsp) { putchar('\n'); if (get_notifications(fd, device_index, notifsp)) { u8 flags = notifsp->reporting_flags_receiver; printf("Reporting Flags (Receiver) = %02x\n", flags & 0xFF); printf("Wireless notifications = %s\n", flags & 1 ? "yes" : "no"); printf("Software Present = %s\n", flags & 4 ? "yes" : "no"); return true; } else { fprintf(stderr, "Failed to get HID++ Notification status\n"); return false; } } bool get_connected_devices(int fd, u8 *devices_count) { struct hidpp_message msg; struct val_reg_connection_state *cval; if (!get_short_register(fd, DEVICE_RECEIVER, REG_CONNECTION_STATE, NULL, &msg)) { return false; } cval = (struct val_reg_connection_state *) msg.msg_short.value; *devices_count = cval->connected_devices_count; return true; } bool pair_start(int fd, u8 timeout) { struct hidpp_message msg; struct val_reg_devpair cmd; cmd.action = DEVPAIR_OPEN_LOCK; // device_index is 1..6 for a specific device, 0x53 is seen for "any // device". Not sure if this is a special value or randomly chosen cmd.device_number = 0; cmd.open_lock_timeout = timeout; if (!set_short_register(fd, DEVICE_RECEIVER, REG_DEVICE_PAIRING, (u8 *) &cmd, &msg)) { return false; } return true; } bool pair_cancel(int fd) { struct hidpp_message msg; struct val_reg_devpair cmd; cmd.action = DEVPAIR_CLOSE_LOCK; // see discussion at pair_start, why did logitech use 0x53? Confusion? cmd.device_number = 0; // timeout applies to open lock, not sure why I saw 0x94 (148 sec) cmd.open_lock_timeout = 0; if (!set_short_register(fd, DEVICE_RECEIVER, REG_DEVICE_PAIRING, (u8 *) &cmd, &msg)) { return false; } return true; } bool device_unpair(int fd, u8 device_index) { struct hidpp_message msg; struct val_reg_devpair cmd; cmd.action = DEVPAIR_DISCONNECT; cmd.device_number = device_index; cmd.open_lock_timeout = 0; if (!set_short_register(fd, DEVICE_RECEIVER, REG_DEVICE_PAIRING, (u8 *) &cmd, &msg)) { return false; } return true; } void perform_pair(int fd, u8 timeout) { struct hidpp_message msg; if (timeout == 0) { timeout = 30; } if (!pair_start(fd, timeout)) { fprintf(stderr, "Failed to send pair request\n"); return; } puts("Please turn your wireless device off and on to start pairing."); // WARNING: mess ahead. I knew it would become messy before writing it. for (;;) { if (!do_read(fd, &msg, SHORT_MESSAGE, timeout * 1000 + 2000)) { fprintf(stderr, "Failed to read short message\n"); break; } if (msg.sub_id == NOTIF_RECV_LOCK_CHANGE) { u8 *bytes = (u8 *) &msg.msg_short; if (msg.report_id != SHORT_MESSAGE || msg.device_index != DEVICE_RECEIVER) { fprintf(stderr, "Received invalid Unifying Receiver Locking Change notification (0x4A)\n"); continue; } if (bytes[0] & 1) { // locking open continue; } else { // locking closed const char *result; switch (bytes[1]) { case 0x00: result = "Success"; break; case 0x01: result = "Timeout"; break; default: result = "Failure"; } printf("Pairing result: %s (%i)\n", result, bytes[1]); break; } } else if (msg.sub_id == NOTIF_DEV_CONNECT) { u8 device_index; bool is_new_dev; if (!process_notif_dev_connect(&msg, &device_index, &is_new_dev)) { // error message is already emitted continue; } if (device_index < 1 || device_index > DEVICES_MAX) { fprintf(stderr, "Invalid device index %#04x\n", device_index); } else if (is_new_dev) { u8 device_type = devices[device_index - 1].device_type; printf("Found new device, id=%#04x %s\n", device_index, device_type_str(device_type)); break; } else { printf("Ignoring existent device id=%#04x\n", device_index); } } } // end of mess if (!pair_cancel(fd)) { fprintf(stderr, "Failed to cancel pair visibility\n"); } } void perform_unpair(int fd, u8 device_index) { struct device *dev = &devices[device_index - 1]; u8 dev_device_type = dev->device_type; // will be overwritten, therefore store it if (!dev->device_present) { printf("Device %#04x does not appear to be paired\n", device_index); return; } if (device_unpair(fd, device_index)) { if (!dev->device_present) { printf("Device %#04x %s successfully unpaired\n", device_index, device_type_str(dev_device_type)); } else { fprintf(stderr, "Unpairing of %#04x possibly failed\n", device_index); } } else { fprintf(stderr, "Failed to send unpair %#04x request\n", device_index); } } // triggers a notification that updates the list of paired devices bool get_all_devices(int fd) { struct hidpp_message msg; struct val_reg_connection_state cval; memset(&cval, 0, sizeof cval); cval.action = CONSTATE_ACTION_LIST_DEVICES; if (!set_short_register(fd, DEVICE_RECEIVER, REG_CONNECTION_STATE, (u8 *) &cval, &msg)) { return false; } return true; } bool get_receiver_info(int fd, struct receiver_info *rinfo) { struct hidpp_message msg; u8 params[3] = {0}; params[0] = 0x03; // undocumented if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { struct msg_receiver_info *info = (struct msg_receiver_info *) &msg.msg_long.str; uint32_t serial_number; memcpy(&serial_number, &info->serial_number, sizeof(serial_number)); rinfo->serial_number = ntohl(serial_number); return true; } return false; } bool get_device_pair_info(int fd, u8 device_index) { struct device *dev = &devices[device_index - 1]; struct hidpp_message msg; u8 params[3] = {0}; params[0] = 0x20 | (device_index - 1); // 0x20..0x2F Unifying Device pairing info if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { struct msg_dev_pair_info *info = (struct msg_dev_pair_info *) &msg.msg_long.str; dev->wireless_pid = (info->pid_msb << 8) | info->pid_lsb; dev->device_type = info->device_type; return true; } return false; } bool get_device_ext_pair_info(int fd, u8 device_index) { struct device *dev = &devices[device_index - 1]; struct hidpp_message msg; u8 params[3] = {0}; params[0] = 0x30 | (device_index - 1); // 0x30..0x3F Unifying Device extended pairing info if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { struct msg_dev_ext_pair_info *info; uint32_t serial_number; info = (struct msg_dev_ext_pair_info *) &msg.msg_long.str; memcpy(&serial_number, &info->serial_number, sizeof(serial_number)); dev->serial_number = ntohl(serial_number); dev->power_switch_location = info->usability_info & 0x0F; return true; } return false; } bool get_device_name(int fd, u8 device_index) { struct device *dev = &devices[device_index - 1]; struct hidpp_message msg; u8 params[3] = {0}; params[0] = 0x40 | (device_index - 1); // 0x40..0x4F Unifying Device Name if (get_long_register(fd, DEVICE_RECEIVER, REG_PAIRING_INFO, params, &msg)) { struct msg_dev_name *name = (struct msg_dev_name *) &msg.msg_long.str; if (name->length > DEVICE_NAME_MAXLEN) { fprintf(stderr, "Invalid name length %#04x for idx=%i\n", name->length, device_index); return false; } memcpy(&dev->name, name->str, name->length); dev->name[name->length] = 0; return true; } return false; } bool get_hidpp_version(int fd, u8 device_index, struct hidpp_version *version) { struct hidpp_message msg; struct msg_short *payload = &msg.msg_short; u8 softwareId = 0x04; // value 1..15 - random choice for 0x4 u8 ping_data = 0x00; // can be any value msg.report_id = SHORT_MESSAGE; msg.device_index = device_index; msg.sub_id = 0x00; // Root feature index memset(payload->value, 0, sizeof payload->value); payload->address = 0x10 | softwareId; payload->value[2] = ping_data; if (!do_write(fd, &msg)) { return false; } for (;;) { if (!do_read(fd, &msg, 0, 3000)) { if (debug_enabled) { fprintf(stderr, "Failed to read HID++ version, device does not respond!\n"); } return false; } if (msg.sub_id == 0x8F) { struct msg_error *error = (struct msg_error *) &msg.msg_error; if (error->sub_id == 0x00 && error->address == (0x10 | softwareId)) { // if error is ERR_INVALID_SUBID (0x01), then HID++ 1.0 if (error->error_code == 0x01) { version->major = 1; version->minor = 0; return true; } else if (debug_enabled) { const char *err_str = error_messages[error->error_code]; fprintf(stderr, "Failed to retrieve version: %#04x (%s)\n", error->error_code, err_str); } // fatal error - is device connected? return false; } // ignore other errors } if (msg.sub_id == 0x00 && (payload->address & 0xF) == softwareId && payload->value[2] == ping_data) { break; // I think we got a version } else if (debug_enabled) { fprintf(stderr, "Ignoring sub_id=%02x\n", msg.sub_id); } // ignore other messages } version->major = payload->value[0]; version->minor = payload->value[1]; return true; } // device_index can also be 0xFF for receiver bool get_device_version(int fd, u8 device_index, u8 version_type, struct val_reg_version *ver) { struct hidpp_message msg; // TODO: not 100% reliable for wireless devices, it may return MSG_ERR // (err=SUCCESS, wtf). Perhaps we need to send another msg type=00 // (whatever the undocumented params are). memset(ver, 0, sizeof *ver); ver->select_field = version_type; if (get_short_register(fd, device_index, REG_VERSION_INFO, (u8 *) ver, &msg)) { memcpy(ver, msg.msg_short.value, sizeof *ver); return true; } return false; } bool get_device_versions(int fd, u8 device_index, struct version *version) { struct val_reg_version ver; memset(version, 0, sizeof *version); if (get_device_version(fd, device_index, VERSION_FIRMWARE, &ver)) { version->fw_major = ver.v1; version->fw_minor = ver.v2; } else { // assume that other versions will fail too return false; } if (get_device_version(fd, device_index, VERSION_FW_BUILD, &ver)) { version->fw_build = (ver.v1 << 8) | ver.v2; } //if (get_device_version(fd, device_index, 3, &ver)) puts("No idea what this is useful for"); if (get_device_version(fd, device_index, VERSION_BOOTLOADER, &ver)) { version->bl_major = ver.v1; version->bl_minor = ver.v2; } return true; } // device index is 1..6 void gather_device_info(int fd, u8 device_index) { if (get_device_pair_info(fd, device_index)) { struct device *dev = &devices[device_index - 1]; dev->device_present = true; get_hidpp_version(fd, device_index, &dev->hidpp_version); get_device_ext_pair_info(fd, device_index); get_device_name(fd, device_index); if (dev->hidpp_version.major == 1 && dev->hidpp_version.minor == 0) { if (get_device_versions(fd, device_index, &dev->version)) { dev->device_available = true; } } else { // TODO: hid++20 support } } else { // retrieve some information from notifier get_all_devices(fd); } } void print_versions(struct version *ver) { // versions are shown as hex. Probably a mistake given that the length // is 3 which can fit 255 instead of FF (and 65535 instead of FF) printf("Firmware version: %03x.%03x.%05x\n", ver->fw_major, ver->fw_minor, ver->fw_build); printf("Bootloader version: BL.%03x.%03x\n", ver->bl_major, ver->bl_minor); } void get_and_print_recv_info(int fd) { if (get_receiver_info(fd, &receiver)) { printf("Serial number: %08X\n", receiver.serial_number); } if (get_device_versions(fd, DEVICE_RECEIVER, &receiver.version)) { print_versions(&receiver.version); } } void print_detailed_device(u8 device_index) { struct device *dev = &devices[device_index - 1]; if (!dev->device_present) { printf("Device %i is not paired\n", device_index); return; } if (dev->hidpp_version.major) { printf("HID++ version: %i.%i\n", dev->hidpp_version.major, dev->hidpp_version.minor); } else { puts("HID++ version: unknown"); } printf("Device index %i\n", device_index); printf("%s\n", device_type_str(dev->device_type)); printf("Name: %s\n", dev->name); printf("Wireless Product ID: %04X\n", dev->wireless_pid); printf("Serial number: %08X\n", dev->serial_number); if (dev->device_available) { print_versions(&dev->version); } else { puts("Device was unavailable, version information not available."); } } void get_device_names(int fd) { u8 i; for (i=0; idevice_present) { continue; } if (!get_device_name(fd, i + 1)) { fprintf(stderr, "Failed to read device name for idx=%i\n", i + 1); } } } static void print_version(void) { fprintf(stderr, "Logitech Unifying tool version " PACKAGE_VERSION "\n" "Copyright (C) 2013 Peter Wu \n"); } void print_all_devices(void) { unsigned i; puts("Connected devices:"); for (i=0; idevice_present) { continue; } printf("idx=%i\t%s\t%s\n", i + 1, device_type_str(dev->device_type), dev->name); } } static void print_usage(const char *program_name) { fprintf(stderr, "Usage: %s [options] cmd [cmd options]\n", program_name); print_version(); fprintf(stderr, "\n" "Generic options:\n" " -d, --device path Bypass detection, specify custom hidraw device.\n" " -D Print debugging information\n" " -h, --help Show this help message\n" "\n" "Commands:\n" " list - show all paired devices\n" " pair [timeout] - Try to pair within \"timeout\" seconds (1 to 255,\n" " default 0 which is an alias for 30s)\n" " unpair idx - Unpair device\n" " info idx - Show more detailed information for a device\n" " receiver-info - Show information about the receiver\n" "In the above lines, \"idx\" refers to the device number shown in the\n" " first column of the list command (between 1 and 6). Alternatively, you\n" " can use the following names (case-insensitive):\n"); print_device_types(); } static bool is_numeric_device_index(const char *str) { char *end; unsigned long device_index = strtoul(str, &end, 0); // if a number was found, there must be no other characters thereafter return !*end && device_index >= 1 && device_index <= DEVICES_MAX; } // Return number of commands and command arguments, -1 on error. If the program // should not run (--help), then 0 is returned and args is NULL. static int validate_args(int argc, char **argv, char ***argsp, char **hidraw_path) { int args_count; char *cmd; int opt; char **args; struct option longopts[] = { { "device", 1, NULL, 'd' }, { "help", 0, NULL, 'h' }, { "version", 0, NULL, 'V' }, { 0, 0, 0, 0 }, }; *argsp = NULL; while ((opt = getopt_long(argc, argv, "+Dd:hV", longopts, NULL)) != -1) { switch (opt) { case 'D': debug_enabled = true; break; case 'd': *hidraw_path = optarg; break; case 'V': print_version(); return 0; case 'h': print_usage(*argv); return 0; default: return -1; } } if (optind >= argc) { // missing command print_usage(*argv); return -1; } *argsp = args = &argv[optind]; args_count = argc - optind - 1; cmd = args[0]; if (!strcmp(cmd, "list") || !strcmp(cmd, "receiver-info")) { /* nothing to check */ } else if (!strcmp(cmd, "pair")) { if (args_count >= 1) { char *end; unsigned long int n; n = strtoul(args[1], &end, 0); if (*end != '\0' || n > 0xFF) { fprintf(stderr, "Timeout must be a number between 0 and 255\n"); return -1; } } } else if (!strcmp(cmd, "unpair") || !strcmp(cmd, "info")) { if (args_count < 1) { fprintf(stderr, "%s requires a device index\n", cmd); return -1; } if (!is_numeric_device_index(args[1]) && device_type_from_str(args[1]) == -1) { fprintf(stderr, "Invalid device type, must be a numeric index or:\n"); print_device_types(); return -1; } } else { fprintf(stderr, "Unrecognized command: %s\n", cmd); return -1; } return args_count; } #ifdef __GNUC__ static FILE *fopen_format(const char *format, ...) __attribute__((format(printf, 1, 2))); #endif static FILE *fopen_format(const char *format, ...) { char buf[1024]; va_list ap; va_start(ap, format); vsnprintf(buf, sizeof buf, format, ap); va_end(ap); return fopen(buf, "r"); } #define RECEIVER_NAME "logitech-djreceiver" int open_hidraw(void) { int fd = -1; glob_t matches; char hiddev_name[32] = {0}; if (!glob("/sys/class/hidraw/hidraw*/device/driver", 0, NULL, &matches)) { size_t i; char buf[1024]; for (i = 0; i < matches.gl_pathc; i++) { ssize_t r; char *name = matches.gl_pathv[i]; const char *last_comp; char *dev_name; FILE *fp; uint32_t vid = 0, pid = 0; r = readlink(name, buf, (sizeof buf) - 1); if (r < 0) { perror(name); continue; } buf[r] = 0; /* readlink does not NUL-terminate */ last_comp = basename(buf); /* retrieve 'hidrawX' name */ dev_name = name + sizeof "/sys/class/hidraw"; *(strchr(dev_name, '/')) = 0; // Assume that the first match is the receiver. Devices bound to the // same receiver may have the same modalias. if ((fp = fopen_format("/sys/class/hidraw/%s/device/modalias", dev_name))) { int m = fscanf(fp, "hid:b%*04Xg%*04Xv%08Xp%08X", &vid, &pid); if (m != 2) { pid = 0; } fclose(fp); } if (vid != VID_LOGITECH) { continue; } if (!strcmp(last_comp, RECEIVER_NAME)) { /* Logitech receiver c52b and c532 - pass. * * Logitech Nano receiver c534 however has * multiple hidraw devices, but the first one is * only used for keyboard events and should be * ignored. The second one is for the mouse, and * that interface has a vendor-specific HID page * for HID++. * Parsing .../device/report_descriptor is much * more complicated, so stick with knowledge * about device-specific interfaces for now. */ if (pid == PID_NANO_RECEIVER_2) { int iface = -1; if ((fp = fopen_format("/sys/class/hidraw/%s/device/../bInterfaceNumber", dev_name))) { fscanf(fp, "%02x", &iface); fclose(fp); } if (iface == 0) { /* Skip first interface. */ continue; } } } else if (!strcmp(last_comp, "hid-generic")) { /* need to test for older nano receiver c52f */ if (pid != PID_NANO_RECEIVER) { continue; } } else { /* unknown driver */ continue; } snprintf(hiddev_name, sizeof hiddev_name, "/dev/%s", dev_name); fd = open(hiddev_name, O_RDWR); if (fd < 0) { perror(hiddev_name); } else { break; } } } if (fd < 0) { if (*hiddev_name) { fprintf(stderr, "Logitech Unifying Receiver device is not accessible.\n" "Try running this program as root or enable read/write permissions\n" "for %s\n", hiddev_name); } else { fprintf(stderr, "No Logitech Unifying Receiver device found\n"); if (access("/sys/class/hidraw", R_OK)) { fputs("The kernel must have CONFIG_HIDRAW enabled.\n", stderr); } if (access("/sys/module/hid_logitech_dj", F_OK)) { fprintf(stderr, "Driver is not loaded, try:" " sudo modprobe hid-logitech-dj\n"); } } } globfree(&matches); return fd; } // returns device index starting at 1 or 0 on failure static u8 find_device_index_for_type(int fd, const char *str, bool *fetched_devices) { char *end; u8 device_index; device_index = strtoul(str, &end, 0); if (*end == '\0') { return device_index; } if (get_all_devices(fd)) { u8 i; int device_type_n; device_type_n = device_type_from_str(str); if (fetched_devices) { *fetched_devices = true; } for (i = 0; i < DEVICES_MAX; i++) { if (devices[i].device_type == device_type_n) { return i + 1; } } } else { fprintf(stderr, "Unable to request a list of paired devices"); } return 0; } int main(int argc, char **argv) { int fd; struct msg_enable_notifs notifs; char *cmd, **args; int args_count; char *hidraw_path = NULL; bool disable_notifs = false; args_count = validate_args(argc, argv, &args, &hidraw_path); if (args_count < 0) { return 1; } else if (args == NULL) { return 0; } cmd = args[0]; if (hidraw_path) { fd = open(hidraw_path, O_RDWR); if (fd < 0) { perror(hidraw_path); } } else { fd = open_hidraw(); } if (fd < 0) { return 1; } if (debug_enabled) { if (!get_and_print_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { goto end_close; } } else { if (!get_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { fprintf(stderr, "Failed to retrieve notification state\n"); goto end_close; } } if (!notifs.reporting_flags_receiver) { disable_notifs = true; notifs.reporting_flags_receiver |= 1; if (set_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { if (debug_enabled) { puts("Successfully enabled notifications"); } } else { fprintf(stderr, "Failed to set HID++ Notification status\n"); } } if (!strcmp(cmd, "pair")) { u8 timeout = 0; if (args_count >= 1) { timeout = (u8) strtoul(args[1], NULL, 0); } perform_pair(fd, timeout); } else if (!strcmp(cmd, "unpair")) { bool fetched_devices = false; u8 device_index; device_index = find_device_index_for_type(fd, args[1], &fetched_devices); if (!fetched_devices && !get_all_devices(fd)) { fprintf(stderr, "Unable to request a list of paired devices\n"); } if (device_index) { perform_unpair(fd, device_index); } else { fprintf(stderr, "Device %s not found\n", args[1]); } } else if (!strcmp(cmd, "list")) { u8 device_count; if (get_connected_devices(fd, &device_count)) { printf("Devices count: %i\n", device_count); } else { fprintf(stderr, "Failed to get connected devices count\n"); } if (get_all_devices(fd)) { get_device_names(fd); print_all_devices(); } else { fprintf(stderr, "Unable to request a list of paired devices\n"); } } else if (!strcmp(cmd, "info")) { u8 device_index; device_index = find_device_index_for_type(fd, args[1], NULL); if (device_index) { struct device *dev = &devices[device_index - 1]; gather_device_info(fd, device_index); print_detailed_device(device_index); if (dev->hidpp_version.major == 2 && dev->hidpp_version.minor == 0) { // TODO: separate fetch/print hidpp20_print_features(fd, device_index); } } else { fprintf(stderr, "Device %s not found\n", args[1]); } } else if (!strcmp(cmd, "receiver-info")) { get_and_print_recv_info(fd); } else { fprintf(stderr, "Unhandled command: %s\n", cmd); } if (disable_notifs) { notifs.reporting_flags_receiver &= ~1; if (set_notifications(fd, DEVICE_RECEIVER, ¬ifs)) { if (debug_enabled) { puts("Successfully disabled notifications"); } } else { fprintf(stderr, "Failed to set HID++ Notification status\n"); } } if (debug_enabled) { get_and_print_notifications(fd, DEVICE_RECEIVER, ¬ifs); } end_close: close(fd); return 0; } ltunify-0.3/notes.txt000066400000000000000000000245351367150467500147660ustar00rootroot00000000000000// vim:syntax=c: // 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) struct dj_report { u8 report_id; u8 device_index; u8 report_type; u8 report_params[DJREPORT_SHORT_LENGTH - 3]; }; // char magic_sequence[] = {0x10, 0xFF, 0x80, 0xB2, 0x01, 0x00, 0x00}; #define REPORT_TYPE_KEYBOARD 0x01 #define REPORT_TYPE_MOUSE 0x02 #define REPORT_TYPE_CONSUMER_CONTROL 0x03 #define REPORT_TYPE_SYSTEM_CONTROL 0x04 #define REPORT_TYPE_MEDIA_CENTER 0x08 #define REPORT_TYPE_LEDS 0x0E #define REPORT_TYPE_NOTIF_DEVICE_UNPAIRED 0x40 #define REPORT_TYPE_NOTIF_DEVICE_PAIRED 0x41 #define REPORT_TYPE_NOTIF_CONNECTION_STATUS 0x42 #define REPORT_TYPE_NOTIF_ERROR 0x7F #define REPORT_TYPE_CMD_SWITCH 0x80 #define REPORT_TYPE_CMD_GET_PAIRED_DEVICES 0x81 dj_report->report_id = REPORT_ID_DJ_SHORT; dj_report->device_index = 0xFF; dj_report->report_type = REPORT_TYPE_CMD_GET_PAIRED_DEVICES; retval = logi_dj_recv_send_report(djrcv_dev, dj_report); report_id = 0x10; device_index = 0xFF; report_type = 0x80; // REPORT_TYPE_CMD_SWITCH report_params = {0xB2, 0x01, 0x00, 0x00}; // { 0xB2 , Connect Devices, Device Number, Open Lock Timeout } // = {0xb2, 0x01, 0x50, 0x3c} // observation: S b203 0100 perform unpair // S b201 533c when discovery is enabled (with no paired devices) // R b200 0000 when discovery is enabled (no paired devices, waiting; also recvd when unpaired while in Advanced mode) // S b202 5394 when discovery is disabled (both with 1 paired kbd and no paired kbd; explicitly issued when closing pair program) // 0:1 1 address // 1:3 3 value (0 is returned on succesfully setting register) // Related to type 0x4A // observations + guesses // issued after 10 ff CMD_SWITCH b2 .. .. .. report_id = 0x10 device_index = 0xFF report_type = 0x4A // receiver status - open for new devices? // R 0100 0000 discovery enabled // R 0001 0000 discovery disabled (issued after timeout) // R 0000 0000 discovery disabled (after CMD_SWITCH b200 0000; succesful pair) // 00000000 00000000 is sent when device is turned off, "null report"? // No paired devices, just started program: // output report_id=10 dev_idx=ff RECV type=83 parms=b5030000 // ep3 report_id=11 dev_idx=ff RECV type=83 parms=b503af4f95 ea150609 00000000 00000000 // No paired devices (same for unpaired kbd off/on), just started program: // output report_id=10 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b5030000 // ep3 report_id=11 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b503af4f95 ea15060a 00000000 00000000 // Press "Advanced", unpaired (same for unpaired kbd on/off): // output report_id=10 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b3000000 // ep3 report_id=11 dev_idx=ff RECV type=83 ?CMD_DEVICE_INFO parms=b308000000 00000000 00000000 00000000 /* report_id=10; dev_id=01 for Sent0+Recv0 and Sent1+Recv1, but ff for Sent1+Recv2 Sent0 Recv0 00120100* 0d000000 810d0200* 07000000 07050000 Sent1 Recv1 Recv2 f1010000 f1012201 f1011201 f1020000 f1020019 f1020019 f1030000 f1030007 81f10300* f1040000 f1040201 f1040214 *) type=8f instead of 81 CMD_GET_PAIRED_DEVICES Order = Sent0+Recv0, Sent1+Recv1, Sent1+Recv2 (+ = interleaved) */ // Discover? (click Advanced and get a lot of this spam) // send: report_id=10 dev_idx=ff type=83 parms=b3000000 # report yourself guys? // // recv: report_id=11 dev_idx=ff type=83 parms=b3a1000000 00000000 00000000 00000000 // ep3 report_id=11 dev_idx=ff type=83 parms=b32a000000 00000000 00000000 00000000 # byte 2 is channel/encrypt key??? // ep3 report_id=11 dev_idx=ff type=83 parms=b331000000 00000000 00000000 00000000 # No devices turned on // ep3 report_id=11 dev_idx=ff type=83 parms=b334000000 00000000 00000000 00000000 # Device pair step 1 (after Sent0+Recv0) // ep3 report_id=11 dev_idx=ff type=83 parms=b336000000 00000000 00000000 00000000 # Device pair step 2 (after Sent1+Recv1) // ep3 report_id=11 dev_idx=ff type=83 parms=b338000000 00000000 00000000 00000000 # Device just paired (after Sent1+Recv2) // Unpair: // send: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b2030100 # R: Unpair device request // // recv: report_id=03 dev_idx=00 type=00 parms=0000 // recv: report_id=10 dev_idx=01 type=40 NOTIF_DEVICE_UNPAIRED parms=02000000 // recv: report_id=00 dev_idx=00 type=00 parms=0000000000 // // recv: report_id=20 dev_idx=01 type=40 NOTIF_DEVICE_UNPAIRED parms=0000000000 00000000 000000 // // recv: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b2000000 # K: Ready to accept other receiver? // Prepare switch? // send: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b201533c # R: Looking for devices? // recv: // recv: report_id=10 dev_idx=ff type=4a parms=01000000 # K: Hi I am a device // recv: // recv: report_id=10 dev_idx=ff type=80 CMD_SWITCH parms=b2000000 # K: I want to pair with you // Switch timeout? (+/- 60 seconds) // recv: report_id=10 dev_idx=ff type=4a parms=00010000 # K: Nevermind, nobody responded // Turn on kbd while pair program is waiting for recv // recv: report_id=10 dev_idx=01 type=41 NOTIF_DEVICE_PAIRED parms=04611020 # L: I just turned myself on // // send: report_id=10 dev_idx=ff type=83 parms=b5400000 # R: Hey, wanna join me? // recv: report_id=20 dev_idx=01 type=41 NOTIF_DEVICE_PAIRED parms=0010201a40 00000000 000000 // // recv: report_id=10 dev_idx=ff type=4a parms=00000000 // // recv: report_id=11 dev_idx=ff type=83 parms=b540044b38 30300000 00000000 00000000 // // send: report_id=10 dev_idx=ff type=83 parms=b5300000 // // recv: report_id=11 dev_idx=ff type=83 parms=b530fb841b 861a4000 00070000 00000000 // b530 // fb841b 86 Serial No // 1a4000 00070000 00000000 ??? // // 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 // type=41 Device Connection // 04 0000 0100 - Unifying protocol // a1 1010 0001 - DeviceType=Keyboard; Link is encrypted; Link is established; Packet with payload // 10 0001 0000 - Wireless PID LSB // 20 0010 0000 - Wireless PID MSB // continued switch. // send: report_id=10 dev_idx=01 type=00 parms=12283f94 // : report_id=10 dev_idx=01 type=4b parms=01000000 // // : report_id=10 dev_idx=01 type=8f parms=00120100 // // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=0d000000 // // : report_id=10 dev_idx=01 type=8f parms=810d0200 // // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=07000000 // // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=07050000 // // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1010000 // // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1012201 // // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1020000 // // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1020019 // // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1030000 // // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1030007 // // send: report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1040000 // // : report_id=10 dev_idx=01 type=81 CMD_GET_PAIRED_DEVICES parms=f1040201 // // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1010000 // : report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1011201 // // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1020000 // : report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1020019 // // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1030000 // : // : report_id=10 dev_idx=ff type=8f parms=81f10300 // : // send: report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1040000 // : report_id=10 dev_idx=ff type=81 CMD_GET_PAIRED_DEVICES parms=f1040214 // http://tequals0.wordpress.com/2011/11/01/reverse-engineering-logitech-unifying-usb-protocol/ /* T: Bus=05 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 3 Spd=12 MxCh= 0 D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=046d ProdID=c52b Rev=12.01 S: Manufacturer=Logitech S: Product=USB Receiver C:* #Ifs= 3 Cfg#= 1 Atr=a0 MxPwr= 98mA I:* If#= 0 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=01 Driver=usbhid E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=8ms I:* If#= 1 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=02 Driver=usbhid E: Ad=82(I) Atr=03(Int.) MxPS= 8 Ivl=2ms I:* If#= 2 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid E: Ad=83(I) Atr=03(Int.) MxPS= 32 Ivl=2ms 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/" see lt/usbmon.awk 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"}' */ ltunify-0.3/read-dev-usbmon.c000066400000000000000000000115411367150467500162220ustar00rootroot00000000000000/* * Tool for reading usbmon messages and writing non-empty data to stdout. * Because of limitations of a single output stream, there is currently a hack * that directly includes hidraw.c. * * Copyright (C) 2013 Peter Wu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include /* getenv */ #include #include /* gettimeofday */ #include /* localtime */ typedef uint16_t u16; typedef int32_t s32; typedef uint64_t u64; typedef int64_t s64; #define SETUP_LEN 8 /* taken from Linux, Documentation/usb/usbmon.txt */ struct usbmon_packet { u64 id; /* 0: URB ID - from submission to callback */ unsigned char type; /* 8: Same as text; extensible. */ unsigned char xfer_type; /* ISO (0), Intr, Control, Bulk (3) */ unsigned char epnum; /* Endpoint number and transfer direction */ unsigned char devnum; /* Device address */ u16 busnum; /* 12: Bus number */ char flag_setup; /* 14: Same as text */ char flag_data; /* 15: Same as text; Binary zero is OK. */ s64 ts_sec; /* 16: gettimeofday */ s32 ts_usec; /* 24: gettimeofday */ int status; /* 28: */ unsigned int length; /* 32: Length of data (submitted or actual) */ unsigned int len_cap; /* 36: Delivered length */ union { /* 40: */ unsigned char setup[SETUP_LEN]; /* Only for Control S-type */ struct iso_rec { /* Only for ISO */ int error_count; int numdesc; } iso; } s; int interval; /* 48: Only for Interrupt and ISO */ int start_frame; /* 52: For ISO */ unsigned int xfer_flags; /* 56: copy of URB's transfer_flags */ unsigned int ndesc; /* 60: Actual number of ISO descriptors */ }; struct mon_get_arg { struct usbmon_packet *hdr; void *data; size_t alloc; /* Length of data (can be zero) */ }; #define MON_IOC_MAGIC 0x92 #define MON_IOCQ_URB_LEN _IO(MON_IOC_MAGIC, 1) #define MON_IOCX_GET _IOW(MON_IOC_MAGIC, 6, struct mon_get_arg) #define NO_MAIN // HACK - otherwise there is no easy wat to tell whether a packet is read or // written from the usbmon #include "hidraw.c" #undef NO_MAIN void print_time(void) { struct timeval tval; struct tm *tm; if (gettimeofday(&tval, NULL)) { perror("gettimeofday"); return; } tm = localtime(&tval.tv_sec); printf("%02d:%02d:%02d.%03ld ", tm->tm_hour, tm->tm_min, tm->tm_sec, tval.tv_usec / 1000); } int main(int argc, char ** argv) { unsigned char data[1024]; struct usbmon_packet hdr; struct mon_get_arg event; int fd, r; if (argc < 2) { fprintf(stderr, "Usage: %s /dev/usbmonX\n", argv[0]); return 1; } fd = open(argv[1], O_RDONLY); if (fd < 0) { perror(argv[1]); return 1; } memset(&hdr, 0, sizeof hdr); event.hdr = &hdr; // hopefully it is OK to use stack for this event.data = &data; event.alloc = sizeof data; //r = ioctl(fd, MON_IOCQ_URB_LEN); //printf("%i\n", r); for (;;) { memset(&data, 0xCC, sizeof data); // for debugging purposes r = ioctl(fd, MON_IOCX_GET, &event); if (r == -1 && errno == EINTR) { continue; } if (r < 0) { perror("ioctl"); break; } // ignore non-data packets if (hdr.len_cap) { if (getenv("HEX")) { unsigned int i; printf("Type=%c\n", hdr.type); for (i=0; i sizeof (struct report)) { fprintf(stderr, "Discarding too large packet of length %u!\n", hdr.len_cap); } else { struct report *report = (struct report *)&data; if (hdr.len_cap < 3) { fprintf(stderr, "Short data len: %i\n", hdr.len_cap); continue; } #define COLOR(c, cstr) "\033[" c "m" cstr "\033[m" print_time(); if (hdr.type == 'C') { printf(COLOR("1;32", "Recv\t")); } else if (hdr.type == 'S') { printf(COLOR("1;31", "Send\t")); } else { printf(COLOR("1;35", "Type=%c\t") "\n", hdr.type); } process_msg(report, hdr.len_cap); fflush(NULL); #if 0 if (write(STDOUT_FILENO, &data, hdr.len_cap) < 0) { perror("write"); break; } #endif } } } close(fd); return 0; } ltunify-0.3/registers.txt000066400000000000000000000634141367150467500156440ustar00rootroot00000000000000Overview USB VID 0x046d USB PID 0xc52b Message Sub ID 0x80 SET_REGISTER 0x81 GET_REGISTER 0x82 SET_LONG_REGISTER 0x83 GET_LONG_REGISTER 0x8F ERROR_MSG Notifications 0x40 Device Disconnection 0x41 Device Connection 0x4A Unifying Receiver Locking Change information Registers 0x00 Enable HID++ Notifications 0x02 Connection State 0xB2 Device Connection and Disconnection (Pairing) 0xB3 Device Activity 0xB5 Pairing Information (more undocumented below) Not documented: Long register B5 (Pairing Information) - nn=03 "Receiver information?" Send: 10 FF 83 B5 03 00 00 00 - Short (10) message for receiver (FF) to retrieve long register (83) PairingInfo (B5) with special param 03 00 00 00. Recv: 11 FF 83 B5 03 AF 4F 95 EA 05 06 0E 00 00 00 00 00 00 00 00 11 - Long message FF - Receiver target 83 - LONG_REGISTER_RESPONSE B5 - Pairing Info 03 - "Receiver information"? AF 4F 95 EA - Serial Number of receiver 05 - dunno, also seen: 02, 18 06 - Max Device Capability? (not sure, but it is six) 0E - dunno, also seen: 32, 47 00 00 00 00 00 00 00 00 - padding? Remaining information: - Wireless Status (0x03) - ModelId (0x46d c52b) - Handle: 0xff000001 (0xff is device ID, 01 is internal to the software, ordered) - Dfu Status (0x1) - Is Dfu Cancellable (yes) DFU = Device Firmware Upgrade 0x00 - Up-to-date 0x01 - Unknown 0x02 - Available 0x03 - Initiated 0x04 - Failed 0x05 - Succeeded 0x06 - Newer software needed 0x06 - Unrecoverable Short register F1 - "Version information" (guessed) Header: 10 FF 81 (short msg, receiver is target, GET_REGISTER) Data: F1 nn xx yy (xx yy is empty for request and contains version for response) nn=01 xx,yy=12 01 nn=02 xx,yy=00 19 nn=03 error 03 (Invalid address) (but returns 00 07 for keyboard) nn=04 xx,yy=02 14 Displayed firmware version: 012.001.00019 (x1 . y1 . x2 y2) Displayed bootloader version: BL.002.014 (BL . x4 . y4) More undocumented short regs **for K800 keyboard**: 17 rw Illumination info get 00 00 00 - Retrieve illimunation status? rsp: 3C 00 02 - (illimunation is disabled) set 3C 00 01 - Activate illumination only when I start typing set 3C 00 02 - Disable keyboard illumination Notif [10 ix 17] 3C 00 00 0n received when the keyboard illumination level is changed. (n = 1..5; 1=off; 5=full brightness) (NOTE: needs notif flag 00 02 00) 01 rw "Keyboard hand detection?" get 00 00 00 - retrieve keyboard hand detection status? rsp: 00 00 20 - (hand detection is disabled) set 00 00 00 - Enable hand detection set 00 00 20 - Disable hand detection 09 rw "F key functions" get 00 00 00 - Retrieve F key function state rsp: 00 00 00 - (F key functions are not swapped) set 00 01 00 - Swap F key functions set 00 00 00 - Do not swap F key functions 00 rw ENABLED_NOTIFS, 10 02 00, 10 is Battery info, buy what is 02? After writing FF FF FF, reading the register shows 13 02 00 .1 .. .. - If enabled, keys like Web and Mute are passed over the HID protocol with short message type=03. This presumably allows the user to customize key bindings. .2 .. .. - If enabled, Fn+F8 (Sleep button) generates a HID++ short message instead of a key event that is captured by the Linux input layer (10 ix 04 0s 00 00 00 where s=1 when pressed and s=0 when released). 10:53:54.914 Recv report_id=10 short device=01 DEV1 type=04 SYSTEM_CONTROL params=01 00 00 00 1. .. .. - battery status (documented) (see also below, register/type 07) .. .2 .. - backlight changes (pressing Fn+F[56]) (more details about bits 0 and 1 of flag 1 in keyboard.txt) 07 r Likely the battery status of the kbd, not observed for M525 mouse get 00 00 00 rsp: 07 00 00 - (Battery full?) rsp: 01 00 00 - (Battery almost empty? Flashing red light on keyboard) Undocumented short type=07 for KBD dev: 10 02 07 01 00 00 00; sent when pressing the Fn+F7 key for battery information. 01 is presumably the battery level (red, danger, etc)) (on kbd enable, 10 02 07 01 00 00 00 is sent ONLY if notification is enabled after NOTIF_DEVICE_PAIRED (0x41) / ?NOTIF_PAIR_ACCEPTED (0x4B); not observed when battery is not near dead) Format: 10 DEVID 07 00 00 (or: response to reading register, 10 DEVID 81 07 07 00) Battery level: 00 is not possible for battery level, when the keyboard went off because of zero power, 01 was still the reported value 01 - red zone 03 - one bar 05 - two bars 07 - three bars Some magic: 00 - after unplugging/when not connected 22 - fully charged 25 - charging 26 - notification when battery is fully charged [10 ix 07] 07 26 00 00 NOTE: 10 ix 07 notifications are only available when r0bit4 (Battery status) of Enable Hid++ Notifications (type=0x00) is enabled. This notification is then sent when pressing the Fn+F7 (battery) key or when an event happens (battery full). Recv report_id=10 short device=02 DEV2 type=41 NOTIF_DEVICE_PAIRED params=04 A1 10 20 # Hello, I am a device! 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? Recv report_id=10 short device=02 DEV2 type=4B ?NOTIF_PAIR_ACCEPTED params=01 00 00 00 # Guessed: pair succeeded? Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=10 00 00 # Enable battery status notifs Recv report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 00 00 # succesfully set Recv report_id=10 short device=02 DEV2 type=07 params=01 00 00 00 # battery low notif! 17 rw ??? Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=3C 00 01 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=3C 00 01 report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=07 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=10 02 00 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=10 02 00 report_id=10 short device=02 DEV2 type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 params=3C 00 01 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=3C 00 01 report_id=10 short device=02 DEV2 type=80 SET_REG reg=17 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 params=00 00 00 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 report_id=10 short device=02 DEV2 type=80 SET_REG reg=01 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 params=00 00 00 Send report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 report_id=10 short device=02 DEV2 type=80 SET_REG reg=09 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=00 00 00 report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 params=07 00 00 Enabled HID++ Notifications for receiver Serial number: 03AF4F95 (shipped with K800 keyboard) Firmware version: 012.001.00019 Bootloader version: BL.002.014 Supported notifs: 00 09 00 Serial number: 08D89AA6 (shipped with M525 mouse) Firmware version: 024.000.00018 Bootloader version: BL.000.006 Supported notifs: 00 19 00 ^--- besides Wireless notifs (r1bit1) and Software present (r1bit3) it also supports another flag: r1bit4 (unknown functionality) Documented in hidpp10: Allows for testing protocol version. Request: 10 DeviceIndex 00 1n 00 00 uu - n is SwId - UU is "ping data" defined by SW Responses: HID++ 1.0: 10 ix 8F 00 Fn 01 00 (ERR_INVALID_SUBID) HID++ 2.0: 10 ix 00 1n 02 00 UU HID++ X.Y: 10 ix 00 1n XX YY UU Discovery: 1. Get Reg 00 2. Set Reg 00 to value from (1) with bit 0 of the second byte enabled (v1[1] |= 1) 3. Send read CONNECTION_STATE register for total number of devices 4. Write CONNECTION_STATE 02 00 00 register 5. (4) triggers a 0x41 notification (Device Paired notification) for each device. Note, device index does not have to start at 1 (if device got unpaired before). 5. Got response for (3). 6. (disable discovery) Get Reg 00 7. Set Reg 00 to value from (6) with bit 0 of the second byte disabled (v1[1] &= ~1) Startup (all targeted at receiver, notifications come from device 1..6): 1. Get receiver details from pairing info register (serial number, etc) 2. Get firmware version (recv) 3. Get bootloader version (recv) 4. Get enabled notifications 5. Enable wireless notifications (set enabled notifs reg) 6. Read connection state register for number of machines for each machine from (6): 7. Write params to connection state register (02 00 00 was written) (read returns after (8)) (possibly a "trigger report all paired devices") 8. (7) immediately triggers a Device Paired notification for a previously paired dev 9. Send/Read request for paired device device name 10. Send/Read request for paired device extended info (serial, .., location of power switch) 11. Read fw+bootloader version information again (this seems useless?) A1. Pairing starts: write DEVICE_PAIRING register, enable pairing with timeout (read returns after (A2)) A2. (A1) triggers Receiver Lock Changed notification (subid=0x41) (reason: no error) A3. On timeout, Receiver Lock Changed notification is received (reason: timeout) A4. (close button) On close, DEVICE_PAIRING register is written to disable pairing discovery A5. Receiver Lock Changed notification is immediately received (reason: timeout) Advanced: B1. Advanced button is pressed. Polling starts, every x time, DEVICE_ACTIVITY register request is sent/read B2. Unpair device, Device Unpaired notification (subid=0x40) received (reason: device disconnected) (note, report id 10 and 20 received, ignore 20) Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=03 00 00 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 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 12 01 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 report_id=10 short device=FF RECV type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 14 Send report_id=10 short device=FF RECV type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 01 00 Send report_id=10 short device=FF RECV type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 01 00 report_id=10 short device=FF RECV type=80 SET_REG reg=00 ENABLED_NOTIFS params=00 00 00 Send report_id=10 short device=FF RECV type=81 GET_REG reg=02 CONNECTION_STATE params=00 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=02 CONNECTION_STATE params=00 01 00 Send report_id=10 short device=FF RECV type=80 SET_REG reg=02 CONNECTION_STATE params=02 00 00 report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=04 61 10 20 report_id=10 short device=FF RECV type=80 SET_REG reg=02 CONNECTION_STATE params=00 00 00 Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=40 00 00 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 Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=30 00 00 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 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 12 01 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 report_id=10 short device=FF RECV type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 14 No paired devices, start pairing: 1. Enable device pairing (response read returns after (2)) 2. Got receiver lock notification (locking open) 3. Enable device, got Device Paired notif (kbd, link encrypted, link not established) 4. Send read request for device name (response in (6)) (5. got report_id=20 for device paired notif) 5. Got Receiver Lock changed notification (lock closed) 6. Response for device name (sent in (4)), Send/Read request for paired device extended info (serial, .., location of power switch) 7. Got Device Paired notif (kbd, link encrypted, link established, link with payload) 8. (??) Send dev1 [header 10 01 00] 12 28 3F 94 9. Got notification ("Pair accepted"?) [header 10 01 4B] 01 00 00 00 10. Dev1 request (8) returned error message (err=01 SUCCESS) 11. Read dev1 register 0D, but it returns an error (err=02 INVALID_SUBID) 12. Request/read dev1 register 07 13. Read dev1 firmware, bootloader version 14. Read recv firmware, bootloader version 15. Write DEVICE_PAIRING register (close lock) 16. Got Receiver Lock changed notification (lock closed) (was already closed in (5) though) Send report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=01 53 3C report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED params=01 00 00 00 report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=00 00 00 report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=04 61 10 20 Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=40 00 00 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 report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED params=00 00 00 00 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 Send report_id=10 short device=FF RECV type=83 GET_LONG_REG reg=B5 PAIRING_INFO params=30 00 00 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 report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED params=04 A1 10 20 Send report_id=10 short device=01 DEV1 type=00 params=12 28 3F 94 report_id=10 short device=01 DEV1 type=4B ?NOTIF_PAIR_ACCEPTED params=01 00 00 00 report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=00 reg=12 err=01 SUCCESS Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=0D params=00 00 00 report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=81 GET_REG reg=0D err=02 INVALID_SUBID Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=07 params=00 00 00 report_id=10 short device=01 DEV1 type=81 GET_REG reg=07 params=07 00 00 Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=01 22 01 Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 07 Send report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 report_id=10 short device=01 DEV1 type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 01 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=01 12 01 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=02 00 19 Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=03 00 00 report_id=10 short device=FF RECV type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS Send report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 00 00 report_id=10 short device=FF RECV type=81 GET_REG reg=F1 VERSION_INFO? params=04 02 14 Send report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=02 53 94 report_id=10 short device=FF RECV type=80 SET_REG reg=B2 DEVICE_PAIRING params=00 00 00 report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED params=00 00 00 00 Somewhere in the below stream is the battery condition of the M525 mouse (good). It could also be that there is no battery information at all and that SetPoint is faking the information. I have tried brand-new Duracell batteries and the output is still the same. Send report_id=10 short device=01 DEV1 type=00 params=05 00 03 00 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 Send report_id=10 short device=01 DEV1 type=02 MOUSE params=05 00 00 00 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 Send report_id=10 short device=01 DEV1 type=02 MOUSE params=15 00 00 00 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 Send report_id=10 short device=01 DEV1 type=04 SYSTEM_CONTROL params=05 00 00 00 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 Send report_id=10 short device=01 DEV1 type=0B params=15 01 00 00 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 The mouse got some messages when SetPoint is closed: Send report_id=10 short device=01 DEV1 type=0B params=15 00 00 00 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 Send report_id=10 short device=01 DEV1 type=04 SYSTEM_CONTROL params=05 00 00 00 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 Back to alive (very, very red battery for keyboard): Recv report_id=10 short device=02 DEV2 type=41 NOTIF_DEVICE_PAIRED params=04 A1 10 20 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 Send report_id=20 unkn device=02 DEV2 type=0E LEDS params=00 00 00 00 00 00 00 00 00 00 00 00 All registers for K800 (HID++ 1.0): Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=00 ENABLED_NOTIFS params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 KBD_HAND_DETECT? params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=01 KBD_HAND_DETECT? params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 BATTERY? params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=07 BATTERY? params=03 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 FN_KEY_SWAP? params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=09 FN_KEY_SWAP? params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 ILLUMINATION_INFO? params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=17 ILLUMINATION_INFO? params=3C 00 01 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=51 params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=51 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=54 params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=54 params=FF 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=D0 params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=D0 params=00 00 00 Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=F1 VERSION_INFO? params=00 00 00 Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=81 GET_REG reg=F1 VERSION_INFO? err=03 INVALID_ADDRESS Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=F2 params=00 00 00 Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=81 GET_REG reg=F2 err=02 INVALID_SUBID Send report_id=10 short device=02 DEV2 type=81 GET_REG reg=F3 params=00 00 00 Recv report_id=10 short device=02 DEV2 type=81 GET_REG reg=F3 params=00 00 00 Send report_id=10 short device=02 DEV2 type=83 GET_LONG_REG reg=0F params=00 00 00 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 Observations about reading regs: - The M525 HID++ 2.0 device does not reply with anything useful to any register request (GET_REG or GET_LONG_REG with 00 params). - eading an unknown register on K800 keyboard (HID++ 1.0) yields: Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=81 GET_REG reg=FE err=02 INVALID_SUBID Recv report_id=10 short device=02 DEV2 type=8F _ERROR_MSG SubID=83 GET_LONG_REG reg=FE err=02 INVALID_SUBID - On a M525 mouse (HID++ 2.0), this shows SUCCESS instead: Recv report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=81 GET_REG reg=FE err=01 SUCCESS Recv report_id=10 short device=01 DEV1 type=8F _ERROR_MSG SubID=83 GET_LONG_REG reg=FE err=01 SUCCESS ltunify-0.3/shell000077500000000000000000000054521367150467500141270ustar00rootroot00000000000000#!/bin/bash # Utilities for HID++ accesses. # # Author: Peter Wu hidw() { local hiddev arg bytes fillchar length oIFS actualArgs hiddev=$1; shift bytes=() case $hiddev in ''|-h|--help|-?) cat < $length" return 1 fi fi echo "${bytes[@]}" | xxd -ps -r > "$hiddev" } _hidpp_main() { local cmd local cmds=(hidw) cmd=$1; shift for c in "${cmds[@]}"; do if [[ $cmd == $c ]]; then $cmd "$@" return fi done case $cmd in ''|-h|--help|-?) cat < # Date: 2013-04-04 BEGIN { OFS=" "; # Taken from Linux source, drivers/hid/hid-logitech-dj.h # Catgegories are taken from patent description of US8386651 # 0x00 - 0x3F HID reports types["01"] = "KEYBOARD"; types["02"] = "MOUSE"; types["03"] = "CONSUMER_CONTROL"; types["04"] = "SYSTEM_CONTROL"; types["08"] = "MEDIA_CENTER"; types["0E"] = "LEDS"; # 0x40 - 0x7F enumerator notifications types["40"] = "NOTIF_DEVICE_UNPAIRED"; types["41"] = "NOTIF_DEVICE_PAIRED"; types["42"] = "NOTIF_CONNECTION_STATUS"; types["4A"] = "NOTIF_RECV_LOCK_CHANGED"; # 0100 0000 = ready for connections, 0010 0000 = new connections disabled types["4B"] = "?NOTIF_PAIR_ACCEPTED"; # 0100 0000 types["7F"] = "NOTIF_ERROR"; # 0x80 - 0xFF enumerator commands; Register Access types["80"] = "SET_REG"; # CMD_SWITCH types["81"] = "GET_REG"; # CMD_GET_PAIRED_DEVICES types["82"] = "SET_LONG_REG"; types["83"] = "GET_LONG_REG"; types["8F"] = "_ERROR_MSG"; # Align type name maxlen=0; for (i in types) { if (maxlen < length(types[i])) { maxlen = length(types[i]); } } # error messages for type=8F (ERROR_MSG) errmsgs["01"] = "SUCCESS"; errmsgs["02"] = "INVALID_SUBID"; errmsgs["03"] = "INVALID_ADDRESS"; errmsgs["04"] = "INVALID_VALUE"; errmsgs["05"] = "CONNECT_FAIL"; errmsgs["06"] = "TOO_MANY_DEVICES"; errmsgs["07"] = "ALREADY_EXISTS"; errmsgs["08"] = "BUSY"; errmsgs["09"] = "UNKNOWN_DEVICE"; errmsgs["0a"] = "RESOURCE_ERROR"; errmsgs["0b"] = "REQUEST_UNAVAILABLE"; errmsgs["0c"] = "INVALID_PARAM_VALUE"; errmsgs["0d"] = "WRONG_PIN_CODE"; regs["00"] = "ENABLED_NOTIFS"; regs["02"] = "CONNECTION_STATE"; regs["b2"] = "DEVICE_PAIRING"; regs["b3"] = "DEVICE_ACTIVITY"; regs["b5"] = "PAIRING_INFO"; } # end of BEGIN function colorize(col, s) { return "\033[" col "m" s "\033[m"; } # global color function c(s) { return colorize(color, s); } function endPoint(ep) { if (ep == "0") return "output"; if (ep == "1") return " input"; if (ep == "2") return colorize("1;33", "enumIf"); # if (ep == "3") return " ???"; # seen in the output of usbmon return sprintf("%6s", "ep" ep); } function dev(hex) { if (hex == "ff") { return "RECV"; } if (int(hex) >= 1 && int(hex) <= 6) { return "DEV" int(hex) } return " "; } function typeStr(hex) { return sprintf("%-" maxlen "s", types[toupper(hex)]); } function payload(type, p) { v1 = substr(p, 1, 2); if (type == "8f") { # error er=substr(p, 5, 2); reg=substr(p, 3, 2); parms = "SubID=" v1 parms = parms ", Reg=" c(reg) " " regs[reg]; parms = parms ", er=" c(er); parms = parms " " errmsgs[er]; } else if (type == "80" || type == "81" || type == "82" || type == "83") { parms = "reg=" c(v1) " " regs[v1]; parms = parms " parms=" c(substr(p, 3)); } else { parms = "parms=" c(p); } return parms; } { if (match($0, /.*?:[0-9]+:[0-9]{3,}:([0-9]+) .*? = (..)(..)(..)(..) (.*)/, a)) { # length 85 is ok for most, but not when starting logitech program if (length($0) > 100) { print $0; $0 = ""; } printf("%-100s", $0); color = "1;32"; # sending data instead of receiving data if ($0 ~ " s ") color = "1;31"; print " " endPoint(a[1]), "report_id=" c(a[2]), "dev_idx=" c(a[3]) " " dev(a[3]), "type=" c(a[4]) " " typeStr(a[4]), payload(a[4], a[5] a[6]); } else { print colorize("1;30", $0); } fflush(); }