Ejemplo n.º 1
0
def run(receivers, args, find_receiver, _ignore):
	assert receivers

	if args.receiver:
		receiver_name = args.receiver.lower()
		receiver = find_receiver(receiver_name)
		if not receiver:
			raise Exception("no receiver found matching '%s'" % receiver_name)
	else:
		receiver = receivers[0]

	assert receiver
	receiver.status = _status.ReceiverStatus(receiver, lambda *args, **kwargs: None)

	# check if it's necessary to set the notification flags
	old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0
	if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
		_hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless)

	# get all current devices
	known_devices = [dev.number for dev in receiver]

	class _HandleWithNotificationHook(int):
		def notifications_hook(self, n):
			assert n
			if n.devnumber == 0xFF:
				_notifications.process(receiver, n)
			elif n.sub_id == 0x41 and n.address == 0x04:
				if n.devnumber not in known_devices:
					receiver.status.new_device = receiver[n.devnumber]

	timeout = 20  # seconds
	receiver.handle = _HandleWithNotificationHook(receiver.handle)

	receiver.set_lock(False, timeout=timeout)
	print ('Pairing: turn your new device on (timing out in', timeout, 'seconds).')

	# the lock-open notification may come slightly later, wait for it a bit
	pairing_start = _timestamp()
	patience = 5  # seconds

	while receiver.status.lock_open or _timestamp() - pairing_start < patience:
		n = _base.read(receiver.handle)
		if n:
			n = _base.make_notification(*n)
			if n:
				receiver.handle.notifications_hook(n)

	if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
		# only clear the flags if they weren't set before, otherwise a
		# concurrently running Solaar app might stop working properly
		_hidpp10.set_notification_flags(receiver, old_notification_flags)

	if receiver.status.new_device:
		dev = receiver.status.new_device
		print ('Paired device %d: %s (%s) [%s:%s]' % (dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
	else:
		error = receiver.status.get(_status.KEYS.ERROR) or 'no device detected?'
		raise Exception("pairing failed: %s" % error)
Ejemplo n.º 2
0
Archivo: base.py Proyecto: Nek-/Solaar
def ping(handle, devnumber):
	"""Check if a device is connected to the UR.

	:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
	"""
	if _log.isEnabledFor(_DEBUG):
		_log.debug("(%s) pinging device %d", handle, devnumber)

	# import inspect as _inspect
	# print ('\n  '.join(str(s) for s in _inspect.stack()))

	# randomize the SoftwareId and mark byte to be able to identify the ping
	# reply, and set most significant (0x8) bit in SoftwareId so that the reply
	# is always distinguishable from notifications
	request_id = 0x0018 | _random_bits(3)
	request_data = _pack(b'!HBBB', request_id, 0, 0, _random_bits(8))

	ihandle = int(handle)
	notifications_hook = getattr(handle, 'notifications_hook', None)
	_skip_incoming(handle, ihandle, notifications_hook)
	write(ihandle, devnumber, request_data)

	while True:
		now = _timestamp()
		reply = _read(handle, _PING_TIMEOUT)
		delta = _timestamp() - now

		if reply:
			report_id, reply_devnumber, reply_data = reply
			if reply_devnumber == devnumber:
				if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
					# HID++ 2.0+ device, currently connected
					return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0

				if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
					assert reply_data[-1:] == b'\x00'
					error = ord(reply_data[3:4])

					if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
						return 1.0

					if error == _hidpp10.ERROR.resource_error: # device unreachable
						# raise DeviceUnreachable(number=devnumber, request=request_id)
						break

					if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
						_log.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
						raise NoSuchDevice(number=devnumber, request=request_id)

			if notifications_hook:
				n = make_notification(reply_devnumber, reply_data)
				if n:
					notifications_hook(n)

		if delta >= _PING_TIMEOUT:
			_log.warn("(%s) timeout on device %d ping", handle, devnumber)
Ejemplo n.º 3
0
    def changed(self,
                active=None,
                alert=ALERT.NONE,
                reason=None,
                timestamp=None):
        assert self._changed_callback
        d = self._device
        # assert d  # may be invalid when processing the 'unpaired' notification
        timestamp = timestamp or _timestamp()

        if active is not None:
            d.online = active
            was_active, self._active = self._active, active
            if active:
                if not was_active:
                    # Make sure to set notification flags on the device, they
                    # get cleared when the device is turned off (but not when the device
                    # goes idle, and we can't tell the difference right now).
                    if d.protocol < 2.0:
                        self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications(
                        )

                    # If we've been inactive for a long time, forget anything
                    # about the battery. (This is probably unnecessary.)
                    if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
                        self[KEYS.BATTERY_LEVEL] = None
                        self[KEYS.BATTERY_STATUS] = None
                        self[KEYS.BATTERY_CHARGING] = None
                        self[KEYS.BATTERY_VOLTAGE] = None

                    # Devices lose configuration when they are turned off,
                    # make sure they're up-to-date.
                    if _log.isEnabledFor(_DEBUG):
                        _log.debug('%s pushing device settings %s', d,
                                   d.settings)
                    for s in d.settings:
                        s.apply()

                    # battery information may have changed so try to read it now
                    self.read_battery(timestamp)
            else:
                if was_active:
                    battery = self.get(KEYS.BATTERY_LEVEL)
                    self.clear()
                    # If we had a known battery level before, assume it's not going
                    # to change much while the device is offline.
                    if battery is not None:
                        self[KEYS.BATTERY_LEVEL] = battery

        if self.updated == 0 and active is True:
            # if the device is active on the very first status notification,
            # (meaning just when the program started or a new receiver was just
            # detected), pop-up a notification about it
            alert |= ALERT.NOTIFICATION
        self.updated = timestamp

        # if _log.isEnabledFor(_DEBUG):
        #     _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
        self._changed_callback(d, alert, reason)
Ejemplo n.º 4
0
 def __localtime_offset_decreased_jtm(self):
     # workaround for strftime GMT offset dependecy
     njtm = self.__jtm.copy()
     if self.tzinfo is None:
         njtm.tm_gmtoff = 0
         njtm.tm_sec -= jlocaltime(int(_timestamp())).tm_gmtoff
         njtm.tm_zone = ''
         normalize_jtm(njtm)
     return njtm
Ejemplo n.º 5
0
 def __localtime_offset_decreased_jtm(self):
     # workaround for strftime GMT offset dependecy
     njtm = self.__jtm.copy()
     if self.tzinfo is None:
         njtm.tm_gmtoff = 0
         njtm.tm_sec -= jlocaltime(int(_timestamp())).tm_gmtoff
         njtm.tm_zone = ''
         normalize_jtm(njtm)
     return njtm
Ejemplo n.º 6
0
def find_paired_node(receiver_path, index, timeout):
    """Find the node of a device paired with a receiver"""
    context = _Context()
    receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS')

    if not receiver_phys:
        return None

    phys = f'{receiver_phys}:{index}'
    timeout += _timestamp()
    delta = _timestamp()
    while delta < timeout:
        for dev in context.list_devices(subsystem='hidraw'):
            dev_phys = dev.find_parent('hid').get('HID_PHYS')
            if dev_phys and dev_phys == phys:
                return dev.device_node
        delta = _timestamp()

    return None
Ejemplo n.º 7
0
	def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None):
		assert self._changed_callback
		d = self._device
		# assert d  # may be invalid when processing the 'unpaired' notification
		timestamp = timestamp or _timestamp()

		if active is not None:
			d.online = active
			was_active, self._active = self._active, active
			if active:
				if not was_active:
					# Make sure to set notification flags on the device, they
					# get cleared when the device is turned off (but not when the device
					# goes idle, and we can't tell the difference right now).
					if d.protocol < 2.0:
						self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications()

					# If we've been inactive for a long time, forget anything
					# about the battery.
					if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
						self[KEYS.BATTERY_LEVEL] = None
						self[KEYS.BATTERY_STATUS] = None
						self[KEYS.BATTERY_CHARGING] = None

					# Devices lose configuration when they are turned off,
					# make sure they're up-to-date.
					# _log.debug("%s settings %s", d, d.settings)
					for s in d.settings:
						s.apply()

					if self.get(KEYS.BATTERY_LEVEL) is None:
						self.read_battery(timestamp)
			else:
				if was_active:
					battery = self.get(KEYS.BATTERY_LEVEL)
					self.clear()
					# If we had a known battery level before, assume it's not going
					# to change much while the device is offline.
					if battery is not None:
						self[KEYS.BATTERY_LEVEL] = battery

		if self.updated == 0 and active == True:
			# if the device is active on the very first status notification,
			# (meaning just when the program started or a new receiver was just
			# detected), pop-up a notification about it
			alert |= ALERT.NOTIFICATION
		self.updated = timestamp

		# if _log.isEnabledFor(_DEBUG):
		# 	_log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
		self._changed_callback(d, alert, reason)
Ejemplo n.º 8
0
	def run(self):
		self._active = True

		ihandle = int(self.receiver.handle)
		_log.info("started with %s (%d)", self.receiver, ihandle)

		self.has_started()

		last_tick = 0
		idle_reads = 0

		while self._active:
			if self._queued_notifications.empty():
				try:
					# _log.debug("read next notification")
					n = _base.read(ihandle, _EVENT_READ_TIMEOUT)
				except _base.NoReceiver:
					_log.warning("receiver disconnected")
					self.receiver.close()
					break

				if n:
					n = _base.make_notification(*n)
			else:
				# deliver any queued notifications
				n = self._queued_notifications.get()

			if n:
				# if _log.isEnabledFor(_DEBUG):
				# 	_log.debug("%s: processing %s", self.receiver, n)
				try:
					self._notifications_callback(n)
				except:
					_log.exception("processing %s", n)

			elif self.tick_period:
				idle_reads += 1
				if idle_reads % _IDLE_READS == 0:
					idle_reads = 0
					now = _timestamp()
					if now - last_tick >= self.tick_period:
						last_tick = now
						self.tick(now)

		del self._queued_notifications
		self.has_stopped()
Ejemplo n.º 9
0
    def strftime(self, format, _decrease_gmtoff_from_secs=False):
        """Return a string representing the date and time, controlled by an
        explicit format string.

        .. Note ::
            To show correct value for some formatting specials, e.g.'%s',
            libjalali's :func:`jstrftime` needs timezone informations filled
            in :attr:`.jtm.tm_gmtoff` (`issue 4`_) which we could not depend
            on here, since naive datetime objects have zero knowledge about
            it.  Storing these timezone information in a naive datetime
            object, make datetime implementation heavily depended on
            :func:`jmktime` which have several issues itself.  Additionally it
            makes :class:`~pyjalali.datetime.datetime` less like
            :class:`python:datetime.datetime` since it should store more
            information and change method signatures.

            For those directives, a possible workaround is to change
            :attr:`.jtm` by detecting local zone and putting its effect in
            other field values, but this makes other directives (hour, minute,
            etc) incorrect.  You can use this workaround by passing True as
            second argument.

            .. _issue 4: https://github.com/ashkang/jcal/issues/4
        """
        self.__date._compute_yday_wday_if_necessary()
        if self.tzinfo is not None:
            njtm = self.__jtm.copy()
            njtm.tm_gmtoff = int(self.utcoffset().total_seconds())
            njtm.tm_zone = self.tzname()
            if _decrease_gmtoff_from_secs:
                njtm.tm_sec += (njtm.tm_gmtoff -
                                jlocaltime(int(_timestamp())).tm_gmtoff)
            return jstrftime(format, njtm)
        if _decrease_gmtoff_from_secs:
            return jstrftime(format, self.__localtime_offset_decreased_jtm())
        return jstrftime(format, self.jtm)
Ejemplo n.º 10
0
    def strftime(self, format, _decrease_gmtoff_from_secs=False):
        """Return a string representing the date and time, controlled by an
        explicit format string.

        .. Note ::
            To show correct value for some formatting specials, e.g.'%s',
            libjalali's :func:`jstrftime` needs timezone informations filled
            in :attr:`.jtm.tm_gmtoff` (`issue 4`_) which we could not depend
            on here, since naive datetime objects have zero knowledge about
            it.  Storing these timezone information in a naive datetime
            object, make datetime implementation heavily depended on
            :func:`jmktime` which have several issues itself.  Additionally it
            makes :class:`~pyjalali.datetime.datetime` less like
            :class:`python:datetime.datetime` since it should store more
            information and change method signatures.

            For those directives, a possible workaround is to change
            :attr:`.jtm` by detecting local zone and putting its effect in
            other field values, but this makes other directives (hour, minute,
            etc) incorrect.  You can use this workaround by passing True as
            second argument.

            .. _issue 4: https://github.com/ashkang/jcal/issues/4
        """
        self.__date._compute_yday_wday_if_necessary()
        if self.tzinfo is not None:
            njtm = self.__jtm.copy()
            njtm.tm_gmtoff = int(self.utcoffset().total_seconds())
            njtm.tm_zone = self.tzname()
            if _decrease_gmtoff_from_secs:
                njtm.tm_sec += (njtm.tm_gmtoff -
                                jlocaltime(int(_timestamp())).tm_gmtoff)
            return jstrftime(format, njtm)
        if _decrease_gmtoff_from_secs:
            return jstrftime(format, self.__localtime_offset_decreased_jtm())
        return jstrftime(format, self.jtm)
Ejemplo n.º 11
0
    def _scroll(ind, _ignore, direction):
        if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
            # ignore all other directions
            return

        if len(_devices_info) < 4:
            # don't bother with scrolling when there's only one receiver
            # with only one device (3 = [receiver, device, separator])
            return

        # scroll events come way too fast (at least 5-6 at once)
        # so take a little break between them
        global _last_scroll
        now = _timestamp()
        if now - _last_scroll < 0.33:  # seconds
            return
        _last_scroll = now

        # if _log.isEnabledFor(_DEBUG):
        # 	_log.debug("scroll direction %s", direction)

        global _picked_device
        candidate = None

        if _picked_device is None:
            for info in _devices_info:
                # pick first peripheral found
                if info[1] is not None:
                    candidate = info
                    break
        else:
            found = False
            for info in _devices_info:
                if not info[1]:
                    # only conside peripherals
                    continue
                # compare peripherals
                if info[0:2] == _picked_device[0:2]:
                    if direction == ScrollDirection.UP and candidate:
                        # select previous device
                        break
                    found = True
                else:
                    if found:
                        candidate = info
                        if direction == ScrollDirection.DOWN:
                            break
                        # if direction is up, but no candidate found before _picked,
                        # let it run through all candidates, will get stuck with the last one
                    else:
                        if direction == ScrollDirection.DOWN:
                            # only use the first one, in case no candidates are after _picked
                            if candidate is None:
                                candidate = info
                        else:
                            candidate = info

            # if the last _picked_device is gone, clear it
            # the candidate will be either the first or last one remaining,
            # depending on the scroll direction
            if not found:
                _picked_device = None

        _picked_device = candidate or _picked_device
        if _log.isEnabledFor(_DEBUG):
            _log.debug("scroll: picked %s", _picked_device)
        _update_tray_icon()
Ejemplo n.º 12
0
def request(handle, devnumber, request_id, *params, no_reply=False, return_error=False, long_message=False):
    """Makes a feature call to a device and waits for a matching reply.
    :param handle: an open UR handle.
    :param devnumber: attached device number.
    :param request_id: a 16-bit integer.
    :param params: parameters for the feature call, 3 to 16 bytes.
    :returns: the reply data, or ``None`` if some error occurred. or no reply expected
    """

    # import inspect as _inspect
    # print ('\n  '.join(str(s) for s in _inspect.stack()))

    assert isinstance(request_id, int)
    if devnumber != 0xFF and request_id < 0x8000:
        # For HID++ 2.0 feature requests, randomize the SoftwareId to make it
        # easier to recognize the reply for this request. also, always set the
        # most significant bit (8) in SoftwareId, to make notifications easier
        # to distinguish from request replies.
        # This only applies to peripheral requests, ofc.
        request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)

    timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
    # be extra patient on long register read
    if request_id & 0xFF00 == 0x8300:
        timeout *= 2

    if params:
        params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
    else:
        params = b''
    # if _log.isEnabledFor(_DEBUG):
    #     _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
    request_data = _pack('!H', request_id) + params

    ihandle = int(handle)
    notifications_hook = getattr(handle, 'notifications_hook', None)
    _skip_incoming(handle, ihandle, notifications_hook)
    write(ihandle, devnumber, request_data, long_message)

    if no_reply:
        return None

    # we consider timeout from this point
    request_started = _timestamp()
    delta = 0

    while delta < timeout:
        reply = _read(handle, timeout)

        if reply:
            report_id, reply_devnumber, reply_data = reply
            if reply_devnumber == devnumber:
                if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
                    error = ord(reply_data[3:4])

                    # if error == _hidpp10.ERROR.resource_error: # device unreachable
                    #     _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
                    #     raise DeviceUnreachable(number=devnumber, request=request_id)

                    # if error == _hidpp10.ERROR.unknown_device: # unknown device
                    #     _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
                    #     raise NoSuchDevice(number=devnumber, request=request_id)

                    if _log.isEnabledFor(_DEBUG):
                        _log.debug(
                            '(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error,
                            _hidpp10.ERROR[error]
                        )
                    return _hidpp10.ERROR[error] if return_error else None
                if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
                    # a HID++ 2.0 feature call returned with an error
                    error = ord(reply_data[3:4])
                    _log.error(
                        '(%s) device %d error on feature request {%04X}: %d = %s', handle, devnumber, request_id, error,
                        _hidpp20.ERROR[error]
                    )
                    raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)

                if reply_data[:2] == request_data[:2]:
                    if request_id & 0xFE00 == 0x8200:
                        # long registry r/w should return a long reply
                        assert report_id == 0x11
                    elif request_id & 0xFE00 == 0x8000:
                        # short registry r/w should return a short reply
                        assert report_id == 0x10

                    if devnumber == 0xFF:
                        if request_id == 0x83B5 or request_id == 0x81F1:
                            # these replies have to match the first parameter as well
                            if reply_data[2:3] == params[:1]:
                                return reply_data[2:]
                            else:
                                # hm, not matching my request, and certainly not a notification
                                continue
                        else:
                            return reply_data[2:]
                    else:
                        return reply_data[2:]
            else:
                # a reply was received, but did not match our request in any way
                # reset the timeout starting point
                request_started = _timestamp()

            if notifications_hook:
                n = make_notification(reply_devnumber, reply_data)
                if n:
                    notifications_hook(n)
                # elif _log.isEnabledFor(_DEBUG):
                #     _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
            # elif _log.isEnabledFor(_DEBUG):
            #     _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))

        delta = _timestamp() - request_started
        # if _log.isEnabledFor(_DEBUG):
        #     _log.debug("(%s) still waiting for reply, delta %f", handle, delta)

    _log.warn(
        'timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]', delta, timeout, devnumber, request_id,
        _strhex(params)
    )
