proto: add support for requesting devices

Add support for a client to request the creation of a new device
from the EIS implementation. This is necessary in situations where the
devices created by the EIS implementation are not (or no longer)
suitable for the client to function correctly.

The primary use-case of this is the upcoming tablet tool support where a
client may need to create multiple tablet tools in response to a new
physical tool brought into proximity locally.

Other use-cases include a client closing a device but requiring that
device (or one with similar capabilities) later.

The implementation in libei is straightforward
- on the client side we have a new function to request the new device:
  ei_seat_request_device_with_capabilities()
- on the server side we have a new event EIS_EVENT_SEAT_DEVICE_REQUESTED
  that can make use of the existing eis_event_seat_has_capability() API

Part-of: <https://gitlab.freedesktop.org/libinput/libei/-/merge_requests/345>
This commit is contained in:
Peter Hutterer 2025-08-12 13:36:53 +10:00 committed by Marge Bot
parent 08d7d5918f
commit 5f9e181073
13 changed files with 202 additions and 15 deletions

View file

@ -420,7 +420,7 @@
<!-- ei_pingpong events version 1 -->
</interface>
<interface name="ei_seat" version="1">
<interface name="ei_seat" version="2">
<description summary="set of input devices that logically belong together">
An ei_seat represents a set of input devices that logically belong together. In most
cases only one seat is present and all input devices on that seat share the same
@ -470,6 +470,39 @@
<arg name="capabilities" type="uint64" summary="bitmask of the capabilities"/>
</request>
<!-- ei_seat client requests version 2 -->
<request name="request_device" since="2">
<description summary="Request a new device with capabilities">
Request a new device from the EIS implementation with the given capability
mask. If the EIS implementation creates a new device in response to this
request that device is announced via the ei_seat.device event as any other
device.
The capabilities argument is a bitmask of one or more of the
masks representing an interface as provided in the ei_seat.capability event.
See the ei_seat.capability event documentation for examples.
The newly created device(s), if any, may not provide all requested capabilities.
The EIS implementation may create multiple devices to accommodate all requested
capabilities. For example, a client requesting a device with the ei_keyboard and
ei_pointer capability may instead see one device with the ei_keyboard and one
device with the ei_pointer capability being created.
If possible, the EIS implementation should create devices immediately in
response to this request so a client can utilize ei_connection.sync to
help identify those devices. This is a suggestion to help the client and not
a requirement.
Requesting masks that are not supported in the ei_device's interface version
is a client bug and may result in disconnection.
Requesting masks that are not currently bound via the most recent ei_seat.bind
is a client bug and may result in disconnection.
</description>
<arg name="capabilities" type="uint64" summary="bitmask of the capabilities"/>
</request>
<!-- ei_seat events version 1 -->
<event name="destroyed" type="destructor" since="1">

View file

@ -349,3 +349,37 @@ ei_seat_unbind_capabilities(struct ei_seat *seat, ...)
ei_seat_send_bind(seat, seat->capabilities.bound);
}
_public_ void
ei_seat_request_device_with_capabilities(struct ei_seat *seat, ...)
{
switch (seat->state) {
case EI_SEAT_STATE_DONE:
break;
case EI_SEAT_STATE_NEW:
case EI_SEAT_STATE_REMOVED:
return;
}
uint64_t mask = 0;
enum ei_device_capability cap;
va_list args;
va_start(args, seat);
while ((cap = va_arg(args, enum ei_device_capability)) > 0) {
mask_add(mask, ei_seat_cap_mask(seat, cap));
}
va_end(args);
if (mask == 0)
return;
/* Check if requested capabilities are a subset of bound capabilities */
if (!mask_all(seat->capabilities.bound, mask)) {
struct ei *ei = ei_seat_get_context(seat);
log_bug_client(ei, "Requested capabilities are not a subset of the bound capabilities");
return;
}
ei_seat_request_request_device(seat, mask);
}

View file

@ -126,7 +126,7 @@ ei_create_context(bool is_sender, void *user_data)
.ei_handshake = VERSION_V(1),
.ei_callback = VERSION_V(1),
.ei_pingpong = VERSION_V(1),
.ei_seat = VERSION_V(1),
.ei_seat = VERSION_V(2),
.ei_device = VERSION_V(3),
.ei_pointer = VERSION_V(1),
.ei_pointer_absolute = VERSION_V(1),

View file

