From a65718e20fb0db6ea0b3d6989dcb336ac67c501f Mon Sep 17 00:00:00 2001 From: rpaulo Date: Mon, 9 Nov 2009 15:59:09 +0000 Subject: Driver for the Apple Touchpad present on MacBook (non-Pro & Pro). Submitted by: Rohit Grover MFC after: 2 months --- sys/conf/files | 1 + sys/dev/usb/input/atp.c | 2047 ++++++++++++++++++++++++++++++++++++++++++ sys/modules/usb/Makefile | 2 +- sys/modules/usb/atp/Makefile | 11 + 4 files changed, 2060 insertions(+), 1 deletion(-) create mode 100644 sys/dev/usb/input/atp.c create mode 100644 sys/modules/usb/atp/Makefile diff --git a/sys/conf/files b/sys/conf/files index b8dee58..a536b6d 100644 --- a/sys/conf/files +++ b/sys/conf/files @@ -1698,6 +1698,7 @@ dev/usb/misc/udbp.c optional udbp # # USB input drivers # +dev/usb/input/atp.c optional atp dev/usb/input/uhid.c optional uhid dev/usb/input/ukbd.c optional ukbd dev/usb/input/ums.c optional ums diff --git a/sys/dev/usb/input/atp.c b/sys/dev/usb/input/atp.c new file mode 100644 index 0000000..1806802 --- /dev/null +++ b/sys/dev/usb/input/atp.c @@ -0,0 +1,2047 @@ +/*- + * Copyright (c) 2009 Rohit Grover + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +__FBSDID("$FreeBSD$"); + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "usbdevs.h" + +#define USB_DEBUG_VAR atp_debug +#include + +#include + +#define ATP_DRIVER_NAME "atp" + +/* + * Driver specific options: the following options may be set by + * `options' statements in the kernel configuration file. + */ + +/* The multiplier used to translate sensor reported positions to mickeys. */ +#ifndef ATP_SCALE_FACTOR +#define ATP_SCALE_FACTOR 48 +#endif + +/* + * This is the age (in microseconds) beyond which a touch is + * considered to be a slide; and therefore a tap event isn't registered. + */ +#ifndef ATP_TOUCH_TIMEOUT +#define ATP_TOUCH_TIMEOUT 125000 +#endif + +/* + * A double-tap followed by a single-finger slide is treated as a + * special gesture. The driver responds to this gesture by assuming a + * virtual button-press for the lifetime of the slide. The following + * threshold is the maximum time gap (in microseconds) between the two + * tap events preceding the slide for such a gesture. + */ +#ifndef ATP_DOUBLE_TAP_N_DRAG_THRESHOLD +#define ATP_DOUBLE_TAP_N_DRAG_THRESHOLD 200000 +#endif + +/* + * The device provides us only with pressure readings from an array of + * X and Y sensors; for our algorithms, we need to interpret groups + * (typically pairs) of X and Y readings as being related to a single + * finger stroke. We can relate X and Y readings based on their times + * of incidence. The coincidence window should be at least 10000us + * since it is used against values from getmicrotime(), which has a + * precision of around 10ms. + */ +#ifndef ATP_COINCIDENCE_THRESHOLD +#define ATP_COINCIDENCE_THRESHOLD 40000 /* unit: microseconds */ +#if ATP_COINCIDENCE_THRESHOLD > 100000 +#error "ATP_COINCIDENCE_THRESHOLD too large" +#endif +#endif /* #ifndef ATP_COINCIDENCE_THRESHOLD */ + +/* + * The wait duration (in microseconds) after losing a touch contact + * before zombied strokes are reaped and turned into button events. + */ +#define ATP_ZOMBIE_STROKE_REAP_WINDOW 50000 +#if ATP_ZOMBIE_STROKE_REAP_WINDOW > 100000 +#error "ATP_ZOMBIE_STROKE_REAP_WINDOW too large" +#endif + +/* end of driver specific options */ + + +/* Tunables */ +SYSCTL_NODE(_hw_usb, OID_AUTO, atp, CTLFLAG_RW, 0, "USB atp"); + +#if USB_DEBUG +enum atp_log_level { + ATP_LLEVEL_DISABLED = 0, + ATP_LLEVEL_ERROR, + ATP_LLEVEL_DEBUG, /* for troubleshooting */ + ATP_LLEVEL_INFO, /* for diagnostics */ +}; +static int atp_debug = ATP_LLEVEL_ERROR; /* the default is to only log errors */ +SYSCTL_INT(_hw_usb_atp, OID_AUTO, debug, CTLFLAG_RW, + &atp_debug, ATP_LLEVEL_ERROR, "ATP debug level"); +#endif /* #if USB_DEBUG */ + +static u_int atp_touch_timeout = ATP_TOUCH_TIMEOUT; +SYSCTL_INT(_hw_usb_atp, OID_AUTO, touch_timeout, CTLFLAG_RW, &atp_touch_timeout, + 125000, "age threshold (in micros) for a touch"); + +static u_int atp_double_tap_threshold = ATP_DOUBLE_TAP_N_DRAG_THRESHOLD; +SYSCTL_INT(_hw_usb_atp, OID_AUTO, double_tap_threshold, CTLFLAG_RW, + &atp_double_tap_threshold, ATP_DOUBLE_TAP_N_DRAG_THRESHOLD, + "maximum time (in micros) between a double-tap"); + +static u_int atp_mickeys_scale_factor = ATP_SCALE_FACTOR; +static int atp_sysctl_scale_factor_handler(SYSCTL_HANDLER_ARGS); +SYSCTL_PROC(_hw_usb_atp, OID_AUTO, scale_factor, CTLTYPE_UINT | CTLFLAG_RW, + &atp_mickeys_scale_factor, sizeof(atp_mickeys_scale_factor), + atp_sysctl_scale_factor_handler, "IU", "movement scale factor"); + +static u_int atp_small_movement_threshold = ATP_SCALE_FACTOR >> 3; +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, small_movement, CTLFLAG_RW, + &atp_small_movement_threshold, ATP_SCALE_FACTOR >> 3, + "the small movement black-hole for filtering noise"); +/* + * The movement threshold for a stroke; this is the maximum difference + * in position which will be resolved as a continuation of a stroke + * component. + */ +static u_int atp_max_delta_mickeys = ((3 * ATP_SCALE_FACTOR) >> 1); +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, max_delta_mickeys, CTLFLAG_RW, + &atp_max_delta_mickeys, ((3 * ATP_SCALE_FACTOR) >> 1), + "max. mickeys-delta which will match against an existing stroke"); +/* + * Strokes which accumulate at least this amount of absolute movement + * from the aggregate of their components are considered as + * slides. Unit: mickeys. + */ +static u_int atp_slide_min_movement = (ATP_SCALE_FACTOR >> 3); +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, slide_min_movement, CTLFLAG_RW, + &atp_slide_min_movement, (ATP_SCALE_FACTOR >> 3), + "strokes with at least this amt. of movement are considered slides"); + +/* + * The minimum age of a stroke for it to be considered mature; this + * helps filter movements (noise) from immature strokes. Units: interrupts. + */ +static u_int atp_stroke_maturity_threshold = 2; +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, stroke_maturity_threshold, CTLFLAG_RW, + &atp_stroke_maturity_threshold, 2, + "the minimum age of a stroke for it to be considered mature"); + +/* Accept pressure readings from sensors only if above this value. */ +static u_int atp_sensor_noise_threshold = 2; +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, sensor_noise_threshold, CTLFLAG_RW, + &atp_sensor_noise_threshold, 2, + "accept pressure readings from sensors only if above this value"); + +/* Ignore pressure spans with cumulative press. below this value. */ +static u_int atp_pspan_min_cum_pressure = 10; +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, pspan_min_cum_pressure, CTLFLAG_RW, + &atp_pspan_min_cum_pressure, 10, + "ignore pressure spans with cumulative press. below this value"); + +/* Maximum allowed width for pressure-spans.*/ +static u_int atp_pspan_max_width = 4; +SYSCTL_UINT(_hw_usb_atp, OID_AUTO, pspan_max_width, CTLFLAG_RW, + &atp_pspan_max_width, 4, + "maximum allowed width (in sensors) for pressure-spans"); + + +/* Define the various flavours of devices supported by this driver. */ +enum { + ATP_DEV_PARAMS_0, + ATP_N_DEV_PARAMS +}; +struct atp_dev_params { + u_int data_len; /* for sensor data */ + u_int n_xsensors; + u_int n_ysensors; +} atp_dev_params[ATP_N_DEV_PARAMS] = { + [ATP_DEV_PARAMS_0] = { + .data_len = 64, + .n_xsensors = 20, + .n_ysensors = 10 + }, +}; + +static const struct usb_device_id atp_devs[] = { + /* Core Duo MacBook & MacBook Pro */ + { USB_VPI(USB_VENDOR_APPLE, 0x0217, ATP_DEV_PARAMS_0) }, + { USB_VPI(USB_VENDOR_APPLE, 0x0218, ATP_DEV_PARAMS_0) }, + { USB_VPI(USB_VENDOR_APPLE, 0x0219, ATP_DEV_PARAMS_0) }, + + /* Core2 Duo MacBook & MacBook Pro */ + { USB_VPI(USB_VENDOR_APPLE, 0x021a, ATP_DEV_PARAMS_0) }, + { USB_VPI(USB_VENDOR_APPLE, 0x021b, ATP_DEV_PARAMS_0) }, + { USB_VPI(USB_VENDOR_APPLE, 0x021c, ATP_DEV_PARAMS_0) }, + + /* Core2 Duo MacBook3,1 */ + { USB_VPI(USB_VENDOR_APPLE, 0x0229, ATP_DEV_PARAMS_0) }, + { USB_VPI(USB_VENDOR_APPLE, 0x022a, ATP_DEV_PARAMS_0) }, + { USB_VPI(USB_VENDOR_APPLE, 0x022b, ATP_DEV_PARAMS_0) }, +}; + +/* + * The following structure captures the state of a pressure span along + * an axis. Each contact with the touchpad results in separate + * pressure spans along the two axes. + */ +typedef struct atp_pspan { + u_int width; /* in units of sensors */ + u_int cum; /* cumulative compression (from all sensors) */ + u_int cog; /* center of gravity */ + u_int loc; /* location (scaled using the mickeys factor) */ + boolean_t matched; /* to track pspans as they match against strokes. */ +} atp_pspan; + +typedef enum atp_stroke_type { + ATP_STROKE_TOUCH, + ATP_STROKE_SLIDE, +} atp_stroke_type; + +#define ATP_MAX_PSPANS_PER_AXIS 3 + +typedef struct atp_stroke_component { + /* Fields encapsulating the pressure-span. */ + u_int loc; /* location (scaled) */ + u_int cum_pressure; /* cumulative compression */ + u_int max_cum_pressure; /* max cumulative compression */ + boolean_t matched; /*to track components as they match against pspans.*/ + + /* Fields containing information about movement. */ + int delta_mickeys; /* change in location (un-smoothened movement)*/ + int pending; /* cum. of pending short movements */ + int movement; /* current smoothened movement */ +} atp_stroke_component; + +typedef enum atp_axis { + X = 0, + Y = 1 +} atp_axis; + +#define ATP_MAX_STROKES (2 * ATP_MAX_PSPANS_PER_AXIS) + +/* + * The following structure captures a finger contact with the + * touchpad. A stroke comprises two p-span components and some state. + */ +typedef struct atp_stroke { + atp_stroke_type type; + struct timeval ctime; /* create time; for coincident siblings. */ + u_int age; /* + * Unit: interrupts; we maintain + * this value in addition to + * 'ctime' in order to avoid the + * expensive call to microtime() + * at every interrupt. + */ + + atp_stroke_component components[2]; + u_int velocity_squared; /* + * Average magnitude (squared) + * of recent velocity. + */ + u_int cum_movement; /* cum. absolute movement so far */ + + uint32_t flags; /* the state of this stroke */ +#define ATSF_ZOMBIE 0x1 +} atp_stroke; + +#define ATP_FIFO_BUF_SIZE 8 /* bytes */ +#define ATP_FIFO_QUEUE_MAXLEN 50 /* units */ + +enum { + ATP_INTR_DT, + ATP_N_TRANSFER, +}; + +struct atp_softc { + device_t sc_dev; + struct usb_device *sc_usb_device; +#define MODE_LENGTH 8 + char sc_mode_bytes[MODE_LENGTH]; /* device mode */ + struct mtx sc_mutex; /* for synchronization */ + struct usb_xfer *sc_xfer[ATP_N_TRANSFER]; + struct usb_fifo_sc sc_fifo; + + struct atp_dev_params *sc_params; + + mousehw_t sc_hw; + mousemode_t sc_mode; + u_int sc_pollrate; + mousestatus_t sc_status; + u_int sc_state; +#define ATP_ENABLED 0x01 +#define ATP_ZOMBIES_EXIST 0x02 +#define ATP_DOUBLE_TAP_DRAG 0x04 + + u_int sc_left_margin; + u_int sc_right_margin; + + atp_stroke sc_strokes[ATP_MAX_STROKES]; + u_int sc_n_strokes; + + int8_t *sensor_data; /* from interrupt packet */ + int *base_x; /* base sensor readings */ + int *base_y; + int *cur_x; /* current sensor readings */ + int *cur_y; + int *pressure_x; /* computed pressures */ + int *pressure_y; + + u_int sc_idlecount; /* preceding idle interrupts */ +#define ATP_IDLENESS_THRESHOLD 10 + + struct timeval sc_reap_time; + struct timeval sc_reap_ctime; /*ctime of siblings to be reaped*/ +}; + +/* + * The last byte of the sensor data contains status bits; the + * following values define the meanings of these bits. + */ +enum atp_status_bits { + ATP_STATUS_BUTTON = (uint8_t)0x01, /* The button was pressed */ + ATP_STATUS_BASE_UPDATE = (uint8_t)0x04, /* Data from an untouched pad.*/ +}; + +typedef enum interface_mode { + RAW_SENSOR_MODE = (uint8_t)0x04, + HID_MODE = (uint8_t)0x08 +} interface_mode; + +/* + * function prototypes + */ +static usb_fifo_cmd_t atp_start_read; +static usb_fifo_cmd_t atp_stop_read; +static usb_fifo_open_t atp_open; +static usb_fifo_close_t atp_close; +static usb_fifo_ioctl_t atp_ioctl; + +static struct usb_fifo_methods atp_fifo_methods = { + .f_open = &atp_open, + .f_close = &atp_close, + .f_ioctl = &atp_ioctl, + .f_start_read = &atp_start_read, + .f_stop_read = &atp_stop_read, + .basename[0] = ATP_DRIVER_NAME, +}; + +/* device initialization and shutdown */ +static usb_error_t atp_req_get_report(struct usb_device *udev, void *data); +static int atp_set_device_mode(device_t dev, interface_mode mode); +static int atp_enable(struct atp_softc *sc); +static void atp_disable(struct atp_softc *sc); +static int atp_softc_populate(struct atp_softc *); +static void atp_softc_unpopulate(struct atp_softc *); + +/* sensor interpretation */ +static __inline void atp_interpret_sensor_data(const int8_t *, u_int, u_int, + int *); +static __inline void atp_get_pressures(int *, const int *, const int *, int); +static void atp_detect_pspans(int *, u_int, u_int, atp_pspan *, + u_int *); + +/* movement detection */ +static boolean_t atp_match_stroke_component(atp_stroke_component *, + const atp_pspan *); +static void atp_match_strokes_against_pspans(struct atp_softc *, + atp_axis, atp_pspan *, u_int, u_int); +static boolean_t atp_update_strokes(struct atp_softc *, + atp_pspan *, u_int, atp_pspan *, u_int); +static __inline void atp_add_stroke(struct atp_softc *, const atp_pspan *, + const atp_pspan *); +static void atp_add_new_strokes(struct atp_softc *, atp_pspan *, + u_int, atp_pspan *, u_int); +static void atp_advance_stroke_state(struct atp_softc *, + atp_stroke *, boolean_t *); +static void atp_terminate_stroke(struct atp_softc *, u_int); +static __inline boolean_t atp_stroke_has_small_movement(const atp_stroke *); +static __inline void atp_update_pending_mickeys(atp_stroke_component *); +static void atp_compute_smoothening_scale_ratio(atp_stroke *, int *, + int *); +static boolean_t atp_compute_stroke_movement(atp_stroke *); + +/* tap detection */ +static __inline void atp_setup_reap_time(struct atp_softc *, struct timeval *); +static void atp_reap_zombies(struct atp_softc *, u_int *, u_int *); + +/* updating fifo */ +static void atp_reset_buf(struct atp_softc *sc); +static void atp_add_to_queue(struct atp_softc *, int, int, uint32_t); + + +usb_error_t +atp_req_get_report(struct usb_device *udev, void *data) +{ + struct usb_device_request req; + + req.bmRequestType = UT_READ_CLASS_INTERFACE; + req.bRequest = UR_GET_REPORT; + USETW2(req.wValue, (uint8_t)0x03 /* type */, (uint8_t)0x00 /* id */); + USETW(req.wIndex, 0); + USETW(req.wLength, MODE_LENGTH); + + return (usbd_do_request(udev, NULL /* mutex */, &req, data)); +} + +static int +atp_set_device_mode(device_t dev, interface_mode mode) +{ + struct atp_softc *sc; + usb_device_request_t req; + usb_error_t err; + + if ((mode != RAW_SENSOR_MODE) && (mode != HID_MODE)) + return (ENXIO); + + sc = device_get_softc(dev); + + sc->sc_mode_bytes[0] = mode; + req.bmRequestType = UT_WRITE_CLASS_INTERFACE; + req.bRequest = UR_SET_REPORT; + USETW2(req.wValue, (uint8_t)0x03 /* type */, (uint8_t)0x00 /* id */); + USETW(req.wIndex, 0); + USETW(req.wLength, MODE_LENGTH); + err = usbd_do_request(sc->sc_usb_device, NULL, &req, sc->sc_mode_bytes); + if (err != USB_ERR_NORMAL_COMPLETION) + return (ENXIO); + + return (0); +} + +static int +atp_enable(struct atp_softc *sc) +{ + /* Allocate the dynamic buffers */ + if (atp_softc_populate(sc) != 0) { + atp_softc_unpopulate(sc); + return (ENOMEM); + } + + /* reset status */ + memset(sc->sc_strokes, 0, sizeof(sc->sc_strokes)); + sc->sc_n_strokes = 0; + memset(&sc->sc_status, 0, sizeof(sc->sc_status)); + sc->sc_idlecount = 0; + sc->sc_state |= ATP_ENABLED; + + DPRINTFN(ATP_LLEVEL_INFO, "enabled atp\n"); + return (0); +} + +static void +atp_disable(struct atp_softc *sc) +{ + atp_softc_unpopulate(sc); + + sc->sc_state &= ~ATP_ENABLED; + DPRINTFN(ATP_LLEVEL_INFO, "disabled atp\n"); +} + +/* Allocate dynamic memory for some fields in softc. */ +static int +atp_softc_populate(struct atp_softc *sc) +{ + const struct atp_dev_params *params = sc->sc_params; + + if (params == NULL) { + DPRINTF("params uninitialized!\n"); + return (ENXIO); + } + if (params->data_len) { + sc->sensor_data = malloc(params->data_len * sizeof(int8_t), + M_USB, M_WAITOK); + if (sc->sensor_data == NULL) { + DPRINTF("mem for sensor_data\n"); + return (ENXIO); + } + } + + if (params->n_xsensors != 0) { + sc->base_x = malloc(params->n_xsensors * sizeof(*(sc->base_x)), + M_USB, M_WAITOK); + if (sc->base_x == NULL) { + DPRINTF("mem for sc->base_x\n"); + return (ENXIO); + } + + sc->cur_x = malloc(params->n_xsensors * sizeof(*(sc->cur_x)), + M_USB, M_WAITOK); + if (sc->cur_x == NULL) { + DPRINTF("mem for sc->cur_x\n"); + return (ENXIO); + } + + sc->pressure_x = + malloc(params->n_xsensors * sizeof(*(sc->pressure_x)), + M_USB, M_WAITOK); + if (sc->pressure_x == NULL) { + DPRINTF("mem. for pressure_x\n"); + return (ENXIO); + } + } + + if (params->n_ysensors != 0) { + sc->base_y = malloc(params->n_ysensors * sizeof(*(sc->base_y)), + M_USB, M_WAITOK); + if (sc->base_y == NULL) { + DPRINTF("mem for base_y\n"); + return (ENXIO); + } + + sc->cur_y = malloc(params->n_ysensors * sizeof(*(sc->cur_y)), + M_USB, M_WAITOK); + if (sc->cur_y == NULL) { + DPRINTF("mem for cur_y\n"); + return (ENXIO); + } + + sc->pressure_y = + malloc(params->n_ysensors * sizeof(*(sc->pressure_y)), + M_USB, M_WAITOK); + if (sc->pressure_y == NULL) { + DPRINTF("mem. for pressure_y\n"); + return (ENXIO); + } + } + + return (0); +} + +/* Free dynamic memory allocated for some fields in softc. */ +static void +atp_softc_unpopulate(struct atp_softc *sc) +{ + const struct atp_dev_params *params = sc->sc_params; + + if (params == NULL) { + return; + } + if (params->n_xsensors != 0) { + if (sc->base_x != NULL) { + free(sc->base_x, M_USB); + sc->base_x = NULL; + } + + if (sc->cur_x != NULL) { + free(sc->cur_x, M_USB); + sc->cur_x = NULL; + } + + if (sc->pressure_x != NULL) { + free(sc->pressure_x, M_USB); + sc->pressure_x = NULL; + } + } + if (params->n_ysensors != 0) { + if (sc->base_y != NULL) { + free(sc->base_y, M_USB); + sc->base_y = NULL; + } + + if (sc->cur_y != NULL) { + free(sc->cur_y, M_USB); + sc->cur_y = NULL; + } + + if (sc->pressure_y != NULL) { + free(sc->pressure_y, M_USB); + sc->pressure_y = NULL; + } + } + if (sc->sensor_data != NULL) { + free(sc->sensor_data, M_USB); + sc->sensor_data = NULL; + } +} + +/* + * Interpret the data from the X and Y pressure sensors. This function + * is called separately for the X and Y sensor arrays. The data in the + * USB packet is laid out in the following manner: + * + * sensor_data: + * --,--,Y1,Y2,--,Y3,Y4,--,Y5,...,Y10, ... X1,X2,--,X3,X4 + * indices: 0 1 2 3 4 5 6 7 8 ... 15 ... 20 21 22 23 24 + * + * '--' (in the above) indicates that the value is unimportant. + * + * Information about the above layout was obtained from the + * implementation of the AppleTouch driver in Linux. + * + * parameters: + * sensor_data + * raw sensor data from the USB packet. + * num + * The number of elements in the array 'arr'. + * di_start + * The index of the first data element to be interpreted for + * this sensor array--i.e. when called to interpret the Y + * sensors, di_start passed in as 2, which is the index of Y1 in + * the raw data. + * arr + * The array to be initialized with the readings. + */ +static __inline void +atp_interpret_sensor_data(const int8_t *sensor_data, u_int num, u_int di_start, + int *arr) +{ + u_int i; + u_int di; /* index into sensor data */ + + for (i = 0, di = di_start; i < num; /* empty */ ) { + arr[i++] = sensor_data[di++]; + arr[i++] = sensor_data[di++]; + di++; + } +} + +static __inline void +atp_get_pressures(int *p, const int *cur, const int *base, int n) +{ + int i; + + for (i = 0; i < n; i++) { + p[i] = cur[i] - base[i]; + if (p[i] > 127) + p[i] -= 256; + if (p[i] < -127) + p[i] += 256; + if (p[i] < 0) + p[i] = 0; + + /* + * Shave off pressures below the noise-pressure + * threshold; this will reduce the contribution from + * lower pressure readings. + */ + if (p[i] <= atp_sensor_noise_threshold) + p[i] = 0; /* filter away noise */ + else + p[i] -= atp_sensor_noise_threshold; + } +} + +static void +atp_detect_pspans(int *p, u_int num_sensors, + u_int max_spans, /* max # of pspans permitted */ + atp_pspan *spans, /* finger spans */ + u_int *nspans_p) /* num spans detected */ +{ + u_int i; + int maxp; /* max pressure seen within a span */ + u_int num_spans = 0; + + enum atp_pspan_state { + ATP_PSPAN_INACTIVE, + ATP_PSPAN_INCREASING, + ATP_PSPAN_DECREASING, + } state; /* state of the pressure span */ + + /* + * The following is a simple state machine to track + * the phase of the pressure span. + */ + memset(spans, 0, max_spans * sizeof(atp_pspan)); + maxp = 0; + state = ATP_PSPAN_INACTIVE; + for (i = 0; i < num_sensors; i++) { + if (num_spans >= max_spans) + break; + + if (p[i] == 0) { + if (state == ATP_PSPAN_INACTIVE) { + /* + * There is no pressure information for this + * sensor, and we aren't tracking a finger. + */ + continue; + } else { + state = ATP_PSPAN_INACTIVE; + maxp = 0; + num_spans++; + } + } else { + switch (state) { + case ATP_PSPAN_INACTIVE: + state = ATP_PSPAN_INCREASING; + maxp = p[i]; + break; + + case ATP_PSPAN_INCREASING: + if (p[i] > maxp) + maxp = p[i]; + else if (p[i] <= (maxp >> 1)) + state = ATP_PSPAN_DECREASING; + break; + + case ATP_PSPAN_DECREASING: + if (p[i] > p[i - 1]) { + /* + * This is the beginning of + * another span; change state + * to give the appearance that + * we're starting from an + * inactive span, and then + * re-process this reading in + * the next iteration. + */ + num_spans++; + state = ATP_PSPAN_INACTIVE; + maxp = 0; + i--; + continue; + } + break; + } + + /* Update the finger span with this reading. */ + spans[num_spans].width++; + spans[num_spans].cum += p[i]; + spans[num_spans].cog += p[i] * (i + 1); + } + } + if (state != ATP_PSPAN_INACTIVE) + num_spans++; /* close the last finger span */ + + /* post-process the spans */ + for (i = 0; i < num_spans; i++) { + /* filter away unwanted pressure spans */ + if ((spans[i].cum < atp_pspan_min_cum_pressure) || + (spans[i].width > atp_pspan_max_width)) { + if ((i + 1) < num_spans) { + memcpy(&spans[i], &spans[i + 1], + (num_spans - i - 1) * sizeof(atp_pspan)); + i--; + } + num_spans--; + continue; + } + + /* compute this span's representative location */ + spans[i].loc = spans[i].cog * atp_mickeys_scale_factor / + spans[i].cum; + + spans[i].matched = FALSE; /* not yet matched against a stroke */ + } + + *nspans_p = num_spans; +} + +/* + * Match a pressure-span against a stroke-component. If there is a + * match, update the component's state and return TRUE. + */ +static boolean_t +atp_match_stroke_component(atp_stroke_component *component, + const atp_pspan *pspan) +{ + int delta_mickeys = pspan->loc - component->loc; + + if (abs(delta_mickeys) > atp_max_delta_mickeys) + return (FALSE); /* the finger span is too far out; no match */ + + component->loc = pspan->loc; + component->cum_pressure = pspan->cum; + if (pspan->cum > component->max_cum_pressure) + component->max_cum_pressure = pspan->cum; + + /* + * If the cumulative pressure drops below a quarter of the max, + * then disregard the component's movement. + */ + if (component->cum_pressure < (component->max_cum_pressure >> 2)) + delta_mickeys = 0; + + component->delta_mickeys = delta_mickeys; + return (TRUE); +} + +static void +atp_match_strokes_against_pspans(struct atp_softc *sc, atp_axis axis, + atp_pspan *pspans, u_int n_pspans, u_int repeat_count) +{ + u_int i, j; + u_int repeat_index = 0; + + /* Determine the index of the multi-span. */ + if (repeat_count) { + u_int cum = 0; + for (i = 0; i < n_pspans; i++) { + if (pspans[i].cum > cum) { + repeat_index = i; + cum = pspans[i].cum; + } + } + } + + for (i = 0; i < sc->sc_n_strokes; i++) { + atp_stroke *stroke = &sc->sc_strokes[i]; + if (stroke->components[axis].matched) + continue; /* skip matched components */ + + for (j = 0; j < n_pspans; j++) { + if (pspans[j].matched) + continue; /* skip matched pspans */ + + if (atp_match_stroke_component( + &stroke->components[axis], &pspans[j])) { + /* There is a match. */ + stroke->components[axis].matched = TRUE; + + /* Take care to repeat at the multi-span. */ + if ((repeat_count > 0) && (j == repeat_index)) + repeat_count--; + else + pspans[j].matched = TRUE; + + break; /* skip to the next stroke */ + } + } /* loop over pspans */ + } /* loop over strokes */ +} + +/* + * Update strokes by matching against current pressure-spans. + * Return TRUE if any movement is detected. + */ +static boolean_t +atp_update_strokes(struct atp_softc *sc, atp_pspan *pspans_x, + u_int n_xpspans, atp_pspan *pspans_y, u_int n_ypspans) +{ + u_int i, j; + atp_stroke *stroke; + boolean_t movement = FALSE; + u_int repeat_count = 0; + + /* Reset X and Y components of all strokes as unmatched. */ + for (i = 0; i < sc->sc_n_strokes; i++) { + stroke = &sc->sc_strokes[i]; + stroke->components[X].matched = FALSE; + stroke->components[Y].matched = FALSE; + } + + /* + * Usually, the X and Y pspans come in pairs (the common case + * being a single pair). It is possible, however, that + * multiple contacts resolve to a single pspan along an + * axis, as illustrated in the following: + * + * F = finger-contact + * + * pspan pspan + * +-----------------------+ + * | . . | + * | . . | + * | . . | + * | . . | + * pspan |.........F......F | + * | | + * | | + * | | + * +-----------------------+ + * + * + * The above case can be detected by a difference in the + * number of X and Y pspans. When this happens, X and Y pspans + * aren't easy to pair or match against strokes. + * + * When X and Y pspans differ in number, the axis with the + * smaller number of pspans is regarded as having a repeating + * pspan (or a multi-pspan)--in the above illustration, the + * Y-axis has a repeating pspan. Our approach is to try to + * match the multi-pspan repeatedly against strokes. The + * difference between the number of X and Y pspans gives us a + * crude repeat_count for matching multi-pspans--i.e. the + * multi-pspan along the Y axis (above) has a repeat_count of 1. + */ + repeat_count = abs(n_xpspans - n_ypspans); + + atp_match_strokes_against_pspans(sc, X, pspans_x, n_xpspans, + (((repeat_count != 0) && ((n_xpspans < n_ypspans))) ? + repeat_count : 0)); + atp_match_strokes_against_pspans(sc, Y, pspans_y, n_ypspans, + (((repeat_count != 0) && (n_ypspans < n_xpspans)) ? + repeat_count : 0)); + + /* Update the state of strokes based on the above pspan matches. */ + for (i = 0; i < sc->sc_n_strokes; i++) { + stroke = &sc->sc_strokes[i]; + if (stroke->components[X].matched && + stroke->components[Y].matched) { + atp_advance_stroke_state(sc, stroke, &movement); + } else { + /* + * At least one component of this stroke + * didn't match against current pspans; + * terminate it. + */ + atp_terminate_stroke(sc, i); + } + } + + /* Add new strokes for pairs of unmatched pspans */ + for (i = 0; i < n_xpspans; i++) { + if (pspans_x[i].matched == FALSE) break; + } + for (j = 0; j < n_ypspans; j++) { + if (pspans_y[j].matched == FALSE) break; + } + if ((i < n_xpspans) && (j < n_ypspans)) { +#if USB_DEBUG + if (atp_debug >= ATP_LLEVEL_INFO) { + printf("unmatched pspans:"); + for (; i < n_xpspans; i++) { + if (pspans_x[i].matched) + continue; + printf(" X:[loc:%u,cum:%u]", + pspans_x[i].loc, pspans_x[i].cum); + } + for (; j < n_ypspans; j++) { + if (pspans_y[j].matched) + continue; + printf(" Y:[loc:%u,cum:%u]", + pspans_y[j].loc, pspans_y[j].cum); + } + printf("\n"); + } +#endif /* #if USB_DEBUG */ + if ((n_xpspans == 1) && (n_ypspans == 1)) + /* The common case of a single pair of new pspans. */ + atp_add_stroke(sc, &pspans_x[0], &pspans_y[0]); + else + atp_add_new_strokes(sc, + pspans_x, n_xpspans, + pspans_y, n_ypspans); + } + +#if USB_DEBUG + if (atp_debug >= ATP_LLEVEL_INFO) { + for (i = 0; i < sc->sc_n_strokes; i++) { + atp_stroke *stroke = &sc->sc_strokes[i]; + + printf(" %s%clc:%u,dm:%d,pnd:%d,mv:%d%c" + ",%clc:%u,dm:%d,pnd:%d,mv:%d%c", + (stroke->flags & ATSF_ZOMBIE) ? "zomb:" : "", + (stroke->type == ATP_STROKE_TOUCH) ? '[' : '<', + stroke->components[X].loc, + stroke->components[X].delta_mickeys, + stroke->components[X].pending, + stroke->components[X].movement, + (stroke->type == ATP_STROKE_TOUCH) ? ']' : '>', + (stroke->type == ATP_STROKE_TOUCH) ? '[' : '<', + stroke->components[Y].loc, + stroke->components[Y].delta_mickeys, + stroke->components[Y].pending, + stroke->components[Y].movement, + (stroke->type == ATP_STROKE_TOUCH) ? ']' : '>'); + } + if (sc->sc_n_strokes) + printf("\n"); + } +#endif /* #if USB_DEBUG */ + + return (movement); +} + +/* Initialize a stroke using a pressure-span. */ +static __inline void +atp_add_stroke(struct atp_softc *sc, const atp_pspan *pspan_x, + const atp_pspan *pspan_y) +{ + atp_stroke *stroke; + + if (sc->sc_n_strokes >= ATP_MAX_STROKES) + return; + stroke = &sc->sc_strokes[sc->sc_n_strokes]; + + memset(stroke, 0, sizeof(atp_stroke)); + + /* + * Strokes begin as potential touches. If a stroke survives + * longer than a threshold, or if it records significant + * cumulative movement, then it is considered a 'slide'. + */ + stroke->type = ATP_STROKE_TOUCH; + microtime(&stroke->ctime); + stroke->age = 1; /* Unit: interrupts */ + + stroke->components[X].loc = pspan_x->loc; + stroke->components[X].cum_pressure = pspan_x->cum; + stroke->components[X].max_cum_pressure = pspan_x->cum; + stroke->components[X].matched = TRUE; + + stroke->components[Y].loc = pspan_y->loc; + stroke->components[Y].cum_pressure = pspan_y->cum; + stroke->components[Y].max_cum_pressure = pspan_y->cum; + stroke->components[Y].matched = TRUE; + + sc->sc_n_strokes++; + if (sc->sc_n_strokes > 1) { + /* Reset double-tap-n-drag if we have more than one strokes. */ + sc->sc_state &= ~ATP_DOUBLE_TAP_DRAG; + } + + DPRINTFN(ATP_LLEVEL_INFO, "[%u,%u], time: %u,%ld\n", + stroke->components[X].loc, + stroke->components[Y].loc, + (unsigned int)stroke->ctime.tv_sec, + (unsigned long int)stroke->ctime.tv_usec); +} + +static void +atp_add_new_strokes(struct atp_softc *sc, atp_pspan *pspans_x, + u_int n_xpspans, atp_pspan *pspans_y, u_int n_ypspans) +{ + int i, j; + atp_pspan spans[2][ATP_MAX_PSPANS_PER_AXIS]; + u_int nspans[2]; + + /* Copy unmatched pspans into the local arrays. */ + for (i = 0, nspans[X] = 0; i < n_xpspans; i++) { + if (pspans_x[i].matched == FALSE) { + spans[X][nspans[X]] = pspans_x[i]; + nspans[X]++; + } + } + for (j = 0, nspans[Y] = 0; j < n_ypspans; j++) { + if (pspans_y[j].matched == FALSE) { + spans[Y][nspans[Y]] = pspans_y[j]; + nspans[Y]++; + } + } + + if (nspans[X] == nspans[Y]) { + /* Create new strokes from pairs of unmatched pspans */ + for (i = 0, j = 0; (i < nspans[X]) && (j < nspans[Y]); i++, j++) + atp_add_stroke(sc, &spans[X][i], &spans[Y][j]); + } else { + u_int cum = 0; + atp_axis repeat_axis; /* axis with multi-pspans */ + u_int repeat_count; /* repeat count for the multi-pspan*/ + u_int repeat_index = 0; /* index of the multi-span */ + + repeat_axis = (nspans[X] > nspans[Y]) ? Y : X; + repeat_count = abs(nspans[X] - nspans[Y]); + for (i = 0; i < nspans[repeat_axis]; i++) { + if (spans[repeat_axis][i].cum > cum) { + repeat_index = i; + cum = spans[repeat_axis][i].cum; + } + } + + /* Create new strokes from pairs of unmatched pspans */ + i = 0, j = 0; + for (; (i < nspans[X]) && (j < nspans[Y]); i++, j++) { + atp_add_stroke(sc, &spans[X][i], &spans[Y][j]); + + /* Take care to repeat at the multi-pspan. */ + if (repeat_count > 0) { + if ((repeat_axis == X) && + (repeat_index == i)) { + i--; /* counter loop increment */ + repeat_count--; + } else if ((repeat_axis == Y) && + (repeat_index == j)) { + j--; /* counter loop increment */ + repeat_count--; + } + } + } + } +} + +/* + * Advance the state of this stroke--and update the out-parameter + * 'movement' as a side-effect. + */ +void +atp_advance_stroke_state(struct atp_softc *sc, atp_stroke *stroke, + boolean_t *movement) +{ + stroke->age++; + if (stroke->age <= atp_stroke_maturity_threshold) { + /* Avoid noise from immature strokes. */ + stroke->components[X].delta_mickeys = 0; + stroke->components[Y].delta_mickeys = 0; + } + + /* Revitalize stroke if it had previously been marked as a zombie. */ + if (stroke->flags & ATSF_ZOMBIE) + stroke->flags &= ~ATSF_ZOMBIE; + + if (atp_compute_stroke_movement(stroke)) + *movement = TRUE; + + /* Convert touch strokes to slides upon detecting movement or age. */ + if (stroke->type == ATP_STROKE_TOUCH) { + struct timeval tdiff; + + /* Compute the stroke's age. */ + getmicrotime(&tdiff); + if (timevalcmp(&tdiff, &stroke->ctime, >)) + timevalsub(&tdiff, &stroke->ctime); + else { + /* + * If we are here, it is because getmicrotime + * reported the current time as being behind + * the stroke's start time; getmicrotime can + * be imprecise. + */ + tdiff.tv_sec = 0; + tdiff.tv_usec = 0; + } + + if ((tdiff.tv_sec > (atp_touch_timeout / 1000000)) || + ((tdiff.tv_sec == (atp_touch_timeout / 1000000)) && + (tdiff.tv_usec > atp_touch_timeout)) || + (stroke->cum_movement >= atp_slide_min_movement)) { + /* Switch this stroke to being a slide. */ + stroke->type = ATP_STROKE_SLIDE; + + /* Are we at the beginning of a double-click-n-drag? */ + if ((sc->sc_n_strokes == 1) && + ((sc->sc_state & ATP_ZOMBIES_EXIST) == 0) && + timevalcmp(&stroke->ctime, &sc->sc_reap_time, >)) { + struct timeval delta; + struct timeval window = { + atp_double_tap_threshold / 1000000, + atp_double_tap_threshold % 1000000 + }; + + delta = stroke->ctime; + timevalsub(&delta, &sc->sc_reap_time); + if (timevalcmp(&delta, &window, <=)) + sc->sc_state |= ATP_DOUBLE_TAP_DRAG; + } + } + } +} + +/* + * Terminate a stroke. While SLIDE strokes are dropped, TOUCH strokes + * are retained as zombies so as to reap all their siblings together; + * this helps establish the number of fingers involved in the tap. + */ +static void +atp_terminate_stroke(struct atp_softc *sc, + u_int index) /* index of the stroke to be terminated */ +{ + atp_stroke *s = &sc->sc_strokes[index]; + + if (s->flags & ATSF_ZOMBIE) { + return; + } + + if ((s->type == ATP_STROKE_TOUCH) && + (s->age > atp_stroke_maturity_threshold)) { + s->flags |= ATSF_ZOMBIE; + + /* If no zombies exist, then prepare to reap zombies later. */ + if ((sc->sc_state & ATP_ZOMBIES_EXIST) == 0) { + atp_setup_reap_time(sc, &s->ctime); + sc->sc_state |= ATP_ZOMBIES_EXIST; + } + } else { + /* Drop this stroke. */ + memcpy(&sc->sc_strokes[index], &sc->sc_strokes[index + 1], + (sc->sc_n_strokes - index - 1) * sizeof(atp_stroke)); + sc->sc_n_strokes--; + + /* + * Reset the double-click-n-drag at the termination of + * any slide stroke. + */ + sc->sc_state &= ~ATP_DOUBLE_TAP_DRAG; + } +} + +static __inline boolean_t +atp_stroke_has_small_movement(const atp_stroke *stroke) +{ + return ((abs(stroke->components[X].delta_mickeys) <= + atp_small_movement_threshold) && + (abs(stroke->components[Y].delta_mickeys) <= + atp_small_movement_threshold)); +} + +/* + * Accumulate delta_mickeys into the component's 'pending' bucket; if + * the aggregate exceeds the small_movement_threshold, then retain + * delta_mickeys for later. + */ +static __inline void +atp_update_pending_mickeys(atp_stroke_component *component) +{ + component->pending += component->delta_mickeys; + if (abs(component->pending) <= atp_small_movement_threshold) + component->delta_mickeys = 0; + else { + /* + * Penalise pending mickeys for having accumulated + * over short deltas. This operation has the effect of + * scaling down the cumulative contribution of short + * movements. + */ + component->pending -= (component->delta_mickeys << 1); + } +} + + +static void +atp_compute_smoothening_scale_ratio(atp_stroke *stroke, int *numerator, + int *denominator) +{ + int dxdt; + int dydt; + u_int vel_squared; /* Square of the velocity vector's magnitude. */ + u_int vel_squared_smooth; + + /* Table holding (10 * sqrt(x)) for x between 1 and 256. */ + static uint8_t sqrt_table[256] = { + 10, 14, 17, 20, 22, 24, 26, 28, + 30, 31, 33, 34, 36, 37, 38, 40, + 41, 42, 43, 44, 45, 46, 47, 48, + 50, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 60, 61, 62, 63, + 64, 64, 65, 66, 67, 67, 68, 69, + 70, 70, 71, 72, 72, 73, 74, 74, + 75, 76, 76, 77, 78, 78, 79, 80, + 80, 81, 81, 82, 83, 83, 84, 84, + 85, 86, 86, 87, 87, 88, 88, 89, + 90, 90, 91, 91, 92, 92, 93, 93, + 94, 94, 95, 95, 96, 96, 97, 97, + 98, 98, 99, 100, 100, 100, 101, 101, + 102, 102, 103, 103, 104, 104, 105, 105, + 106, 106, 107, 107, 108, 108, 109, 109, + 110, 110, 110, 111, 111, 112, 112, 113, + 113, 114, 114, 114, 115, 115, 116, 116, + 117, 117, 117, 118, 118, 119, 119, 120, + 120, 120, 121, 121, 122, 122, 122, 123, + 123, 124, 124, 124, 125, 125, 126, 126, + 126, 127, 127, 128, 128, 128, 129, 129, + 130, 130, 130, 131, 131, 131, 132, 132, + 133, 133, 133, 134, 134, 134, 135, 135, + 136, 136, 136, 137, 137, 137, 138, 138, + 138, 139, 139, 140, 140, 140, 141, 141, + 141, 142, 142, 142, 143, 143, 143, 144, + 144, 144, 145, 145, 145, 146, 146, 146, + 147, 147, 147, 148, 148, 148, 149, 149, + 150, 150, 150, 150, 151, 151, 151, 152, + 152, 152, 153, 153, 153, 154, 154, 154, + 155, 155, 155, 156, 156, 156, 157, 157, + 157, 158, 158, 158, 159, 159, 159, 160 + }; + const u_int N = sizeof(sqrt_table) / sizeof(sqrt_table[0]); + + dxdt = stroke->components[X].delta_mickeys; + dydt = stroke->components[Y].delta_mickeys; + + *numerator = 0, *denominator = 0; /* default values. */ + + /* Compute a smoothened magnitude_squared of the stroke's velocity. */ + vel_squared = dxdt * dxdt + dydt * dydt; + vel_squared_smooth = (3 * stroke->velocity_squared + vel_squared) >> 2; + stroke->velocity_squared = vel_squared_smooth; /* retained as history */ + if ((vel_squared == 0) || (vel_squared_smooth == 0)) + return; /* returning (numerator == 0) will imply zero movement*/ + + /* + * In order to determine the overall movement scale factor, + * we're actually interested in the effect of smoothening upon + * the *magnitude* of velocity; i.e. we need to compute the + * square-root of (vel_squared_smooth / vel_squared) in the + * form of a numerator and denominator. + */ + + /* Keep within the bounds of the square-root table. */ + while ((vel_squared > N) || (vel_squared_smooth > N)) { + /* Dividing uniformly by 2 won't disturb the final ratio. */ + vel_squared >>= 1; + vel_squared_smooth >>= 1; + } + + *numerator = sqrt_table[vel_squared_smooth - 1]; + *denominator = sqrt_table[vel_squared - 1]; +} + +/* + * Compute a smoothened value for the stroke's movement from + * delta_mickeys in the X and Y components. + */ +static boolean_t +atp_compute_stroke_movement(atp_stroke *stroke) +{ + int num; /* numerator of scale ratio */ + int denom; /* denominator of scale ratio */ + + /* + * Short movements are added first to the 'pending' bucket, + * and then acted upon only when their aggregate exceeds a + * threshold. This has the effect of filtering away movement + * noise. + */ + if (atp_stroke_has_small_movement(stroke)) { + atp_update_pending_mickeys(&stroke->components[X]); + atp_update_pending_mickeys(&stroke->components[Y]); + } else { /* large movement */ + /* clear away any pending mickeys if there are large movements*/ + stroke->components[X].pending = 0; + stroke->components[Y].pending = 0; + } + + /* Get the scale ratio and smoothen movement. */ + atp_compute_smoothening_scale_ratio(stroke, &num, &denom); + if ((num == 0) || (denom == 0)) { + stroke->components[X].movement = 0; + stroke->components[Y].movement = 0; + stroke->velocity_squared >>= 1; /* Erode velocity_squared. */ + } else { + stroke->components[X].movement = + (stroke->components[X].delta_mickeys * num) / denom; + stroke->components[Y].movement = + (stroke->components[Y].delta_mickeys * num) / denom; + + stroke->cum_movement += + abs(stroke->components[X].movement) + + abs(stroke->components[Y].movement); + } + + return ((stroke->components[X].movement != 0) || + (stroke->components[Y].movement != 0)); +} + +static __inline void +atp_setup_reap_time(struct atp_softc *sc, struct timeval *tvp) +{ + struct timeval reap_window = { + ATP_ZOMBIE_STROKE_REAP_WINDOW / 1000000, + ATP_ZOMBIE_STROKE_REAP_WINDOW % 1000000 + }; + + microtime(&sc->sc_reap_time); + timevaladd(&sc->sc_reap_time, &reap_window); + + sc->sc_reap_ctime = *tvp; /* ctime to reap */ +} + +static void +atp_reap_zombies(struct atp_softc *sc, u_int *n_reaped, u_int *reaped_xlocs) +{ + u_int i; + atp_stroke *stroke; + + *n_reaped = 0; + for (i = 0; i < sc->sc_n_strokes; i++) { + struct timeval tdiff; + + stroke = &sc->sc_strokes[i]; + + if ((stroke->flags & ATSF_ZOMBIE) == 0) + continue; + + /* Compare this stroke's ctime with the ctime being reaped. */ + if (timevalcmp(&stroke->ctime, &sc->sc_reap_ctime, >=)) { + tdiff = stroke->ctime; + timevalsub(&tdiff, &sc->sc_reap_ctime); + } else { + tdiff = sc->sc_reap_ctime; + timevalsub(&tdiff, &stroke->ctime); + } + + if ((tdiff.tv_sec > (ATP_COINCIDENCE_THRESHOLD / 1000000)) || + ((tdiff.tv_sec == (ATP_COINCIDENCE_THRESHOLD / 1000000)) && + (tdiff.tv_usec > (ATP_COINCIDENCE_THRESHOLD % 1000000)))) { + continue; /* Skip non-siblings. */ + } + + /* + * Reap this sibling zombie stroke. + */ + + if (reaped_xlocs != NULL) + reaped_xlocs[*n_reaped] = stroke->components[X].loc; + + /* Erase the stroke from the sc. */ + memcpy(&stroke[i], &stroke[i + 1], + (sc->sc_n_strokes - i - 1) * sizeof(atp_stroke)); + sc->sc_n_strokes--; + + *n_reaped += 1; + --i; /* Decr. i to keep it unchanged for the next iteration */ + } + + DPRINTFN(ATP_LLEVEL_INFO, "reaped %u zombies\n", *n_reaped); + + /* There could still be zombies remaining in the system. */ + for (i = 0; i < sc->sc_n_strokes; i++) { + stroke = &sc->sc_strokes[i]; + if (stroke->flags & ATSF_ZOMBIE) { + DPRINTFN(ATP_LLEVEL_INFO, "zombies remain!\n"); + atp_setup_reap_time(sc, &stroke->ctime); + return; + } + } + + /* If we reach here, then no more zombies remain. */ + sc->sc_state &= ~ATP_ZOMBIES_EXIST; +} + + +/* Device methods. */ +static device_probe_t atp_probe; +static device_attach_t atp_attach; +static device_detach_t atp_detach; +static usb_callback_t atp_intr; + +static const struct usb_config atp_config[ATP_N_TRANSFER] = { + [ATP_INTR_DT] = { + .type = UE_INTERRUPT, + .endpoint = UE_ADDR_ANY, + .direction = UE_DIR_IN, + .flags = { + .pipe_bof = 1, + .short_xfer_ok = 1, + }, + .bufsize = 0, /* use wMaxPacketSize */ + .callback = &atp_intr, + }, +}; + +static int +atp_probe(device_t self) +{ + struct usb_attach_arg *uaa = device_get_ivars(self); + + if (uaa->usb_mode != USB_MODE_HOST) + return (ENXIO); + + if ((uaa->info.bInterfaceClass != UICLASS_HID) || + (uaa->info.bInterfaceProtocol != UIPROTO_MOUSE)) + return (ENXIO); + + if (usbd_lookup_id_by_uaa(atp_devs, sizeof(atp_devs), uaa) == 0) + return BUS_PROBE_SPECIFIC; + else + return ENXIO; +} + +static int +atp_attach(device_t dev) +{ + struct atp_softc *sc = device_get_softc(dev); + struct usb_attach_arg *uaa = device_get_ivars(dev); + usb_error_t err; + + /* ensure that the probe was successful */ + if (uaa->driver_info >= ATP_N_DEV_PARAMS) { + DPRINTF("device probe returned bad id: %lu\n", + uaa->driver_info); + return (ENXIO); + } + DPRINTFN(ATP_LLEVEL_INFO, "sc=%p\n", sc); + + sc->sc_dev = dev; + sc->sc_usb_device = uaa->device; + + /* + * By default the touchpad behaves like an HID device, sending + * packets with reportID = 2. Such reports contain only + * limited information--they encode movement deltas and button + * events,--but do not include data from the pressure + * sensors. The device input mode can be switched from HID + * reports to raw sensor data using vendor-specific USB + * control commands; but first the mode must be read. + */ + err = atp_req_get_report(sc->sc_usb_device, sc->sc_mode_bytes); + if (err != USB_ERR_NORMAL_COMPLETION) { + DPRINTF("failed to read device mode (%d)\n", err); + return (ENXIO); + } + + if (atp_set_device_mode(dev, RAW_SENSOR_MODE) != 0) { + DPRINTF("failed to set mode to 'RAW_SENSOR' (%d)\n", err); + return (ENXIO); + } + + mtx_init(&sc->sc_mutex, "atpmtx", NULL, MTX_DEF | MTX_RECURSE); + + err = usbd_transfer_setup(uaa->device, + &uaa->info.bIfaceIndex, sc->sc_xfer, atp_config, + ATP_N_TRANSFER, sc, &sc->sc_mutex); + + if (err) { + DPRINTF("error=%s\n", usbd_errstr(err)); + goto detach; + } + + if (usb_fifo_attach(sc->sc_usb_device, sc, &sc->sc_mutex, + &atp_fifo_methods, &sc->sc_fifo, + device_get_unit(dev), 0 - 1, uaa->info.bIfaceIndex, + UID_ROOT, GID_OPERATOR, 0644)) { + goto detach; + } + + device_set_usb_desc(dev); + + sc->sc_params = &atp_dev_params[uaa->driver_info]; + + sc->sc_hw.buttons = 3; + sc->sc_hw.iftype = MOUSE_IF_USB; + sc->sc_hw.type = MOUSE_PAD; + sc->sc_hw.model = MOUSE_MODEL_GENERIC; + sc->sc_hw.hwid = 0; + sc->sc_mode.protocol = MOUSE_PROTO_MSC; + sc->sc_mode.rate = -1; + sc->sc_mode.resolution = MOUSE_RES_UNKNOWN; + sc->sc_mode.accelfactor = 0; + sc->sc_mode.level = 0; + sc->sc_mode.packetsize = MOUSE_MSC_PACKETSIZE; + sc->sc_mode.syncmask[0] = MOUSE_MSC_SYNCMASK; + sc->sc_mode.syncmask[1] = MOUSE_MSC_SYNC; + + sc->sc_state = 0; + + sc->sc_left_margin = atp_mickeys_scale_factor; + sc->sc_right_margin = (sc->sc_params->n_xsensors - 1) * + atp_mickeys_scale_factor; + + return (0); + +detach: + atp_detach(dev); + return (ENOMEM); +} + +static int +atp_detach(device_t dev) +{ + struct atp_softc *sc; + int err; + + sc = device_get_softc(dev); + if (sc->sc_state & ATP_ENABLED) { + mtx_lock(&sc->sc_mutex); + atp_disable(sc); + mtx_unlock(&sc->sc_mutex); + } + + usb_fifo_detach(&sc->sc_fifo); + + usbd_transfer_unsetup(sc->sc_xfer, ATP_N_TRANSFER); + + mtx_destroy(&sc->sc_mutex); + + err = atp_set_device_mode(dev, HID_MODE); + if (err != 0) { + DPRINTF("failed to reset mode to 'HID' (%d)\n", err); + return (err); + } + + return (0); +} + +static void +atp_intr(struct usb_xfer *xfer, usb_error_t error) +{ + struct atp_softc *sc = usbd_xfer_softc(xfer); + int len; + struct usb_page_cache *pc; + uint8_t status_bits; + atp_pspan pspans_x[ATP_MAX_PSPANS_PER_AXIS]; + atp_pspan pspans_y[ATP_MAX_PSPANS_PER_AXIS]; + u_int n_xpspans = 0, n_ypspans = 0; + u_int reaped_xlocs[ATP_MAX_STROKES]; + u_int tap_fingers = 0; + + usbd_xfer_status(xfer, &len, NULL, NULL, NULL); + + switch (USB_GET_STATE(xfer)) { + case USB_ST_TRANSFERRED: + if (len > sc->sc_params->data_len) { + DPRINTFN(ATP_LLEVEL_ERROR, + "truncating large packet from %u to %u bytes\n", + len, sc->sc_params->data_len); + len = sc->sc_params->data_len; + } + if (len == 0) + goto tr_setup; + + pc = usbd_xfer_get_frame(xfer, 0); + usbd_copy_out(pc, 0, sc->sensor_data, sc->sc_params->data_len); + + /* Interpret sensor data */ + atp_interpret_sensor_data(sc->sensor_data, + sc->sc_params->n_xsensors, 20, sc->cur_x); + atp_interpret_sensor_data(sc->sensor_data, + sc->sc_params->n_ysensors, 2, sc->cur_y); + + /* + * If this is the initial update (from an untouched + * pad), we should set the base values for the sensor + * data; deltas with respect to these base values can + * be used as pressure readings subsequently. + */ + status_bits = sc->sensor_data[sc->sc_params->data_len - 1]; + if (status_bits & ATP_STATUS_BASE_UPDATE) { + memcpy(sc->base_x, sc->cur_x, + sc->sc_params->n_xsensors * sizeof(*(sc->base_x))); + memcpy(sc->base_y, sc->cur_y, + sc->sc_params->n_ysensors * sizeof(*(sc->base_y))); + goto tr_setup; + } + + /* Get pressure readings and detect p-spans for both axes. */ + atp_get_pressures(sc->pressure_x, sc->cur_x, sc->base_x, + sc->sc_params->n_xsensors); + atp_detect_pspans(sc->pressure_x, sc->sc_params->n_xsensors, + ATP_MAX_PSPANS_PER_AXIS, + pspans_x, &n_xpspans); + atp_get_pressures(sc->pressure_y, sc->cur_y, sc->base_y, + sc->sc_params->n_ysensors); + atp_detect_pspans(sc->pressure_y, sc->sc_params->n_ysensors, + ATP_MAX_PSPANS_PER_AXIS, + pspans_y, &n_ypspans); + + /* Update strokes with new pspans to detect movements. */ + sc->sc_status.flags &= ~MOUSE_POSCHANGED; + if (atp_update_strokes(sc, + pspans_x, n_xpspans, + pspans_y, n_ypspans)) + sc->sc_status.flags |= MOUSE_POSCHANGED; + + /* Reap zombies if it is time. */ + if (sc->sc_state & ATP_ZOMBIES_EXIST) { + struct timeval now; + + getmicrotime(&now); + if (timevalcmp(&now, &sc->sc_reap_time, >=)) + atp_reap_zombies(sc, &tap_fingers, + reaped_xlocs); + } + + sc->sc_status.flags &= ~MOUSE_STDBUTTONSCHANGED; + sc->sc_status.obutton = sc->sc_status.button; + + /* Get the state of the physical buttton. */ + sc->sc_status.button = (status_bits & ATP_STATUS_BUTTON) ? + MOUSE_BUTTON1DOWN : 0; + if (sc->sc_status.button != 0) { + /* Reset DOUBLE_TAP_N_DRAG if the button is pressed. */ + sc->sc_state &= ~ATP_DOUBLE_TAP_DRAG; + } else if (sc->sc_state & ATP_DOUBLE_TAP_DRAG) { + /* Assume a button-press with DOUBLE_TAP_N_DRAG. */ + sc->sc_status.button = MOUSE_BUTTON1DOWN; + } + + sc->sc_status.flags |= + sc->sc_status.button ^ sc->sc_status.obutton; + if (sc->sc_status.flags & MOUSE_STDBUTTONSCHANGED) { + DPRINTFN(ATP_LLEVEL_INFO, "button %s\n", + ((sc->sc_status.button & MOUSE_BUTTON1DOWN) ? + "pressed" : "released")); + } else if ((sc->sc_status.obutton == 0) && + (sc->sc_status.button == 0) && + (tap_fingers != 0)) { + /* Ignore single-finger taps at the edges. */ + if ((tap_fingers == 1) && + ((reaped_xlocs[0] <= sc->sc_left_margin) || + (reaped_xlocs[0] > sc->sc_right_margin))) { + tap_fingers = 0; + } + DPRINTFN(ATP_LLEVEL_INFO, + "tap_fingers: %u\n", tap_fingers); + } + + if (sc->sc_status.flags & + (MOUSE_POSCHANGED | MOUSE_STDBUTTONSCHANGED)) { + int dx, dy; + u_int n_movements; + + dx = 0, dy = 0, n_movements = 0; + for (u_int i = 0; i < sc->sc_n_strokes; i++) { + atp_stroke *stroke = &sc->sc_strokes[i]; + + if ((stroke->components[X].movement) || + (stroke->components[Y].movement)) { + dx += stroke->components[X].movement; + dy += stroke->components[Y].movement; + n_movements++; + } + } + /* + * Disregard movement if multiple + * strokes record motion. + */ + if (n_movements != 1) + dx = 0, dy = 0; + + sc->sc_status.dx += dx; + sc->sc_status.dy += dy; + atp_add_to_queue(sc, dx, -dy, sc->sc_status.button); + } + + if (tap_fingers != 0) { + /* Add a pair of events (button-down and button-up). */ + switch (tap_fingers) { + case 1: atp_add_to_queue(sc, 0, 0, MOUSE_BUTTON1DOWN); + break; + case 2: atp_add_to_queue(sc, 0, 0, MOUSE_BUTTON2DOWN); + break; + case 3: atp_add_to_queue(sc, 0, 0, MOUSE_BUTTON3DOWN); + break; + default: break;/* handle taps of only up to 3 fingers */ + } + atp_add_to_queue(sc, 0, 0, 0); /* button release */ + } + + /* + * The device continues to trigger interrupts at a + * fast rate even after touchpad activity has + * stopped. Upon detecting that the device has + * remained idle beyond a threshold, we reinitialize + * it to silence the interrupts. + */ + if ((sc->sc_status.flags == 0) && + (sc->sc_n_strokes == 0) && + (sc->sc_status.button == 0)) { + sc->sc_idlecount++; + if (sc->sc_idlecount >= ATP_IDLENESS_THRESHOLD) { + DPRINTFN(ATP_LLEVEL_INFO, "idle\n"); + atp_set_device_mode(sc->sc_dev,RAW_SENSOR_MODE); + sc->sc_idlecount = 0; + } + } else { + sc->sc_idlecount = 0; + } + + case USB_ST_SETUP: + tr_setup: + /* check if we can put more data into the FIFO */ + if (usb_fifo_put_bytes_max( + sc->sc_fifo.fp[USB_FIFO_RX]) != 0) { + usbd_xfer_set_frame_len(xfer, 0, + usbd_xfer_max_len(xfer)); + usbd_transfer_submit(xfer); + } + break; + + default: /* Error */ + if (error != USB_ERR_CANCELLED) { + /* try clear stall first */ + usbd_xfer_set_stall(xfer); + goto tr_setup; + } + break; + } + + return; +} + +static void +atp_add_to_queue(struct atp_softc *sc, int dx, int dy, uint32_t buttons_in) +{ + uint32_t buttons_out; + uint8_t buf[8]; + + dx = imin(dx, 254); dx = imax(dx, -256); + dy = imin(dy, 254); dy = imax(dy, -256); + + buttons_out = MOUSE_MSC_BUTTONS; + if (buttons_in & MOUSE_BUTTON1DOWN) + buttons_out &= ~MOUSE_MSC_BUTTON1UP; + else if (buttons_in & MOUSE_BUTTON2DOWN) + buttons_out &= ~MOUSE_MSC_BUTTON2UP; + else if (buttons_in & MOUSE_BUTTON3DOWN) + buttons_out &= ~MOUSE_MSC_BUTTON3UP; + + DPRINTFN(ATP_LLEVEL_INFO, "dx=%d, dy=%d, buttons=%x\n", + dx, dy, buttons_out); + + /* Encode the mouse data in standard format; refer to mouse(4) */ + buf[0] = sc->sc_mode.syncmask[1]; + buf[0] |= buttons_out; + buf[1] = dx >> 1; + buf[2] = dy >> 1; + buf[3] = dx - (dx >> 1); + buf[4] = dy - (dy >> 1); + /* Encode extra bytes for level 1 */ + if (sc->sc_mode.level == 1) { + buf[5] = 0; /* dz */ + buf[6] = 0; /* dz - (dz / 2) */ + buf[7] = MOUSE_SYS_EXTBUTTONS; /* Extra buttons all up. */ + } + + usb_fifo_put_data_linear(sc->sc_fifo.fp[USB_FIFO_RX], buf, + sc->sc_mode.packetsize, 1); +} + +static void +atp_reset_buf(struct atp_softc *sc) +{ + /* reset read queue */ + usb_fifo_reset(sc->sc_fifo.fp[USB_FIFO_RX]); +} + +static void +atp_start_read(struct usb_fifo *fifo) +{ + struct atp_softc *sc = usb_fifo_softc(fifo); + int rate; + + /* Check if we should override the default polling interval */ + rate = sc->sc_pollrate; + /* Range check rate */ + if (rate > 1000) + rate = 1000; + /* Check for set rate */ + if ((rate > 0) && (sc->sc_xfer[ATP_INTR_DT] != NULL)) { + /* Stop current transfer, if any */ + usbd_transfer_stop(sc->sc_xfer[ATP_INTR_DT]); + /* Set new interval */ + usbd_xfer_set_interval(sc->sc_xfer[ATP_INTR_DT], 1000 / rate); + /* Only set pollrate once */ + sc->sc_pollrate = 0; + } + + usbd_transfer_start(sc->sc_xfer[ATP_INTR_DT]); +} + +static void +atp_stop_read(struct usb_fifo *fifo) +{ + struct atp_softc *sc = usb_fifo_softc(fifo); + + usbd_transfer_stop(sc->sc_xfer[ATP_INTR_DT]); +} + + +static int +atp_open(struct usb_fifo *fifo, int fflags) +{ + DPRINTFN(ATP_LLEVEL_INFO, "\n"); + + if (fflags & FREAD) { + struct atp_softc *sc = usb_fifo_softc(fifo); + int rc; + + if (sc->sc_state & ATP_ENABLED) + return (EBUSY); + + if (usb_fifo_alloc_buffer(fifo, + ATP_FIFO_BUF_SIZE, ATP_FIFO_QUEUE_MAXLEN)) { + return (ENOMEM); + } + + rc = atp_enable(sc); + if (rc != 0) { + usb_fifo_free_buffer(fifo); + return (rc); + } + } + + return (0); +} + +static void +atp_close(struct usb_fifo *fifo, int fflags) +{ + if (fflags & FREAD) { + struct atp_softc *sc = usb_fifo_softc(fifo); + + atp_disable(sc); + usb_fifo_free_buffer(fifo); + } +} + +int +atp_ioctl(struct usb_fifo *fifo, u_long cmd, void *addr, int fflags) +{ + struct atp_softc *sc = usb_fifo_softc(fifo); + mousemode_t mode; + int error = 0; + + mtx_lock(&sc->sc_mutex); + + switch(cmd) { + case MOUSE_GETHWINFO: + *(mousehw_t *)addr = sc->sc_hw; + break; + case MOUSE_GETMODE: + *(mousemode_t *)addr = sc->sc_mode; + break; + case MOUSE_SETMODE: + mode = *(mousemode_t *)addr; + + if (mode.level == -1) + /* Don't change the current setting */ + ; + else if ((mode.level < 0) || (mode.level > 1)) { + error = EINVAL; + goto done; + } + sc->sc_mode.level = mode.level; + sc->sc_pollrate = mode.rate; + sc->sc_hw.buttons = 3; + + if (sc->sc_mode.level == 0) { + sc->sc_mode.protocol = MOUSE_PROTO_MSC; + sc->sc_mode.packetsize = MOUSE_MSC_PACKETSIZE; + sc->sc_mode.syncmask[0] = MOUSE_MSC_SYNCMASK; + sc->sc_mode.syncmask[1] = MOUSE_MSC_SYNC; + } else if (sc->sc_mode.level == 1) { + sc->sc_mode.protocol = MOUSE_PROTO_SYSMOUSE; + sc->sc_mode.packetsize = MOUSE_SYS_PACKETSIZE; + sc->sc_mode.syncmask[0] = MOUSE_SYS_SYNCMASK; + sc->sc_mode.syncmask[1] = MOUSE_SYS_SYNC; + } + atp_reset_buf(sc); + break; + case MOUSE_GETLEVEL: + *(int *)addr = sc->sc_mode.level; + break; + case MOUSE_SETLEVEL: + if (*(int *)addr < 0 || *(int *)addr > 1) { + error = EINVAL; + goto done; + } + sc->sc_mode.level = *(int *)addr; + sc->sc_hw.buttons = 3; + + if (sc->sc_mode.level == 0) { + sc->sc_mode.protocol = MOUSE_PROTO_MSC; + sc->sc_mode.packetsize = MOUSE_MSC_PACKETSIZE; + sc->sc_mode.syncmask[0] = MOUSE_MSC_SYNCMASK; + sc->sc_mode.syncmask[1] = MOUSE_MSC_SYNC; + } else if (sc->sc_mode.level == 1) { + sc->sc_mode.protocol = MOUSE_PROTO_SYSMOUSE; + sc->sc_mode.packetsize = MOUSE_SYS_PACKETSIZE; + sc->sc_mode.syncmask[0] = MOUSE_SYS_SYNCMASK; + sc->sc_mode.syncmask[1] = MOUSE_SYS_SYNC; + } + atp_reset_buf(sc); + break; + case MOUSE_GETSTATUS: { + mousestatus_t *status = (mousestatus_t *)addr; + + *status = sc->sc_status; + sc->sc_status.obutton = sc->sc_status.button; + sc->sc_status.button = 0; + sc->sc_status.dx = 0; + sc->sc_status.dy = 0; + sc->sc_status.dz = 0; + + if (status->dx || status->dy || status->dz) + status->flags |= MOUSE_POSCHANGED; + if (status->button != status->obutton) + status->flags |= MOUSE_BUTTONSCHANGED; + break; + } + default: + error = ENOTTY; + } + +done: + mtx_unlock(&sc->sc_mutex); + return (error); +} + +static int +atp_sysctl_scale_factor_handler(SYSCTL_HANDLER_ARGS) +{ + int error; + u_int tmp; + u_int prev_mickeys_scale_factor; + + prev_mickeys_scale_factor = atp_mickeys_scale_factor; + + tmp = atp_mickeys_scale_factor; + error = sysctl_handle_int(oidp, &tmp, 0, req); + if (error != 0 || req->newptr == NULL) + return (error); + + if (tmp == prev_mickeys_scale_factor) + return (0); /* no change */ + + atp_mickeys_scale_factor = tmp; + DPRINTFN(ATP_LLEVEL_INFO, "%s: resetting mickeys_scale_factor to %u\n", + ATP_DRIVER_NAME, tmp); + + /* Update dependent thresholds. */ + if (atp_small_movement_threshold == (prev_mickeys_scale_factor >> 3)) + atp_small_movement_threshold = atp_mickeys_scale_factor >> 3; + if (atp_max_delta_mickeys == ((3 * prev_mickeys_scale_factor) >> 1)) + atp_max_delta_mickeys = ((3 * atp_mickeys_scale_factor) >>1); + if (atp_slide_min_movement == (prev_mickeys_scale_factor >> 3)) + atp_slide_min_movement = atp_mickeys_scale_factor >> 3; + + return (0); +} + +static device_method_t atp_methods[] = { + /* Device interface */ + DEVMETHOD(device_probe, atp_probe), + DEVMETHOD(device_attach, atp_attach), + DEVMETHOD(device_detach, atp_detach), + { 0, 0 } +}; + +static driver_t atp_driver = { + ATP_DRIVER_NAME, + atp_methods, + sizeof(struct atp_softc) +}; + +static devclass_t atp_devclass; + +DRIVER_MODULE(atp, uhub, atp_driver, atp_devclass, NULL, 0); +MODULE_DEPEND(atp, usb, 1, 1, 1); diff --git a/sys/modules/usb/Makefile b/sys/modules/usb/Makefile index 2555b83..8c771d7 100644 --- a/sys/modules/usb/Makefile +++ b/sys/modules/usb/Makefile @@ -28,7 +28,7 @@ SUBDIR = usb SUBDIR += ehci musb ohci uhci uss820dci ${_at91dci} ${_atmegadci} SUBDIR += rum uath upgt ural zyd ${_urtw} -SUBDIR += uhid ukbd ums udbp ufm +SUBDIR += atp uhid ukbd ums udbp ufm SUBDIR += ucom u3g uark ubsa ubser uchcom ucycom ufoma uftdi ugensa uipaq ulpt \ umct umodem umoscom uplcom uslcom uvisor uvscom SUBDIR += uether aue axe cdce cue kue rue udav diff --git a/sys/modules/usb/atp/Makefile b/sys/modules/usb/atp/Makefile new file mode 100644 index 0000000..8e68d1c --- /dev/null +++ b/sys/modules/usb/atp/Makefile @@ -0,0 +1,11 @@ +# $FreeBSD$ + +S= ${.CURDIR}/../../.. + +.PATH: $S/dev/usb/input + +KMOD= atp +SRCS= opt_bus.h opt_usb.h device_if.h bus_if.h usb_if.h vnode_if.h usbdevs.h \ + atp.c + +.include -- cgit v1.1