Ejemplo n.º 13
0
def ping(handle, devnumber, long_message=False):
    """Check if a device is connected to the receiver.

    :returns: The HID protocol supported by the device, as a floating point number, if the device is active.
    """
    if _log.isEnabledFor(_DEBUG):
        _log.debug('(%s) pinging device %d', handle, devnumber)

    # import inspect as _inspect
    # print ('\n  '.join(str(s) for s in _inspect.stack()))

    assert devnumber != 0xFF
    assert devnumber >= 0x00
    assert devnumber < 0x0F

    # randomize the SoftwareId and mark byte to be able to identify the ping
    # reply, and set most significant (0x8) bit in SoftwareId so that the reply
    # is always distinguishable from notifications
    request_id = 0x0018 | _random_bits(3)
    request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))

    ihandle = int(handle)
    notifications_hook = getattr(handle, 'notifications_hook', None)
    _skip_incoming(handle, ihandle, notifications_hook)
    write(ihandle, devnumber, request_data, long_message)

    # we consider timeout from this point
    request_started = _timestamp()
    delta = 0

    while delta < _PING_TIMEOUT:
        reply = _read(handle, _PING_TIMEOUT)

        if reply:
            report_id, reply_devnumber, reply_data = reply
            if reply_devnumber == devnumber:
                if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
                    # HID++ 2.0+ device, currently connected
                    return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0

                if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
                    assert reply_data[-1:] == b'\x00'
                    error = ord(reply_data[3:4])

                    if error == _hidpp10.ERROR.invalid_SubID__command:  # a valid reply from a HID++ 1.0 device
                        return 1.0

                    if error == _hidpp10.ERROR.resource_error:  # device unreachable
                        return

                    if error == _hidpp10.ERROR.unknown_device:  # no paired device with that number
                        _log.error('(%s) device %d error on ping request: unknown device', handle, devnumber)
                        raise NoSuchDevice(number=devnumber, request=request_id)

            if notifications_hook:
                n = make_notification(reply_devnumber, reply_data)
                if n:
                    notifications_hook(n)
                # elif _log.isEnabledFor(_DEBUG):
                #     _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))

        delta = _timestamp() - request_started

    _log.warn('(%s) timeout (%0.2f/%0.2f) on device %d ping', handle, delta, _PING_TIMEOUT, devnumber)