@ -1089,6 +1089,31 @@ void
ei_seat_unbind_capabilities(struct ei_seat *seat, ...)
__attribute__((sentinel));
/**
* @ingroup libei-seat
*
* Request a new device with (a subset of) the given capabilities from the EIS
* implementation. If the EIS implementation creates a device in response
* to this request the device will arrive via an event of type @ref
* EI_EVENT_DEVICE_ADDED.
*
* The device's capabilities may not match the requested capabilities. For
* example requesting a pointer + keyboard device may result in the creation
* of a pointer-only device or it may result in the creation of a pointer +
* keyboard + touch device. It is up to the caller to handle capability
* mismatches.
*
* This function should be used by a caller when the current set of devices
* is insufficient for the functionality the client requires. For example
* a client that has previously called ei_device_close() on a device may
* need a device again with similar capabilities.
*
* The capabilities must be a subset of the capabilities requested in
* ei_seat_bind_capabilities().
*/
void
ei_seat_request_device_with_capabilities(struct ei_seat *seat, ...)
__attribute__((sentinel));
/**
* @ingroup libei-seat

View file

@ -521,7 +521,7 @@ eis_client_new(struct eis *eis, int fd)
.ei_handshake = VERSION_V(1),
.ei_callback = VERSION_V(1),
.ei_pingpong = VERSION_V(1),
.ei_seat = VERSION_V(1),
.ei_seat = VERSION_V(2),
.ei_device = flag_is_set(eis->flags, EIS_FLAG_DEVICE_READY) ? VERSION_V(3) : VERSION_V(2),
.ei_pointer = VERSION_V(1),
.ei_pointer_absolute = VERSION_V(1),

View file

@ -41,6 +41,7 @@ eis_event_destroy(struct eis_event *event)
case EIS_EVENT_CLIENT_CONNECT:
case EIS_EVENT_CLIENT_DISCONNECT:
case EIS_EVENT_SEAT_BIND:
case EIS_EVENT_SEAT_DEVICE_REQUESTED:
case EIS_EVENT_DEVICE_CLOSED:
case EIS_EVENT_DEVICE_START_EMULATING:
case EIS_EVENT_DEVICE_STOP_EMULATING:
@ -208,7 +209,10 @@ eis_event_pong_get_ping(struct eis_event *event)
_public_ bool
eis_event_seat_has_capability(struct eis_event *event, enum eis_device_capability cap)
{
require_event_type(event, false, EIS_EVENT_SEAT_BIND);
require_event_type(event,
false,
EIS_EVENT_SEAT_BIND,
EIS_EVENT_SEAT_DEVICE_REQUESTED);
switch (cap) {
case EIS_DEVICE_CAP_POINTER:

View file

@ -103,6 +103,9 @@ eis_queue_sync_event(struct eis_client *client, object_id_t newid, uint32_t vers
void
eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities);
void
eis_queue_seat_device_requested_event(struct eis_seat *seat, uint32_t capabilities);
void
eis_queue_device_closed_event(struct eis_device *device);

View file

@ -86,15 +86,11 @@ client_msg_release(struct eis_seat *seat)
return NULL;
}
static struct brei_result *
client_msg_bind(struct eis_seat *seat, uint64_t caps)
static inline uint32_t
convert_capabilities(uint64_t caps)
{
uint32_t capabilities = 0;
if (caps & ~seat->capabilities.proto_mask)
return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_VALUE,
"Invalid capabilities %#" PRIx64, caps);
/* Convert from protocol capabilities to our C API capabilities */
if (caps & bit(EIS_POINTER_INTERFACE_INDEX))
capabilities |= EIS_DEVICE_CAP_POINTER;
@ -109,14 +105,41 @@ client_msg_bind(struct eis_seat *seat, uint64_t caps)
if (caps & bit(EIS_SCROLL_INTERFACE_INDEX))
capabilities |= EIS_DEVICE_CAP_SCROLL;
return capabilities;
}
static struct brei_result *
client_msg_bind(struct eis_seat *seat, uint64_t caps)
{
if (caps & ~seat->capabilities.proto_mask)
return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_VALUE,
"Invalid capabilities %#" PRIx64, caps);
uint32_t capabilities = convert_capabilities(caps);
eis_seat_bind(seat, capabilities);
return NULL;
}
static struct brei_result *
client_msg_request_device(struct eis_seat *seat, uint64_t caps)
{
if (caps == 0 || caps & ~seat->capabilities.proto_mask)
return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_VALUE,
"Invalid capabilities %#" PRIx64, caps);
uint32_t capabilities = convert_capabilities(caps);
eis_queue_seat_device_requested_event(seat, capabilities);
return NULL;
}
static const struct eis_seat_interface interface = {
.release = client_msg_release,
.bind = client_msg_bind,
.request_device = client_msg_request_device,
};
const struct eis_seat_interface *

View file

