330 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			330 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
| # -*- python-mode -*-
 | |
| # -*- coding: UTF-8 -*-
 | |
| 
 | |
| ## Copyright (C) 2012-2013  Daniel Pavel
 | |
| ##
 | |
| ## This program is free software; you can redistribute it and/or modify
 | |
| ## it under the terms of the GNU General Public License as published by
 | |
| ## the Free Software Foundation; either version 2 of the License, or
 | |
| ## (at your option) any later version.
 | |
| ##
 | |
| ## This program is distributed in the hope that it will be useful,
 | |
| ## but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| ## GNU General Public License for more details.
 | |
| ##
 | |
| ## You should have received a copy of the GNU General Public License along
 | |
| ## with this program; if not, write to the Free Software Foundation, Inc.,
 | |
| ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | |
| 
 | |
| from __future__ import absolute_import, division, print_function, unicode_literals
 | |
| 
 | |
| from logging import getLogger  # , DEBUG as _DEBUG
 | |
| _log = getLogger(__name__)
 | |
| del getLogger
 | |
| 
 | |
| 
 | |
| from .common import (strhex as _strhex,
 | |
| 					bytes2int as _bytes2int,
 | |
| 					int2bytes as _int2bytes,
 | |
| 					NamedInts as _NamedInts,
 | |
| 					FirmwareInfo as _FirmwareInfo)
 | |
| from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
 | |
| 
 | |
| #
 | |
| # Constants - most of them as defined by the official Logitech HID++ 1.0
 | |
| # documentation, some of them guessed.
 | |
| #
 | |
| 
 | |
| DEVICE_KIND = _NamedInts(
 | |
| 				keyboard=0x01,
 | |
| 				mouse=0x02,
 | |
| 				numpad=0x03,
 | |
| 				presenter=0x04,
 | |
| 				trackball=0x08,
 | |
| 				touchpad=0x09)
 | |
| 
 | |
| POWER_SWITCH_LOCATION = _NamedInts(
 | |
| 				base=0x01,
 | |
| 				top_case=0x02,
 | |
| 				edge_of_top_right_corner=0x03,
 | |
| 				top_left_corner=0x05,
 | |
| 				bottom_left_corner=0x06,
 | |
| 				top_right_corner=0x07,
 | |
| 				bottom_right_corner=0x08,
 | |
| 				top_edge=0x09,
 | |
| 				right_edge=0x0A,
 | |
| 				left_edge=0x0B,
 | |
| 				bottom_edge=0x0C)
 | |
| 
 | |
| # Some flags are used both by devices and receivers. The Logitech documentation
 | |
| # mentions that the first and last (third) byte are used for devices while the
 | |
| # second is used for the receiver. In practise, the second byte is also used for
 | |
| # some device-specific notifications (keyboard illumination level). Do not
 | |
| # simply set all notification bits if the software does not support it. For
 | |
| # example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless
 | |
| # the software is updated to handle that event.
 | |
| # Observations:
 | |
| # - wireless and software present were seen on receivers, reserved_r1b4 as well
 | |
| # - the rest work only on devices as far as we can tell right now
 | |
| # In the future would be useful to have separate enums for receiver and device notification flags,
 | |
| # but right now we don't know enough.
 | |
| NOTIFICATION_FLAG = _NamedInts(
 | |
| 				battery_status=         0x100000,  # send battery charge notifications (0x07 or 0x0D)
 | |
| 				keyboard_sleep_raw=     0x020000,  # system control keys such as Sleep
 | |
| 				keyboard_multimedia_raw=0x010000,  # consumer controls such as Mute and Calculator
 | |
| 				# reserved_r1b4=        0x001000,  # unknown, seen on a unifying receiver
 | |
| 				software_present=       0x000800,  # .. no idea
 | |
| 				keyboard_illumination=  0x000200,  # illumination brightness level changes (by pressing keys)
 | |
| 				wireless=               0x000100,  # notify when the device wireless goes on/off-line
 | |
| 				)
 | |
| 
 | |
| ERROR = _NamedInts(
 | |
| 				invalid_SubID__command=0x01,
 | |
| 				invalid_address=0x02,
 | |
| 				invalid_value=0x03,
 | |
| 				connection_request_failed=0x04,
 | |
| 				too_many_devices=0x05,
 | |
| 				already_exists=0x06,
 | |
| 				busy=0x07,
 | |
| 				unknown_device=0x08,
 | |
| 				resource_error=0x09,
 | |
| 				request_unavailable=0x0A,
 | |
| 				unsupported_parameter_value=0x0B,
 | |
| 				wrong_pin_code=0x0C)
 | |
| 
 | |
| PAIRING_ERRORS = _NamedInts(
 | |
| 				device_timeout=0x01,
 | |
| 				device_not_supported=0x02,
 | |
| 				too_many_devices=0x03,
 | |
| 				sequence_timeout=0x06)
 | |
| 
 | |
