[PATCH] evdev: Only send the events at synchronization time.
oliver.mcfadden at nokia.com
oliver.mcfadden at nokia.com
Mon Jul 20 00:15:55 PDT 2009
From: Oliver McFadden <oliver.mcfadden at nokia.com>
Instead of just posting the button/key press/release events to the
server as soon as they arrive, add them to an internal queue and post
them once we receive an EV_SYN synchronization event.
The motion events are always sent first, followed by the queued events.
There will be one motion event and possibly many queued button/key
events posted every EV_SYN event.
Note that the size of the event queue (EVDEV_MAXQUEUE) is arbitrary and
you may change it. If we receive more events than the queue can handle,
those events are dropped and a warning message printed.
Tested on my Lenovo T400 using evdev for all input devices; keyboard,
touchpad, and trackpoint.
---
src/evdev.c | 563 +++++++++++++++++++++++++++++++++++++++--------------------
src/evdev.h | 17 ++
2 files changed, 389 insertions(+), 191 deletions(-)
diff --git a/src/evdev.c b/src/evdev.c
index ecca94a..2b1d3d8 100644
--- a/src/evdev.c
+++ b/src/evdev.c
@@ -249,21 +249,12 @@ static int wheel_left_button = 6;
static int wheel_right_button = 7;
static void
-PostButtonClicks(InputInfoPtr pInfo, int button, int count)
-{
- int i;
-
- for (i = 0; i < count; i++) {
- xf86PostButtonEvent(pInfo->dev, 0, button, 1, 0, 0);
- xf86PostButtonEvent(pInfo->dev, 0, button, 0, 0, 0);
- }
-}
-
-static void
PostKbdEvent(InputInfoPtr pInfo, struct input_event *ev, int value)
{
int code = ev->code + MIN_KEYCODE;
static char warned[KEY_CNT];
+ EventQueuePtr event;
+ EvdevPtr pEvdev = pInfo->private;
/* Filter all repeated events from device.
We'll do softrepeat in the server, but only since 1.6 */
@@ -292,7 +283,51 @@ PostKbdEvent(InputInfoPtr pInfo, struct input_event *ev, int value)
return;
}
- xf86PostKeyboardEvent(pInfo->dev, code, value);
+ if (pEvdev->num_queue >= EVDEV_MAXQUEUE)
+ {
+ xf86Msg(X_WARNING,
+ "%s: dropping event due to full queue! (Consider increasing EVDEV_MAXQUEUE)\n",
+ pInfo->name);
+ return;
+ }
+
+ event = &pEvdev->queue[pEvdev->num_queue];
+ event->type = EV_QUEUE_KEY;
+ event->key = code;
+ event->val = value;
+ pEvdev->num_queue++;
+}
+
+static void
+PostButtonEvent(InputInfoPtr pInfo, struct input_event *ev, int value)
+{
+ EventQueuePtr event;
+ EvdevPtr pEvdev = pInfo->private;
+
+ if (pEvdev->num_queue >= EVDEV_MAXQUEUE)
+ {
+ xf86Msg(X_WARNING,
+ "%s: dropping event due to full queue! (Consider increasing EVDEV_MAXQUEUE)\n",
+ pInfo->name);
+ return;
+ }
+
+ event = &pEvdev->queue[pEvdev->num_queue];
+ event->type = EV_QUEUE_BTN;
+ event->key = EvdevUtilButtonEventToButtonNumber(pEvdev, ev->code);
+ event->val = value;
+ pEvdev->num_queue++;
+}
+
+static void
+PostButtonClicks(InputInfoPtr pInfo, struct input_event *ev, int button, int count)
+{
+ int i;
+
+ for (i = 0; i < count; i++) {
+ PostButtonEvent(pInfo, ev, 1);
+ PostButtonEvent(pInfo, ev, 0);
+ }
}
/**
@@ -349,212 +384,358 @@ EvdevReopenTimer(OsTimerPtr timer, CARD32 time, pointer arg)
#define ABS_Y_VALUE 0x2
#define ABS_VALUE 0x4
/**
- * Take one input event and process it accordingly.
+ * Take the valuators and process them accordingly.
*/
static void
-EvdevProcessEvent(InputInfoPtr pInfo, struct input_event *ev)
+EvdevProcessValuators(InputInfoPtr pInfo, int v[MAX_VALUATORS], int *num_v,
+ int *first_v)
+{
+ int tmp;
+ EvdevPtr pEvdev = pInfo->private;
+
+ *num_v = *first_v = 0;
+
+ /* convert to relative motion for touchpads */
+ if (pEvdev->abs && (pEvdev->flags & EVDEV_TOUCHPAD)) {
+ if (pEvdev->tool) { /* meaning, touch is active */
+ if (pEvdev->old_vals[0] != -1)
+ pEvdev->delta[REL_X] = pEvdev->vals[0] - pEvdev->old_vals[0];
+ if (pEvdev->old_vals[1] != -1)
+ pEvdev->delta[REL_Y] = pEvdev->vals[1] - pEvdev->old_vals[1];
+ if (pEvdev->abs & ABS_X_VALUE)
+ pEvdev->old_vals[0] = pEvdev->vals[0];
+ if (pEvdev->abs & ABS_Y_VALUE)
+ pEvdev->old_vals[1] = pEvdev->vals[1];
+ } else {
+ pEvdev->old_vals[0] = pEvdev->old_vals[1] = -1;
+ }
+ pEvdev->abs = 0;
+ pEvdev->rel = 1;
+ }
+
+ if (pEvdev->rel) {
+ int first = REL_CNT, last = 0;
+ int i;
+
+ if (pEvdev->swap_axes) {
+ tmp = pEvdev->delta[REL_X];
+ pEvdev->delta[REL_X] = pEvdev->delta[REL_Y];
+ pEvdev->delta[REL_Y] = tmp;
+ }
+ if (pEvdev->invert_x)
+ pEvdev->delta[REL_X] *= -1;
+ if (pEvdev->invert_y)
+ pEvdev->delta[REL_Y] *= -1;
+
+ for (i = 0; i < REL_CNT; i++)
+ {
+ int map = pEvdev->axis_map[i];
+ if (pEvdev->delta[i] && map != -1)
+ {
+ v[map] = pEvdev->delta[i];
+ if (map < first)
+ first = map;
+ if (map > last)
+ last = map;
+ }
+ }
+
+ *num_v = (last - first + 1);
+ *first_v = first;
+ }
+ /*
+ * Some devices only generate valid abs coords when BTN_DIGI is
+ * pressed. On wacom tablets, this means that the pen is in
+ * proximity of the tablet. After the pen is removed, BTN_DIGI is
+ * released, and a (0, 0) absolute event is generated. Checking
+ * pEvdev->digi here, lets us ignore that event. pEvdev is
+ * initialized to 1 so devices that doesn't use this scheme still
+ * just works.
+ */
+ else if (pEvdev->abs && pEvdev->tool) {
+ memcpy(v, pEvdev->vals, sizeof(int) * pEvdev->num_vals);
+ if (pEvdev->flags & EVDEV_CALIBRATED)
+ {
+ v[0] = xf86ScaleAxis(v[0],
+ pEvdev->absinfo[ABS_X].maximum,
+ pEvdev->absinfo[ABS_X].minimum,
+ pEvdev->calibration.max_x, pEvdev->calibration.min_x);
+ v[1] = xf86ScaleAxis(v[1],
+ pEvdev->absinfo[ABS_Y].maximum,
+ pEvdev->absinfo[ABS_Y].minimum,
+ pEvdev->calibration.max_y, pEvdev->calibration.min_y);
+ }
+
+ if (pEvdev->swap_axes) {
+ int tmp = v[0];
+ v[0] = v[1];
+ v[1] = tmp;
+ }
+
+ if (pEvdev->invert_x)
+ v[0] = (pEvdev->absinfo[ABS_X].maximum - v[0] +
+ pEvdev->absinfo[ABS_X].minimum);
+ if (pEvdev->invert_y)
+ v[1] = (pEvdev->absinfo[ABS_Y].maximum - v[1] +
+ pEvdev->absinfo[ABS_Y].minimum);
+
+ *num_v = pEvdev->num_vals;
+ *first_v = 0;
+ }
+}
+
+/**
+ * Take a button input event and process it accordingly.
+ */
+static void
+EvdevProcessButtonEvent(InputInfoPtr pInfo, struct input_event *ev)
{
- static int delta[REL_CNT];
- static int tmp, value;
- static unsigned int abs, rel;
unsigned int button;
+ int value;
EvdevPtr pEvdev = pInfo->private;
+ button = EvdevUtilButtonEventToButtonNumber(pEvdev, ev->code);
+
/* Get the signed value, earlier kernels had this as unsigned */
value = ev->value;
- switch (ev->type) {
- case EV_REL:
- /* Ignore EV_REL events if we never set up for them. */
- if (!(pEvdev->flags & EVDEV_RELATIVE_EVENTS))
- break;
+ /* Handle drag lock */
+ if (EvdevDragLockFilterEvent(pInfo, button, value))
+ return;
- /* Handle mouse wheel emulation */
- if (EvdevWheelEmuFilterMotion(pInfo, ev))
- break;
+ if (EvdevWheelEmuFilterButton(pInfo, button, value))
+ return;
- rel = 1;
-
- switch (ev->code) {
- case REL_WHEEL:
- if (value > 0)
- PostButtonClicks(pInfo, wheel_up_button, value);
- else if (value < 0)
- PostButtonClicks(pInfo, wheel_down_button, -value);
- break;
-
- case REL_DIAL:
- case REL_HWHEEL:
- if (value > 0)
- PostButtonClicks(pInfo, wheel_right_button, value);
- else if (value < 0)
- PostButtonClicks(pInfo, wheel_left_button, -value);
- break;
-
- /* We don't post wheel events as axis motion. */
- default:
- delta[ev->code] += value;
- break;
- }
+ if (EvdevMBEmuFilterEvent(pInfo, button, value))
+ return;
+
+ if (button)
+ PostButtonEvent(pInfo, ev, value);
+ else
+ PostKbdEvent(pInfo, ev, value);
+}
+
+/**
+ * Take the relative motion input event and process it accordingly.
+ */
+static void
+EvdevProcessRelativeMotionEvent(InputInfoPtr pInfo, struct input_event *ev)
+{
+ static int value;
+ EvdevPtr pEvdev = pInfo->private;
+
+ /* Get the signed value, earlier kernels had this as unsigned */
+ value = ev->value;
+
+ /* Ignore EV_REL events if we never set up for them. */
+ if (!(pEvdev->flags & EVDEV_RELATIVE_EVENTS))
+ return;
+
+ /* Handle mouse wheel emulation */
+ if (EvdevWheelEmuFilterMotion(pInfo, ev))
+ return;
+
+ pEvdev->rel = 1;
+
+ switch (ev->code) {
+ case REL_WHEEL:
+ if (value > 0)
+ PostButtonClicks(pInfo, ev, wheel_up_button, value);
+ else if (value < 0)
+ PostButtonClicks(pInfo, ev, wheel_down_button, -value);
break;
- case EV_ABS:
- /* Ignore EV_ABS events if we never set up for them. */
- if (!(pEvdev->flags & EVDEV_ABSOLUTE_EVENTS))
- break;
+ case REL_DIAL:
+ case REL_HWHEEL:
+ if (value > 0)
+ PostButtonClicks(pInfo, ev, wheel_right_button, value);
+ else if (value < 0)
+ PostButtonClicks(pInfo, ev, wheel_left_button, -value);
+ break;
- if (ev->code > ABS_MAX)
- break;
- pEvdev->vals[pEvdev->axis_map[ev->code]] = value;
- if (ev->code == ABS_X)
- abs |= ABS_X_VALUE;
- else if (ev->code == ABS_Y)
- abs |= ABS_Y_VALUE;
- else
- abs |= ABS_VALUE;
+ /* We don't post wheel events as axis motion. */
+ default:
+ pEvdev->delta[ev->code] += value;
break;
+ }
+}
- case EV_KEY:
- /* don't repeat mouse buttons */
- if (ev->code >= BTN_MOUSE && ev->code < KEY_OK)
- if (value == 2)
- break;
-
- switch (ev->code) {
- case BTN_TOUCH:
- case BTN_TOOL_PEN:
- case BTN_TOOL_RUBBER:
- case BTN_TOOL_BRUSH:
- case BTN_TOOL_PENCIL:
- case BTN_TOOL_AIRBRUSH:
- case BTN_TOOL_FINGER:
- case BTN_TOOL_MOUSE:
- case BTN_TOOL_LENS:
- pEvdev->tool = value ? ev->code : 0;
- if (!(pEvdev->flags & EVDEV_TOUCHSCREEN))
- break;
- /* Treat BTN_TOUCH from devices that only have BTN_TOUCH as
- * BTN_LEFT. */
- ev->code = BTN_LEFT;
- /* Intentional fallthrough! */
-
- default:
- button = EvdevUtilButtonEventToButtonNumber(pEvdev, ev->code);
-
- /* Handle drag lock */
- if (EvdevDragLockFilterEvent(pInfo, button, value))
- break;
-
- if (EvdevWheelEmuFilterButton(pInfo, button, value))
- break;
-
- if (EvdevMBEmuFilterEvent(pInfo, button, value))
- break;
-
- if (button)
- xf86PostButtonEvent(pInfo->dev, 0, button, value, 0, 0);
- else
- PostKbdEvent(pInfo, ev, value);
- break;
- }
+/**
+ * Take the absolute motion input event and process it accordingly.
+ */
+static void
+EvdevProcessAbsoluteMotionEvent(InputInfoPtr pInfo, struct input_event *ev)
+{
+ static int value;
+ EvdevPtr pEvdev = pInfo->private;
+
+ /* Get the signed value, earlier kernels had this as unsigned */
+ value = ev->value;
+
+ /* Ignore EV_ABS events if we never set up for them. */
+ if (!(pEvdev->flags & EVDEV_ABSOLUTE_EVENTS))
+ return;
+
+ if (ev->code > ABS_MAX)
+ return;
+
+ pEvdev->vals[pEvdev->axis_map[ev->code]] = value;
+ if (ev->code == ABS_X)
+ pEvdev->abs |= ABS_X_VALUE;
+ else if (ev->code == ABS_Y)
+ pEvdev->abs |= ABS_Y_VALUE;
+ else
+ pEvdev->abs |= ABS_VALUE;
+}
+
+/**
+ * Take the key press/release input event and process it accordingly.
+ */
+static void
+EvdevProcessKeyEvent(InputInfoPtr pInfo, struct input_event *ev)
+{
+ static int value;
+ EvdevPtr pEvdev = pInfo->private;
+
+ /* Get the signed value, earlier kernels had this as unsigned */
+ value = ev->value;
+
+ /* don't repeat mouse buttons */
+ if (ev->code >= BTN_MOUSE && ev->code < KEY_OK)
+ if (value == 2)
+ return;
+
+ switch (ev->code) {
+ case BTN_TOUCH:
+ case BTN_TOOL_PEN:
+ case BTN_TOOL_RUBBER:
+ case BTN_TOOL_BRUSH:
+ case BTN_TOOL_PENCIL:
+ case BTN_TOOL_AIRBRUSH:
+ case BTN_TOOL_FINGER:
+ case BTN_TOOL_MOUSE:
+ case BTN_TOOL_LENS:
+ pEvdev->tool = value ? ev->code : 0;
+ if (!(pEvdev->flags & EVDEV_TOUCHSCREEN))
+ break;
+ /* Treat BTN_TOUCH from devices that only have BTN_TOUCH as
+ * BTN_LEFT. */
+ ev->code = BTN_LEFT;
+ /* Intentional fallthrough! */
+
+ default:
+ EvdevProcessButtonEvent(pInfo, ev);
break;
+ }
+}
- case EV_SYN:
- /* convert to relative motion for touchpads */
- if (abs && (pEvdev->flags & EVDEV_TOUCHPAD)) {
- if (pEvdev->tool) { /* meaning, touch is active */
- if (pEvdev->old_vals[0] != -1)
- delta[REL_X] = pEvdev->vals[0] - pEvdev->old_vals[0];
- if (pEvdev->old_vals[1] != -1)
- delta[REL_Y] = pEvdev->vals[1] - pEvdev->old_vals[1];
- if (abs & ABS_X_VALUE)
- pEvdev->old_vals[0] = pEvdev->vals[0];
- if (abs & ABS_Y_VALUE)
- pEvdev->old_vals[1] = pEvdev->vals[1];
- } else {
- pEvdev->old_vals[0] = pEvdev->old_vals[1] = -1;
- }
- abs = 0;
- rel = 1;
- }
+/**
+ * Post the relative motion events.
+ */
+static void
+EvdevPostRelativeMotionEvents(InputInfoPtr pInfo, int *num_v, int *first_v,
+ int v[MAX_VALUATORS])
+{
+ EvdevPtr pEvdev = pInfo->private;
- if (rel) {
- int post_deltas[REL_CNT] = {0}; /* axis-mapped deltas */
- int first = REL_CNT, last = 0;
- int i;
+ if (pEvdev->rel) {
+ xf86PostMotionEventP(pInfo->dev, FALSE, *first_v, *num_v, v + *first_v);
+ }
+}
- if (pEvdev->swap_axes) {
- tmp = delta[REL_X];
- delta[REL_X] = delta[REL_Y];
- delta[REL_Y] = tmp;
- }
- if (pEvdev->invert_x)
- delta[REL_X] *= -1;
- if (pEvdev->invert_y)
- delta[REL_Y] *= -1;
+/**
+ * Post the absolute motion events.
+ */
+static void
+EvdevPostAbsoluteMotionEvents(InputInfoPtr pInfo, int *num_v, int *first_v,
+ int v[MAX_VALUATORS])
+{
+ EvdevPtr pEvdev = pInfo->private;
- for (i = 0; i < REL_CNT; i++)
- {
- int map = pEvdev->axis_map[i];
- if (delta[i] && map != -1)
- {
- post_deltas[map] = delta[i];
- if (map < first)
- first = map;
- if (map > last)
- last = map;
- }
- }
+ /*
+ * Some devices only generate valid abs coords when BTN_DIGI is
+ * pressed. On wacom tablets, this means that the pen is in
+ * proximity of the tablet. After the pen is removed, BTN_DIGI is
+ * released, and a (0, 0) absolute event is generated. Checking
+ * pEvdev->digi here, lets us ignore that event. pEvdev is
+ * initialized to 1 so devices that doesn't use this scheme still
+ * just works.
+ */
+ if (pEvdev->abs && pEvdev->tool) {
+ xf86PostMotionEventP(pInfo->dev, TRUE, *first_v, *num_v, v);
+ }
+}
- xf86PostMotionEventP(pInfo->dev, FALSE, first,
- (last - first + 1), &post_deltas[first]);
- }
+/**
+ * Post the queued key/button events.
+ */
+static void EvdevPostQueuedEvents(InputInfoPtr pInfo, int *num_v, int *first_v,
+ int v[MAX_VALUATORS])
+{
+ int i;
+ EvdevPtr pEvdev = pInfo->private;
- /*
- * Some devices only generate valid abs coords when BTN_DIGI is
- * pressed. On wacom tablets, this means that the pen is in
- * proximity of the tablet. After the pen is removed, BTN_DIGI is
- * released, and a (0, 0) absolute event is generated. Checking
- * pEvdev->digi here, lets us ignore that event. pEvdev is
- * initialized to 1 so devices that doesn't use this scheme still
- * just works.
- */
- if (abs && pEvdev->tool) {
- int v[MAX_VALUATORS];
-
- memcpy(v, pEvdev->vals, sizeof(int) * pEvdev->num_vals);
- if (pEvdev->flags & EVDEV_CALIBRATED)
- {
- v[0] = xf86ScaleAxis(v[0],
- pEvdev->absinfo[ABS_X].maximum,
- pEvdev->absinfo[ABS_X].minimum,
- pEvdev->calibration.max_x, pEvdev->calibration.min_x);
- v[1] = xf86ScaleAxis(v[1],
- pEvdev->absinfo[ABS_Y].maximum,
- pEvdev->absinfo[ABS_Y].minimum,
- pEvdev->calibration.max_y, pEvdev->calibration.min_y);
- }
+ for (i = 0; i < pEvdev->num_queue; i++) {
+ switch (pEvdev->queue[i].type) {
+ case EV_QUEUE_KEY:
+ xf86PostKeyboardEvent(pInfo->dev, pEvdev->queue[i].key,
+ pEvdev->queue[i].val);
+ break;
+ case EV_QUEUE_BTN:
+ /* FIXME: Add xf86PostButtonEventP to the X server so that we may
+ * pass the valuators on ButtonPress/Release events, too. Currently
+ * only MotionNotify events contain the pointer position. */
+ xf86PostButtonEvent(pInfo->dev, 0, pEvdev->queue[i].key,
+ pEvdev->queue[i].val, 0, 0);
+ break;
+ }
+ }
+}
- if (pEvdev->swap_axes) {
- int tmp = v[0];
- v[0] = v[1];
- v[1] = tmp;
- }
+/**
+ * Take the synchronization input event and process it accordingly; the motion
+ * notify events are sent first, then any button/key press/release events.
+ */
+static void
+EvdevProcessSyncEvent(InputInfoPtr pInfo, struct input_event *ev)
+{
+ int num_v = 0, first_v = 0;
+ int v[MAX_VALUATORS];
+ EvdevPtr pEvdev = pInfo->private;
- if (pEvdev->invert_x)
- v[0] = (pEvdev->absinfo[ABS_X].maximum - v[0] +
- pEvdev->absinfo[ABS_X].minimum);
- if (pEvdev->invert_y)
- v[1] = (pEvdev->absinfo[ABS_Y].maximum - v[1] +
- pEvdev->absinfo[ABS_Y].minimum);
+ EvdevProcessValuators(pInfo, v, &num_v, &first_v);
- xf86PostMotionEventP(pInfo->dev, TRUE, 0, pEvdev->num_vals, v);
- }
+ EvdevPostRelativeMotionEvents(pInfo, &num_v, &first_v, v);
+ EvdevPostAbsoluteMotionEvents(pInfo, &num_v, &first_v, v);
+ EvdevPostQueuedEvents(pInfo, &num_v, &first_v, v);
+
+ memset(pEvdev->delta, 0, sizeof(pEvdev->delta));
+ memset(pEvdev->queue, 0, sizeof(pEvdev->queue));
+ pEvdev->abs = 0;
+ pEvdev->rel = 0;
+ pEvdev->num_queue = 0;
+}
- memset(delta, 0, sizeof(delta));
- tmp = 0;
- abs = 0;
- rel = 0;
+/**
+ * Process the events from the server; nothing is actually posted to the server
+ * until an EV_SYN event is received.
+ */
+static void
+EvdevProcessEvent(InputInfoPtr pInfo, struct input_event *ev)
+{
+ switch (ev->type) {
+ case EV_REL:
+ EvdevProcessRelativeMotionEvent(pInfo, ev);
+ break;
+ case EV_ABS:
+ EvdevProcessAbsoluteMotionEvent(pInfo, ev);
+ break;
+ case EV_KEY:
+ EvdevProcessKeyEvent(pInfo, ev);
+ break;
+ case EV_SYN:
+ EvdevProcessSyncEvent(pInfo, ev);
+ break;
}
}
diff --git a/src/evdev.h b/src/evdev.h
index 5b95369..4cf0be6 100644
--- a/src/evdev.h
+++ b/src/evdev.h
@@ -54,6 +54,7 @@
#endif
#define EVDEV_MAXBUTTONS 32
+#define EVDEV_MAXQUEUE 32
#if GET_ABI_MAJOR(ABI_XINPUT_VERSION) >= 3
#define HAVE_PROPERTIES 1
@@ -88,6 +89,15 @@ typedef struct {
} WheelAxis, *WheelAxisPtr;
typedef struct {
+ enum {
+ EV_QUEUE_KEY, /* xf86PostKeyboardEvent() */
+ EV_QUEUE_BTN, /* xf86PostButtonEvent() */
+ } type;
+ int key;
+ int val;
+} EventQueueRec, *EventQueuePtr;
+
+typedef struct {
const char *device;
int grabDevice; /* grab the event device? */
@@ -103,6 +113,9 @@ typedef struct {
BOOL invert_x;
BOOL invert_y;
+ int delta[REL_CNT];
+ unsigned int abs, rel;
+
/* XKB stuff has to be per-device rather than per-driver */
#if GET_ABI_MAJOR(ABI_XINPUT_VERSION) < 5
XkbComponentNamesRec xkbnames;
@@ -159,6 +172,10 @@ typedef struct {
/* minor/major number */
dev_t min_maj;
+
+ /* event queue used for button/key events between EV_SYN events */
+ int num_queue;
+ EventQueueRec queue[EVDEV_MAXQUEUE];
} EvdevRec, *EvdevPtr;
unsigned int EvdevUtilButtonEventToButtonNumber(EvdevPtr pEvdev, int code);
--
1.6.1
More information about the xorg-devel
mailing list