Ejemplo n.º 14
0
def run(receivers, args, find_receiver, _ignore):
    assert receivers

    if args.receiver:
        receiver_name = args.receiver.lower()
        receiver = find_receiver(receivers, receiver_name)
        if not receiver:
            raise Exception("no receiver found matching '%s'" % receiver_name)
    else:
        receiver = receivers[0]

    assert receiver
    receiver.status = _status.ReceiverStatus(receiver,
                                             lambda *args, **kwargs: None)

    # check if it's necessary to set the notification flags
    old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0
    if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
        _hidpp10.set_notification_flags(
            receiver,
            old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless)

    # get all current devices
    known_devices = [dev.number for dev in receiver]

    class _HandleWithNotificationHook(int):
        def notifications_hook(self, n):
            nonlocal known_devices
            assert n
            if n.devnumber == 0xFF:
                _notifications.process(receiver, n)
            elif n.sub_id == 0x41 and len(
                    n.data) == _base._SHORT_MESSAGE_SIZE - 4:
                kd, known_devices = known_devices, None  # only process one connection notification
                if kd is not None:
                    if n.devnumber not in kd:
                        receiver.status.new_device = receiver.register_new_device(
                            n.devnumber, n)
                    elif receiver.re_pairs:
                        del receiver[
                            n.
                            devnumber]  # get rid of information on device re-paired away
                        receiver.status.new_device = receiver.register_new_device(
                            n.devnumber, n)

    timeout = 30  # seconds
    receiver.handle = _HandleWithNotificationHook(receiver.handle)

    if receiver.receiver_kind == 'bolt':  # Bolt receivers require authentication to pair a device
        receiver.discover(timeout=timeout)
        print(
            'Bolt Pairing: long-press the pairing key or button on your device (timing out in',
            timeout, 'seconds).')
        pairing_start = _timestamp()
        patience = 5  # the discovering notification may come slightly later, so be patient
        while receiver.status.discovering or _timestamp(
        ) - pairing_start < patience:
            if receiver.status.device_address and receiver.status.device_authentication and receiver.status.device_name:
                break
            n = _base.read(receiver.handle)
            n = _base.make_notification(*n) if n else None
            if n:
                receiver.handle.notifications_hook(n)
        address = receiver.status.device_address
        name = receiver.status.device_name
        authentication = receiver.status.device_authentication
        kind = receiver.status.device_kind
        print(f'Bolt Pairing: discovered {name}')
        receiver.pair_device(
            address=address,
            authentication=authentication,
            entropy=20 if kind == _hidpp10.DEVICE_KIND.keyboard else 10)
        pairing_start = _timestamp()
        patience = 5  # the discovering notification may come slightly later, so be patient
        while receiver.status.lock_open or _timestamp(
        ) - pairing_start < patience:
            if receiver.status.device_passkey:
                break
            n = _base.read(receiver.handle)
            n = _base.make_notification(*n) if n else None
            if n:
                receiver.handle.notifications_hook(n)
        if authentication & 0x01:
            print(
                f'Bolt Pairing: type passkey {receiver.status.device_passkey} and then press the enter key'
            )
        else:
            passkey = f'{int(receiver.status.device_passkey):010b}'
            passkey = ', '.join(
                ['right' if bit == '1' else 'left' for bit in passkey])
            print(f'Bolt Pairing: press {passkey}')
            print('and then press left and right buttons simultaneously')
        while receiver.status.lock_open:
            n = _base.read(receiver.handle)
            n = _base.make_notification(*n) if n else None
            if n:
                receiver.handle.notifications_hook(n)

    else:
        receiver.set_lock(False, timeout=timeout)
        print('Pairing: turn your new device on (timing out in', timeout,
              'seconds).')
        pairing_start = _timestamp()
        patience = 5  # the lock-open notification may come slightly later, wait for it a bit
        while receiver.status.lock_open or _timestamp(
        ) - pairing_start < patience:
            n = _base.read(receiver.handle)
            if n:
                n = _base.make_notification(*n)
                if n:
                    receiver.handle.notifications_hook(n)

    if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
        # only clear the flags if they weren't set before, otherwise a
        # concurrently running Solaar app might stop working properly
        _hidpp10.set_notification_flags(receiver, old_notification_flags)

    if receiver.status.new_device:
        dev = receiver.status.new_device
        print('Paired device %d: %s (%s) [%s:%s]' %
              (dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
    else:
        error = receiver.status.get(_status.KEYS.ERROR)
        if error:
            raise Exception('pairing failed: %s' % error)
        else:
            print('Paired device')  # this is better than an error
Ejemplo n.º 15
0
Archivo: base.py Proyecto: 3v1n0/Solaar
def request(handle, devnumber, request_id, *params):
	"""Makes a feature call to a device and waits for a matching reply.

	This function will wait for a matching reply indefinitely.

	:param handle: an open UR handle.
	:param devnumber: attached device number.
	:param request_id: a 16-bit integer.
	:param params: parameters for the feature call, 3 to 16 bytes.
	:returns: the reply data, or ``None`` if some error occured.
	"""

	# import inspect as _inspect
	# print ('\n  '.join(str(s) for s in _inspect.stack()))

	assert isinstance(request_id, int)
	if devnumber != 0xFF and request_id < 0x8000:
		# For HID++ 2.0 feature requests, randomize the SoftwareId to make it
		# easier to recognize the reply for this request. also, always set the
		# most significant bit (8) in SoftwareId, to make notifications easier
		# to distinguish from request replies.
		# This only applies to peripheral requests, ofc.
		request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)

	timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
	# be extra patient on long register read
	if request_id & 0xFF00 == 0x8300:
		timeout *= 2

	if params:
		params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
	else:
		params = b''
	# if _log.isEnabledFor(_DEBUG):
	# 	_log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
	request_data = _pack('!H', request_id) + params

	ihandle = int(handle)
	notifications_hook = getattr(handle, 'notifications_hook', None)
	_skip_incoming(handle, ihandle, notifications_hook)
	write(ihandle, devnumber, request_data)

	# we consider timeout from this point
	request_started = _timestamp()
	delta = 0

	while delta < timeout:
		reply = _read(handle, timeout)

		if reply:
			report_id, reply_devnumber, reply_data = reply
			if reply_devnumber == devnumber:
				if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
					error = ord(reply_data[3:4])

					# if error == _hidpp10.ERROR.resource_error: # device unreachable
					# 	_log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
					# 	raise DeviceUnreachable(number=devnumber, request=request_id)

					# if error == _hidpp10.ERROR.unknown_device: # unknown device
					# 	_log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
					# 	raise NoSuchDevice(number=devnumber, request=request_id)

					if _log.isEnabledFor(_DEBUG):
						_log.debug("(%s) device 0x%02X error on request {%04X}: %d = %s",
										handle, devnumber, request_id, error, _hidpp10.ERROR[error])
					return

				if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
					# a HID++ 2.0 feature call returned with an error
					error = ord(reply_data[3:4])
					_log.error("(%s) device %d error on feature request {%04X}: %d = %s",
									handle, devnumber, request_id, error, _hidpp20.ERROR[error])
					raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)

				if reply_data[:2] == request_data[:2]:
					if request_id & 0xFE00 == 0x8200:
						# long registry r/w should return a long reply
						assert report_id == 0x11
					elif request_id & 0xFE00 == 0x8000:
						# short registry r/w should return a short reply
						assert report_id == 0x10

					if devnumber == 0xFF:
						if request_id == 0x83B5 or request_id == 0x81F1:
							# these replies have to match the first parameter as well
							if reply_data[2:3] == params[:1]:
								return reply_data[2:]
							else:
								# hm, not mathing my request, and certainly not a notification
								continue
						else:
							return reply_data[2:]
					else:
						return reply_data[2:]
			else:
				# a reply was received, but did not match our request in any way
				# reset the timeout starting point
				request_started = _timestamp()

			if notifications_hook:
				n = make_notification(reply_devnumber, reply_data)
				if n:
					notifications_hook(n)
				# elif _log.isEnabledFor(_DEBUG):
				# 	_log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
			# elif _log.isEnabledFor(_DEBUG):
			# 	_log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))

		delta = _timestamp() - request_started
		# if _log.isEnabledFor(_DEBUG):
		# 	_log.debug("(%s) still waiting for reply, delta %f", handle, delta)

	_log.warn("timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
					delta, timeout, devnumber, request_id, _strhex(params))