| BATTERY_APPOX = _NamedInts(
 | |
| 				empty = 0,
 | |
| 				critical = 5,
 | |
| 				low = 20,
 | |
| 				good = 50,
 | |
| 				full = 90)
 | |
| 
 | |
| """Known registers.
 | |
| Devices usually have a (small) sub-set of these. Some registers are only
 | |
| applicable to certain device kinds (e.g. smooth_scroll only applies to mice."""
 | |
| REGISTERS = _NamedInts(
 | |
| 				# only apply to receivers
 | |
| 				receiver_connection=0x02,
 | |
| 				receiver_pairing=0xB2,
 | |
| 				devices_activity=0x2B3,
 | |
| 				receiver_info=0x2B5,
 | |
| 
 | |
| 				# only apply to devices
 | |
| 				mouse_button_flags=0x01,
 | |
| 				keyboard_hand_detection=0x01,
 | |
| 				battery_status=0x07,
 | |
| 				keyboard_fn_swap=0x09,
 | |
| 				battery_charge=0x0D,
 | |
| 				keyboard_illumination=0x17,
 | |
| 				three_leds=0x51,
 | |
| 				mouse_dpi=0x63,
 | |
| 
 | |
| 				# apply to both
 | |
| 				notifications=0x00,
 | |
| 				firmware=0xF1,
 | |
| 			)
 | |
| 
 | |
| #
 | |
| # functions
 | |
| #
 | |
| 
 | |
| def read_register(device, register_number, *params):
 | |
| 	assert device, 'tried to read register %02X from invalid device %s' % (register_number, device)
 | |
| 	# support long registers by adding a 2 in front of the register number
 | |
| 	request_id = 0x8100 | (int(register_number) & 0x2FF)
 | |
| 	return device.request(request_id, *params)
 | |
| 
 | |
| 
 | |
| def write_register(device, register_number, *value):
 | |
| 	assert device, 'tried to write register %02X to invalid device %s' % (register_number, device)
 | |
| 	# support long registers by adding a 2 in front of the register number
 | |
| 	request_id = 0x8000 | (int(register_number) & 0x2FF)
 | |
| 	return device.request(request_id, *value)
 | |
| 
 | |
| 
 | |
| def get_battery(device):
 | |
| 	assert device
 | |
| 	assert device.kind is not None
 | |
| 	if not device.online:
 | |
| 		return
 | |
| 
 | |
| 	"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
 | |
| 	if device.protocol and device.protocol >= 2.0:
 | |
| 		# let's just assume HID++ 2.0 devices do not provide the battery info in a register
 | |
| 		return
 | |
| 
 | |
| 	for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
 | |
| 		if r in device.registers:
 | |
| 			reply = read_register(device, r)
 | |
| 			if reply:
 | |
| 				return parse_battery_status(r, reply)
 | |
| 			return
 | |
| 
 | |
| 	# the descriptor does not tell us which register this device has, try them both
 | |
| 	reply = read_register(device, REGISTERS.battery_charge)
 | |
| 	if reply:
 | |
| 		# remember this for the next time
 | |
| 		device.registers.append(REGISTERS.battery_charge)
 | |
| 		return parse_battery_status(REGISTERS.battery_charge, reply)
 | |
| 
 | |
| 	reply = read_register(device, REGISTERS.battery_status)
 | |
| 	if reply:
 | |
| 		# remember this for the next time
 | |
| 		device.registers.append(REGISTERS.battery_status)
 | |
| 		return parse_battery_status(REGISTERS.battery_status, reply)
 | |
| 
 | |
| 
 | |
| def parse_battery_status(register, reply):
 | |
| 	if register == REGISTERS.battery_charge:
 | |
| 		charge = ord(reply[:1])
 | |
| 		status_byte = ord(reply[2:3]) & 0xF0
 | |
| 		status_text = (BATTERY_STATUS.discharging if status_byte == 0x30
 | |
| 				else BATTERY_STATUS.recharging if status_byte == 0x50
 | |
| 				else BATTERY_STATUS.full if status_byte == 0x90
 | |
| 				else None)
 | |
| 		return charge, status_text
 | |
| 
 | |
| 	if register == REGISTERS.battery_status:
 | |
| 		status_byte = ord(reply[:1])
 | |
| 		charge = (BATTERY_APPOX.full if status_byte == 7 # full
 | |
| 			else BATTERY_APPOX.good if status_byte == 5 # good
 | |
| 			else BATTERY_APPOX.low if status_byte == 3 # low
 | |
| 			else BATTERY_APPOX.critical if status_byte == 1 # critical
 | |
| 			# pure 'charging' notifications may come without a status
 | |
| 			else BATTERY_APPOX.empty)
 | |
| 
 | |
| 		charging_byte = ord(reply[1:2])
 | |
| 		if charging_byte == 0x00:
 | |
| 			status_text = BATTERY_STATUS.discharging
 | |
| 		elif charging_byte & 0x21 == 0x21:
 | |
| 			status_text = BATTERY_STATUS.recharging
 | |
| 		elif charging_byte & 0x22 == 0x22:
 | |