@ -137,6 +137,7 @@ eis_event_type_to_string(enum eis_event_type type)
CASE_RETURN_STRING(EIS_EVENT_CLIENT_CONNECT);
CASE_RETURN_STRING(EIS_EVENT_CLIENT_DISCONNECT);
CASE_RETURN_STRING(EIS_EVENT_SEAT_BIND);
CASE_RETURN_STRING(EIS_EVENT_SEAT_DEVICE_REQUESTED);
CASE_RETURN_STRING(EIS_EVENT_DEVICE_CLOSED);
CASE_RETURN_STRING(EIS_EVENT_DEVICE_READY);
CASE_RETURN_STRING(EIS_EVENT_PONG);
@ -268,6 +269,16 @@ eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities)
eis_queue_event(e);
}
void
eis_queue_seat_device_requested_event(struct eis_seat *seat,
uint32_t capabilities)
{
struct eis_event *e = eis_event_new_for_seat(seat);
e->type = EIS_EVENT_SEAT_DEVICE_REQUESTED;
e->bind.capabilities = capabilities;
eis_queue_event(e);
}
void
eis_queue_device_closed_event(struct eis_device *device)
{

View file

@ -286,6 +286,14 @@ enum eis_event_type {
*/
EIS_EVENT_DEVICE_READY,
/**
* The client requested a device with the given capabilities
* on this seat.
*
* @since 1.6
*/
EIS_EVENT_SEAT_DEVICE_REQUESTED,
/**
* Returned in response to eis_ping().
*/
@ -779,11 +787,12 @@ struct eis_ping *
eis_event_pong_get_ping(struct eis_event *event);
/**
* For an event of type @ref EIS_EVENT_SEAT_BIND, return the capabilities
* requested by the client.
* For an event of type @ref EIS_EVENT_SEAT_BIND or @ref
* EIS_EVENT_SEAT_DEVICE_REQUESTED, return the capabilities requested by the
* client.
*
* This is the set of *all* capabilities bound by the client as of this event,
* not just the changed ones.
* For events of type @ref EIS_EVENT_SEAT_BIND this is the set of *all*
* capabilities bound by the client as of this event, not just the changed ones.
*/
bool
eis_event_seat_has_capability(struct eis_event *event, enum eis_device_capability cap);

View file

@ -1117,6 +1117,8 @@ _peck_dispatch_eis(struct peck *peck, int lineno)
else
process_event = tristate_no;
break;
case EIS_EVENT_SEAT_DEVICE_REQUESTED:
break;
case EIS_EVENT_DEVICE_CLOSED:
if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_CLOSE_DEVICE))
process_event = tristate_yes;
@ -1686,6 +1688,7 @@ peck_eis_event_type_name(enum eis_event_type type)
CASE_STRING(CLIENT_CONNECT);
CASE_STRING(CLIENT_DISCONNECT);
CASE_STRING(SEAT_BIND);
CASE_STRING(SEAT_DEVICE_REQUESTED);
CASE_STRING(DEVICE_CLOSED);
CASE_STRING(PONG);
CASE_STRING(SYNC);

View file

@ -173,3 +173,43 @@ MUNIT_TEST(test_ei_seat_bind_unbind_immediately)
return MUNIT_OK;
}
MUNIT_TEST(test_ei_seat_request_device)
{
_unref_(peck) *peck = peck_new();
peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL);
peck_dispatch_until_stable(peck);
with_client(peck) {
struct ei_seat *seat = peck_ei_get_default_seat(peck);
ei_seat_request_device_with_capabilities(seat,
EI_DEVICE_CAP_POINTER,
EI_DEVICE_CAP_KEYBOARD,
NULL);
ei_seat_request_device_with_capabilities(seat,
EI_DEVICE_CAP_TOUCH,
EI_DEVICE_CAP_POINTER_ABSOLUTE,
NULL);
}
peck_dispatch_until_stable(peck);
with_server(peck) {
_unref_(eis_event) *e1 = peck_eis_next_event(eis, EIS_EVENT_SEAT_DEVICE_REQUESTED);
_unref_(eis_event) *e2 = peck_eis_next_event(eis, EIS_EVENT_SEAT_DEVICE_REQUESTED);
munit_assert(eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_POINTER));
munit_assert(eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_KEYBOARD));
munit_assert(!eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_TOUCH));
munit_assert(!eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_POINTER_ABSOLUTE));
munit_assert(!eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_POINTER));
munit_assert(!eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_KEYBOARD));
munit_assert(eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_TOUCH));
munit_assert(eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_POINTER_ABSOLUTE));
}
return MUNIT_OK;
}

View file

@ -590,7 +590,9 @@ class TestEiProtocol:
assert ei.seats
for seat in ei.seats:
assert seat.version == 1 # we have 100, but the server only has 1
assert seat.version == VERSION_V(
2
) # we have 100, but the server only has 1
for call in seat.calllog:
if call.name == "Capability":
assert call.args["mask"].bit_count() == 1