Ejemplo n.º 16
0
def _scroll(tray_icon, event, direction=None):
    if direction is None:
        direction = event.direction
        now = event.time / 1000.0
    else:
        now = None

    if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
        # ignore all other directions
        return

    if sum(map(lambda i: i[1] is not None, _devices_info)) < 2:  # don't bother even trying to scroll if less than two devices
        return

    # scroll events come way too fast (at least 5-6 at once)
    # so take a little break between them
    global _last_scroll
    now = now or _timestamp()
    if now - _last_scroll < 0.33:  # seconds
        return
    _last_scroll = now

    # if _log.isEnabledFor(_DEBUG):
    #     _log.debug("scroll direction %s", direction)

    global _picked_device
    candidate = None

    if _picked_device is None:
        for info in _devices_info:
            # pick first peripheral found
            if info[1] is not None:
                candidate = info
                break
    else:
        found = False
        for info in _devices_info:
            if not info[1]:
                # only conside peripherals
                continue
            # compare peripherals
            if info[0:2] == _picked_device[0:2]:
                if direction == ScrollDirection.UP and candidate:
                    # select previous device
                    break
                found = True
            else:
                if found:
                    candidate = info
                    if direction == ScrollDirection.DOWN:
                        break
                    # if direction is up, but no candidate found before _picked,
                    # let it run through all candidates, will get stuck with the last one
                else:
                    if direction == ScrollDirection.DOWN:
                        # only use the first one, in case no candidates are after _picked
                        if candidate is None:
                            candidate = info
                    else:
                        candidate = info

        # if the last _picked_device is gone, clear it
        # the candidate will be either the first or last one remaining,
        # depending on the scroll direction
        if not found:
            _picked_device = None

    _picked_device = candidate or _picked_device
    if _log.isEnabledFor(_DEBUG):
        _log.debug('scroll: picked %s', _picked_device)
    _update_tray_icon()