| 			status_text = BATTERY_STATUS.full
 | |
| 		else:
 | |
| 			_log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte)
 | |
| 			status_text = None
 | |
| 
 | |
| 		if charging_byte & 0x03 and status_byte == 0:
 | |
| 			# some 'charging' notifications may come with no battery level information
 | |
| 			charge = None
 | |
| 
 | |
| 		return charge, status_text
 | |
| 
 | |
| 
 | |
| def get_firmware(device):
 | |
| 	assert device
 | |
| 
 | |
| 	firmware = [None, None, None]
 | |
| 
 | |
| 	reply = read_register(device, REGISTERS.firmware, 0x01)
 | |
| 	if not reply:
 | |
| 		# won't be able to read any of it now...
 | |
| 		return
 | |
| 
 | |
| 	fw_version = _strhex(reply[1:3])
 | |
| 	fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
 | |
| 	reply = read_register(device, REGISTERS.firmware, 0x02)
 | |
| 	if reply:
 | |
| 		fw_version += '.B' + _strhex(reply[1:3])
 | |
| 	fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
 | |
| 	firmware[0] = fw
 | |
| 
 | |
| 	reply = read_register(device, REGISTERS.firmware, 0x04)
 | |
| 	if reply:
 | |
| 		bl_version = _strhex(reply[1:3])
 | |
| 		bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
 | |
| 		bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
 | |
| 		firmware[1] = bl
 | |
| 
 | |
| 	reply = read_register(device, REGISTERS.firmware, 0x03)
 | |
| 	if reply:
 | |
| 		o_version = _strhex(reply[1:3])
 | |
| 		o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
 | |
| 		o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
 | |
| 		firmware[2] = o
 | |
| 
 | |
| 	if any(firmware):
 | |
| 		return tuple(f for f in firmware if f)
 | |
| 
 | |
| 
 | |
| def set_3leds(device, battery_level=None, charging=None, warning=None):
 | |
| 	assert device
 | |
| 	assert device.kind is not None
 | |
| 	if not device.online:
 | |
| 		return
 | |
| 
 | |
| 	if REGISTERS.three_leds not in device.registers:
 | |
| 		return
 | |
| 
 | |
| 	if battery_level is not None:
 | |
| 		if battery_level < BATTERY_APPOX.critical:
 | |
| 			# 1 orange, and force blink
 | |
| 			v1, v2 = 0x22, 0x00
 | |
| 			warning = True
 | |
| 		elif battery_level < BATTERY_APPOX.low:
 | |
| 			# 1 orange
 | |
| 			v1, v2 = 0x22, 0x00
 | |
| 		elif battery_level < BATTERY_APPOX.good:
 | |
| 			# 1 green
 | |
| 			v1, v2 = 0x20, 0x00
 | |
| 		elif battery_level < BATTERY_APPOX.full:
 | |
| 			# 2 greens
 | |
| 			v1, v2 = 0x20, 0x02
 | |
| 		else:
 | |
| 			# all 3 green
 | |
| 			v1, v2 = 0x20, 0x22
 | |
| 		if warning:
 | |
| 			# set the blinking flag for the leds already set
 | |
| 			v1 |= (v1 >> 1)
 | |
| 			v2 |= (v2 >> 1)
 | |
| 	elif charging:
 | |
| 		# blink all green
 | |
| 		v1, v2 = 0x30,0x33
 | |
| 	elif warning:
 | |
| 		# 1 red
 | |
| 		v1, v2 = 0x02, 0x00
 | |
| 	else:
 | |
| 		# turn off all leds
 | |
| 		v1, v2 = 0x11, 0x11
 | |
| 
 | |
| 	write_register(device, REGISTERS.three_leds, v1, v2)
 | |
| 
 | |
| 
 | |
| def get_notification_flags(device):
 | |
| 	assert device
 | |
| 
 | |
| 	# Avoid a call if the device is not online,
 | |
| 	# or the device does not support registers.
 | |
| 	if device.kind is not None:
 | |
| 		# peripherals with protocol >= 2.0 don't support registers
 | |
| 		if device.protocol and device.protocol >= 2.0:
 | |
| 			return
 | |
| 
 | |
| 	flags = read_register(device, REGISTERS.notifications)
 | |
| 	if flags is not None:
 | |
| 		assert len(flags) == 3
 | |
| 		return _bytes2int(flags)
 | |
| 
 | |
| 
 | |
| def set_notification_flags(device, *flag_bits):
 | |
| 	assert device
 | |
| 
 | |
| 	# Avoid a call if the device is not online,
 | |
| 	# or the device does not support registers.
 | |
| 	if device.kind is not None:
 | |
| 		# peripherals with protocol >= 2.0 don't support registers
 | |
| 		if device.protocol and device.protocol >= 2.0:
 | |
| 			return
 | |
| 
 | |
| 	flag_bits = sum(int(b) for b in flag_bits)
 | |
| 	assert flag_bits & 0x00FFFFFF == flag_bits
 | |
| 	result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3))
 | |
| 	return result is not None
 |