Lets not artificially cap the magnitude in the device specific implementation. Since the default magnitude for the common rumble feedback was set to 0.5 in the previous commit the effective magnitude in existing themes remains unchanged for that event. The pattern feedbacks didn't make it into a release yet so there's nothing that could get broken. The change doubles the effective when using the Vibra DBus API but as it seems that libmanette didn't land support yet lets just ignore that here. Periodic feedback is unaffected as it uses a different internal API. Signed-off-by: Guido Günther <agx@sigxcpu.org>
396 lines
9.8 KiB
C
396 lines
9.8 KiB
C
/*
|
|
* Copyright (C) 2020 Purism SPC
|
|
* SPDX-License-Identifier: GPL-3.0+
|
|
* Author: Guido Günther <agx@sigxcpu.org>
|
|
*
|
|
* See https://www.kernel.org/doc/html/latest/input/ff.html
|
|
* and fftest.c from the joystick package.
|
|
*/
|
|
|
|
#define G_LOG_DOMAIN "fbd-dev-vibra"
|
|
|
|
#include "fbd-dev-vibra.h"
|
|
|
|
#include <gio/gio.h>
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <linux/input.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
/**
|
|
* SECTION:fbd-dev-vibra
|
|
* @short_description: Haptic motor device interface
|
|
* @Title: FbdDevVibra
|
|
*
|
|
* The #FbdDevVibra is used to interface with haptic motor via the force
|
|
* feedback interface. It currently only supports one id at a time.
|
|
*/
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_DEVICE,
|
|
PROP_LAST_PROP,
|
|
};
|
|
static GParamSpec *props[PROP_LAST_PROP];
|
|
|
|
typedef enum {
|
|
FBD_DEV_VIBRA_FEATURE_RUMBLE,
|
|
FBD_DEV_VIBRA_FEATURE_PERIODIC,
|
|
FBD_DEV_VIBRA_FEATURE_GAIN,
|
|
} FbdDevVibraFeatureFlags;
|
|
|
|
typedef struct _FbdDevVibra {
|
|
GObject parent;
|
|
|
|
GUdevDevice *device;
|
|
gint fd;
|
|
gint id; /* currently used id */
|
|
|
|
FbdDevVibraFeatureFlags features;
|
|
} FbdDevVibra;
|
|
|
|
static void initable_iface_init (GInitableIface *iface);
|
|
|
|
G_DEFINE_TYPE_WITH_CODE (FbdDevVibra, fbd_dev_vibra, G_TYPE_OBJECT,
|
|
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init));
|
|
|
|
static void
|
|
fbd_dev_vibra_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
FbdDevVibra *self = FBD_DEV_VIBRA (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_DEVICE:
|
|
g_clear_object (&self->device);
|
|
self->device = g_value_dup_object (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
fbd_dev_vibra_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (property_id) {
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#define BITS_PER_LONG (8 * sizeof (long))
|
|
|
|
#define HAS_FEATURE(fbit, a) \
|
|
(a[(fbit/BITS_PER_LONG)] >> ((fbit % BITS_PER_LONG)) & 1)
|
|
|
|
static gboolean
|
|
initable_init (GInitable *initable,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
FbdDevVibra *self = FBD_DEV_VIBRA (initable);
|
|
const char *filename = g_udev_device_get_device_file (self->device);
|
|
gulong features[1 + FF_MAX/BITS_PER_LONG];
|
|
struct input_event gain = { 0 };
|
|
|
|
self->fd = open (filename, O_RDWR | O_NONBLOCK, O_RDWR);
|
|
if (self->fd < 0) {
|
|
g_set_error (error,
|
|
G_FILE_ERROR, G_FILE_ERROR_FAILED,
|
|
"Unable to open '%s': %s",
|
|
filename, g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (ioctl (self->fd, EVIOCGBIT(EV_FF, sizeof (features)), features) == -1) {
|
|
g_set_error (error,
|
|
G_FILE_ERROR, G_FILE_ERROR_FAILED,
|
|
"Unable to probe features of '%s': %s",
|
|
filename, g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (HAS_FEATURE(FF_RUMBLE, features))
|
|
self->features |= FBD_DEV_VIBRA_FEATURE_RUMBLE;
|
|
else {
|
|
g_set_error (error,
|
|
G_FILE_ERROR, G_FILE_ERROR_FAILED,
|
|
"No rumble capable vibra device “%s”: %s",
|
|
filename, g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (HAS_FEATURE(FF_PERIODIC, features))
|
|
self->features |= FBD_DEV_VIBRA_FEATURE_PERIODIC;
|
|
else {
|
|
g_set_error (error,
|
|
G_FILE_ERROR, G_FILE_ERROR_FAILED,
|
|
"No rumble capable vibra device “%s”: %s",
|
|
filename, g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
/* Set gain to 75% if supported */
|
|
if (HAS_FEATURE(FF_GAIN, features)) {
|
|
self->features |= FBD_DEV_VIBRA_FEATURE_GAIN;
|
|
memset(&gain, 0, sizeof(gain));
|
|
gain.type = EV_FF;
|
|
gain.code = FF_GAIN;
|
|
gain.value = 0xC000; /* [0, 0xFFFF]) */
|
|
|
|
g_debug("Setting master gain to 75%%");
|
|
if (write(self->fd, &gain, sizeof(gain)) != sizeof(gain)) {
|
|
g_set_error (error,
|
|
G_FILE_ERROR, G_FILE_ERROR_FAILED,
|
|
"Unable to set gain of '%s': %s",
|
|
filename, g_strerror (errno));
|
|
}
|
|
} else {
|
|
g_debug ("Gain unsupported");
|
|
}
|
|
|
|
g_debug ("Vibra device at '%s' usable", filename);
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
initable_iface_init (GInitableIface *iface)
|
|
{
|
|
iface->init = initable_init;
|
|
}
|
|
|
|
static void
|
|
fbd_dev_vibra_dispose (GObject *object)
|
|
{
|
|
FbdDevVibra *self = FBD_DEV_VIBRA (object);
|
|
|
|
g_clear_object (&self->device);
|
|
|
|
G_OBJECT_CLASS (fbd_dev_vibra_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
fbd_dev_vibra_finalize (GObject *object)
|
|
{
|
|
FbdDevVibra *self = FBD_DEV_VIBRA (object);
|
|
|
|
if (self->fd >= 0) {
|
|
close (self->fd);
|
|
self->fd = -1;
|
|
}
|
|
|
|
G_OBJECT_CLASS (fbd_dev_vibra_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
fbd_dev_vibra_class_init (FbdDevVibraClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->set_property = fbd_dev_vibra_set_property;
|
|
object_class->get_property = fbd_dev_vibra_get_property;
|
|
|
|
object_class->dispose = fbd_dev_vibra_dispose;
|
|
object_class->finalize = fbd_dev_vibra_finalize;
|
|
|
|
props[PROP_DEVICE] =
|
|
g_param_spec_object (
|
|
"device",
|
|
"Device",
|
|
"The udev device",
|
|
G_UDEV_TYPE_DEVICE,
|
|
G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
|
|
|
|
g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
|
|
}
|
|
|
|
static void
|
|
fbd_dev_vibra_init (FbdDevVibra *self)
|
|
{
|
|
self->id = -1;
|
|
}
|
|
|
|
FbdDevVibra *
|
|
fbd_dev_vibra_new (GUdevDevice *device, GError **error)
|
|
{
|
|
return FBD_DEV_VIBRA (g_initable_new (FBD_TYPE_DEV_VIBRA,
|
|
NULL,
|
|
error,
|
|
"device", device,
|
|
NULL));
|
|
}
|
|
|
|
gboolean
|
|
fbd_dev_vibra_rumble (FbdDevVibra *self, double magnitude, guint duration, gboolean upload)
|
|
{
|
|
struct input_event event = { 0 };
|
|
struct ff_effect effect = { 0 };
|
|
|
|
g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE);
|
|
|
|
memset(&effect, 0, sizeof(effect));
|
|
effect.type = FF_RUMBLE;
|
|
effect.id = -1;
|
|
effect.u.rumble.strong_magnitude = 0xFFFF * magnitude;
|
|
effect.u.rumble.weak_magnitude = 0;
|
|
effect.replay.length = duration;
|
|
effect.replay.delay = 0;
|
|
|
|
if (upload) {
|
|
g_debug("Uploading rumbling vibra effect (%d)", self->fd);
|
|
if (ioctl(self->fd, EVIOCSFF, &effect) == -1) {
|
|
g_warning ("Failed to upload rumbling vibra effect: %s", g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
self->id = effect.id;
|
|
}
|
|
|
|
g_debug("Playing rumbling vibra effect id %d", effect.id);
|
|
event.type = EV_FF;
|
|
event.value = 1;
|
|
event.code = self->id;
|
|
|
|
if (write (self->fd, (const void*) &event, sizeof (event)) < 0) {
|
|
g_warning ("Failed to play rumbling vibra effect.");
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* TODO: fall back to multiple rumbles when sine not supported */
|
|
gboolean
|
|
fbd_dev_vibra_periodic (FbdDevVibra *self, guint duration, guint magnitude,
|
|
guint fade_in_level, guint fade_in_time)
|
|
{
|
|
struct input_event event;
|
|
struct ff_effect effect = { 0 };
|
|
|
|
g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE);
|
|
|
|
if (!magnitude)
|
|
magnitude = 0x7FFF;
|
|
|
|
if (!fade_in_level)
|
|
fade_in_level = magnitude;
|
|
|
|
if (!fade_in_time)
|
|
fade_in_time = duration;
|
|
|
|
effect.type = FF_PERIODIC;
|
|
effect.id = -1;
|
|
effect.u.periodic.waveform = FF_SINE;
|
|
effect.u.periodic.period = 10;
|
|
effect.u.periodic.magnitude = magnitude;
|
|
effect.u.periodic.offset = 0;
|
|
effect.u.periodic.phase = 0;
|
|
effect.direction = 0x4000;
|
|
effect.u.periodic.envelope.attack_length = fade_in_time;
|
|
effect.u.periodic.envelope.attack_level = fade_in_level;;
|
|
effect.u.periodic.envelope.fade_length = 0;
|
|
effect.u.periodic.envelope.fade_level = 0;
|
|
effect.trigger.button = 0;
|
|
effect.trigger.interval = 0;
|
|
effect.replay.length = duration;
|
|
effect.replay.delay = 200;
|
|
|
|
g_debug("Uploading periodic effect (%d)", self->fd);
|
|
if (ioctl(self->fd, EVIOCSFF, &effect) == -1) {
|
|
g_warning ("Failed to upload periodic vibra effect: %s", g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
g_debug("Playing periodic vibra effect id %d", effect.id);
|
|
event.type = EV_FF;
|
|
self->id = event.code = effect.id;
|
|
event.value = 1;
|
|
|
|
if (write (self->fd, (const void*) &event, sizeof (event)) < 0) {
|
|
g_warning ("Failed to play rumbling effect.");
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
gboolean
|
|
fbd_dev_vibra_remove_effect (FbdDevVibra *self)
|
|
{
|
|
g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE);
|
|
|
|
if (self->id == -1)
|
|
return TRUE;
|
|
|
|
g_debug("Erasing vibra effect (%d)", self->fd);
|
|
if (ioctl(self->fd, EVIOCRMFF, self->id) == -1) {
|
|
g_warning ("Failed to erase vibra effect with id %d: %s", self->id, strerror(errno));
|
|
self->id = -1;
|
|
return FALSE;
|
|
}
|
|
self->id = -1;
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
gboolean
|
|
fbd_dev_vibra_stop (FbdDevVibra *self)
|
|
{
|
|
struct input_event stop = { 0 };
|
|
|
|
g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE);
|
|
|
|
if (self->id == -1)
|
|
return TRUE;
|
|
|
|
stop.type = EV_FF;
|
|
stop.code = self->id;
|
|
stop.value = 0;
|
|
|
|
if (write(self->fd, (const void*) &stop, sizeof(stop)) < 0) {
|
|
g_warning ("Failed to stop vibra effect with id %d: %s", self->id, strerror(errno));
|
|
return FALSE;
|
|
}
|
|
|
|
return fbd_dev_vibra_remove_effect (self);
|
|
}
|
|
|
|
GUdevDevice *
|
|
fbd_dev_vibra_get_device (FbdDevVibra *self)
|
|
{
|
|
g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE);
|
|
|
|
return self->device;
|
|
}
|
|
|
|
/**
|
|
* fbd_dev_vibra_is_busy:
|
|
* @self: The vibra device
|
|
*
|
|
* Check whether the device is currently in used
|
|
*
|
|
* Returns: `TRUE` when the device is in use, otherwise `FALSE`
|
|
*/
|
|
gboolean
|
|
fbd_dev_vibra_is_busy (FbdDevVibra *self)
|
|
{
|
|
if (self == NULL)
|
|
return FALSE;
|
|
|
|
g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), TRUE);
|
|
|
|
return self->id != -1;
|
|
}
|