Ejemplo n.º 17
0
    def changed(self,
                active=None,
                alert=ALERT.NONE,
                reason=None,
                push=False,
                timestamp=None):
        assert self._changed_callback
        d = self._device
        # assert d  # may be invalid when processing the 'unpaired' notification
        timestamp = timestamp or _timestamp()

        _log.info(
            f"tjones: changed: active = {self._active} -> {active} ... reason = [{reason}]"
        )
        if active is not None:
            d.online = active
            was_active, self._active = self._active, active

            if active:
                # If the device went to sleep we may not have been notified. As the device will
                # still be marked as active we need to also check if it's just been woken up to see
                # if we need to re-apply settings.
                if not was_active or reason == _('powered on'):
                    #if not was_active:
                    # Make sure to set notification flags on the device, they
                    # get cleared when the device is turned off (but not when the device
                    # goes idle, and we can't tell the difference right now).
                    if d.protocol < 2.0:
                        self[
                            KEYS.
                            NOTIFICATION_FLAGS] = d.enable_connection_notifications(
                            )

                    # battery information may have changed so try to read it now
                    self.read_battery(timestamp)

                # Push settings for new devices (was_active is None),
                # when devices request software reconfiguration
                # and when devices become active if they don't have wireless device status feature,
                if was_active is None or push or not was_active and (
                        not d.features
                        or _hidpp20.FEATURE.WIRELESS_DEVICE_STATUS
                        not in d.features):
                    if _log.isEnabledFor(_INFO):
                        _log.info('%s pushing device settings %s', d,
                                  d.settings)
                    _settings.apply_all_settings(d)

            else:
                if was_active:
                    battery = self.get(KEYS.BATTERY_LEVEL)
                    self.clear()
                    # If we had a known battery level before, assume it's not going
                    # to change much while the device is offline.
                    if battery is not None:
                        self[KEYS.BATTERY_LEVEL] = battery

        # A device that is not active on the first status notification
        # but becomes active afterwards does not produce a pop-up notification
        # so don't produce one here.  This cuts off pop-ups when Solaar starts,
        # which can be problematic if Solaar is autostarted.
        ## if self.updated == 0 and active is True:
        ## if the device is active on the very first status notification,
        ## (meaning just when the program started or a new receiver was just
        ## detected), pop up a notification about it
        ##    alert |= ALERT.NOTIFICATION

        self.updated = timestamp

        # if _log.isEnabledFor(_DEBUG):
        #     _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
        self._changed_callback(d, alert, reason)
Ejemplo n.º 18
0
def utcnow():
    """See :meth:`.datetime.utcnow`."""
    return datetime_from_ts(_timestamp(), False)
Ejemplo n.º 19
0
def _scroll(tray_icon, event, direction=None):
	if direction is None:
		direction = event.direction
		now = event.time / 1000.0
	else:
		now = None

	if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
		# ignore all other directions
		return

	if len(_devices_info) < 4:
		# don't bother with scrolling when there's only one receiver
		# with only one device (3 = [receiver, device, separator])
		return

	# scroll events come way too fast (at least 5-6 at once)
	# so take a little break between them
	global _last_scroll
	now = now or _timestamp()
	if now - _last_scroll < 0.33:  # seconds
		return
	_last_scroll = now

	# if _log.isEnabledFor(_DEBUG):
	# 	_log.debug("scroll direction %s", direction)

	global _picked_device
	candidate = None

	if _picked_device is None:
		for info in _devices_info:
			# pick first peripheral found
			if info[1] is not None:
				candidate = info
				break
	else:
		found = False
		for info in _devices_info:
			if not info[1]:
				# only conside peripherals
				continue
			# compare peripherals
			if info[0:2] == _picked_device[0:2]:
				if direction == ScrollDirection.UP and candidate:
					# select previous device
					break
				found = True
			else:
				if found:
					candidate = info
					if direction == ScrollDirection.DOWN:
						break
					# if direction is up, but no candidate found before _picked,
					# let it run through all candidates, will get stuck with the last one
				else:
					if direction == ScrollDirection.DOWN:
						# only use the first one, in case no candidates are after _picked
						if candidate is None:
							candidate = info
					else:
						candidate = info

		# if the last _picked_device is gone, clear it
		# the candidate will be either the first or last one remaining,
		# depending on the scroll direction
		if not found:
			_picked_device = None

	_picked_device = candidate or _picked_device
	if _log.isEnabledFor(_DEBUG):
		_log.debug("scroll: picked %s", _picked_device)
	_update_tray_icon()
Ejemplo n.º 20
0
def now(timezone=None):
    """See :meth:`.datetime.now`."""
    return datetime_from_ts(_timestamp(), True, tz=timezone)
Ejemplo n.º 21
0
 def today(self):
     """Return current local date"""
     return date.fromtimestamp(_timestamp())
Ejemplo n.º 22
0
def now(timezone=None):
    """See :meth:`.datetime.now`."""
    return datetime_from_ts(_timestamp(), True, tz=timezone)
Ejemplo n.º 23
0
 def today(self):
     """Return current local date"""
     return date.fromtimestamp(_timestamp())
Ejemplo n.º 24
0
def utcnow():
    """See :meth:`.datetime.utcnow`."""
    return datetime_from_ts(_timestamp(), False)