⛏️ index : haiku.git

/*
 * Copyright 2020, Jérôme Duval, jerome.duval@gmail.com.
 * Copyright 2008-2011, Michael Lotz <mmlr@mlotz.ch>
 * Copyright 2020, 2022 Vladimir Kondratyev <wulf@FreeBSD.org>
 * Copyright 2023 Vladimir Serbinenko <phcoder@gmail.com>
 * Distributed under the terms of the MIT license.
 */


//!	Driver for I2C Elan Devices.
// Partially based on FreeBSD ietp driver


#include "Driver.h"
#include "ELANDevice.h"
#include "HIDReport.h"
#include <keyboard_mouse_driver.h>
#include <kernel.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <new>

#define LO_ELAN_REPORT_SIZE 32
#define HI_ELAN_REPORT_SIZE 37
#define MIN_ELAN_REPORT  11

#define	IETP_MAX_X_AXIS		0x0106
#define	IETP_MAX_Y_AXIS		0x0107

#define	IETP_CONTROL		0x0300
#define	IETP_CTRL_ABSOLUTE	0x0001
#define	IETP_CTRL_STANDARD	0x0000

ELANDevice::ELANDevice(device_node* parent, i2c_device_interface* i2c,
	i2c_device i2cCookie)
	:	fStatus(B_NO_INIT),
		fTransferLastschedule(0),
		fTransferScheduled(0),
		fOpenCount(0),
		fRemoved(false),
		fPublishPath(nullptr),
		fReportID(0x5d),
		fHighPrecision(false),
		fParent(parent),
		fI2C(i2c),
		fI2CCookie(i2cCookie),
		fLastButtons(0),
 		fClickCount(0),
		fLastClickTime(0),
		fClickSpeed(250000),
		fReportStatus(B_NO_INIT),
		fCurrentReportLength(0),
		fBusyCount(0)
{
	fConditionVariable.Init(this, "elan report");

	uint16 descriptorAddress = 1;
	// fetch HID descriptor
	CALLED();
	fStatus = _FetchBuffer((uint8*)&descriptorAddress,
		sizeof(descriptorAddress), &fDescriptor, sizeof(fDescriptor));
	if (fStatus != B_OK) {
		ERROR("failed to fetch HID descriptor\n");
		return;
	}

	if (_SetAbsoluteMode(true) != B_OK) {
		TRACE_ALWAYS("failed to set absolute mode\n");
		return;
	}

	fHardwareSpecs.areaStartX = 0;
	fHardwareSpecs.areaStartY = 0;

	uint16_t buf;

	if (_ReadRegister(IETP_MAX_X_AXIS, sizeof(buf), &buf) != B_OK) {
		TRACE_ALWAYS("failed reading max x\n");
		return;
	}
	fHardwareSpecs.areaEndX = buf;

	if (_ReadRegister(IETP_MAX_Y_AXIS, sizeof(buf), &buf) != B_OK) {
		TRACE_ALWAYS("failed reading max y\n");
		return;
	}
	fHardwareSpecs.areaEndY = buf;

	fHardwareSpecs.edgeMotionWidth = 55;

	fHardwareSpecs.minPressure = 0;
	fHardwareSpecs.realMaxPressure = 50;
	fHardwareSpecs.maxPressure = 255;

	TRACE("Dimensions %dx%d\n", fHardwareSpecs.areaEndX, fHardwareSpecs.areaEndY);

	fStatus = B_OK;
}


ELANDevice::~ELANDevice()
{
}


status_t
ELANDevice::Open(uint32 flags)
{
	atomic_add(&fOpenCount, 1);
	_Reset();

	return B_OK;
}


status_t
ELANDevice::Close()
{
	atomic_add(&fOpenCount, -1);
	_SetPower(I2C_HID_POWER_OFF);

	return B_OK;
}


void
ELANDevice::Removed()
{
	fRemoved = true;
}


status_t
ELANDevice::_MaybeScheduleTransfer(int type, int id, int reportSize)
{
	if (fRemoved)
		return ENODEV;

	if (atomic_get_and_set(&fTransferScheduled, 1) != 0) {
		// someone else already caused a transfer to be scheduled
		return B_OK;
	}

	snooze_until(fTransferLastschedule, B_SYSTEM_TIMEBASE);
	fTransferLastschedule = system_time() + 10000;

	TRACE("scheduling interrupt transfer of %u bytes\n",
		reportSize);
	return _FetchReport(type, id, reportSize);
}

void
ELANDevice::SetPublishPath(char *publishPath)
{
	free(fPublishPath);
	fPublishPath = publishPath;
}

status_t
ELANDevice::Control(uint32 op, void *buffer,
	size_t length)
{
	switch (op) {

		case B_GET_DEVICE_NAME:
		{
			if (!IS_USER_ADDRESS(buffer))
				return B_BAD_ADDRESS;

			if (user_strlcpy((char *)buffer, "Elantech I2C touchpad", length) > 0)
				return B_OK;

			return B_ERROR;
		}

		case MS_IS_TOUCHPAD:
			TRACE("ELANTECH: MS_IS_TOUCHPAD\n");
			if (buffer == NULL)
				return B_OK;
			return user_memcpy(buffer, &fHardwareSpecs, sizeof(fHardwareSpecs));

		case MS_READ_TOUCHPAD:
		{
			touchpad_read read;
			int zero_report_count = 0;
			if (length < sizeof(touchpad_read))
				return B_BUFFER_OVERFLOW;

			if (user_memcpy(&read.timeout, &(((touchpad_read*)buffer)->timeout),
					sizeof(bigtime_t)) != B_OK)
				return B_BAD_ADDRESS;

			read.event = MS_READ_TOUCHPAD;

			while (true) {
				status_t result = _ReadAndParseReport(
					&read.u.touchpad, read.timeout,
					zero_report_count);
				if (result == B_INTERRUPTED)
					return result;
				if (result == B_BUSY)
					continue;

				if (!IS_USER_ADDRESS(buffer)
					|| user_memcpy(buffer, &read, sizeof(read))
						!= B_OK) {
					return B_BAD_ADDRESS;
				}

				TRACE("Returning MS_READ_TOUCHPAD: %x\n", result);

				return result;
			}
		}
	}

	return B_ERROR;
}


status_t
ELANDevice::_SetAbsoluteMode(bool enable)
{
	return _WriteRegister(IETP_CONTROL, enable ? IETP_CTRL_ABSOLUTE : IETP_CTRL_STANDARD);
}


status_t
ELANDevice::_WaitForReport(bigtime_t timeout)
{
	CALLED();
	while (atomic_get(&fBusyCount) != 0)
		snooze(1000);

	ConditionVariableEntry conditionVariableEntry;
	fConditionVariable.Add(&conditionVariableEntry);
	TRACE("Starting report wait\n");
	status_t result = _MaybeScheduleTransfer(
		HID_REPORT_TYPE_INPUT, fReportID,
		fHighPrecision ? HI_ELAN_REPORT_SIZE : LO_ELAN_REPORT_SIZE);
	if (result != B_OK) {
		TRACE_ALWAYS("scheduling transfer failed\n");
		conditionVariableEntry.Wait(B_RELATIVE_TIMEOUT, 0);
		return result;
	}

	result = conditionVariableEntry.Wait(B_RELATIVE_TIMEOUT, timeout);
	if (result != B_OK)
		return result;

	if (fReportStatus != B_OK)
		return fReportStatus;

	atomic_add(&fBusyCount, 1);
	return B_OK;
}


status_t
ELANDevice::_ReadAndParseReport(touchpad_movement *info, bigtime_t timeout, int &zero_report_count)
{
	CALLED();
	status_t result = _WaitForReport(timeout);
	if (result != B_OK) {
		if (IsRemoved()) {
			TRACE("device has been removed\n");
			return B_DEV_NOT_READY;
		}

		if (result == B_INTERRUPTED)
			return result;

		if (result != B_BUSY) {
			// "busy" happens when other reports come in on the same input as ours
			TRACE_ALWAYS("error waiting for report: %s\n", strerror(result));
		}

		if (result == B_TIMED_OUT)
			return result;

		// signal that we simply want to try again
		return B_BUSY;
	}

	if (fCurrentReportLength == 0 && ++zero_report_count < 3) {
		atomic_add(&fBusyCount, -1);
		return B_BUSY;
	}

	uint8 report_copy[TRANSFER_BUFFER_SIZE];
	memset(report_copy, 0, TRANSFER_BUFFER_SIZE);
	memcpy(report_copy, fCurrentReport, MIN(TRANSFER_BUFFER_SIZE, fCurrentReportLength));
	atomic_add(&fBusyCount, -1);

	memset(info, 0, sizeof(*info));

	if (report_copy[0] == 0 || report_copy[0] == 0xff)
		return B_OK;

	info->buttons = report_copy[0] & 0x7;
	uint8 fingers = (report_copy[0] >> 3) & 0x1f;
	info->fingers = fingers;
	TRACE("buttons=%x, fingers=%x\n", info->buttons, fingers);
	const uint8 *fingerData = fCurrentReport + 1;
	int fingerCount = 0;
	int sumx = 0, sumy = 0, sumz = 0, sumw = 0;
	for (int finger = 0; finger < 5; finger++, fingerData += 5)
		if (fingers & (1 << finger)) {
			TRACE("finger %d:\n", finger);
			uint8 wh;
			int x, y, w;
			if (fHighPrecision) {
				x = fingerData[0] << 8 | fingerData[1];
				y = fingerData[2] << 8 | fingerData[3];
				wh = report_copy[30 + finger];
			} else {
				x = (fingerData[0] & 0xf0) << 4 | fingerData[1];
				y = (fingerData[0] & 0x0f) << 8 | fingerData[2];
				wh = fingerData[3];
			}

			int z = fingerData[4];

			w = MAX((wh >> 4) & 0xf, wh & 0xf);

			sumw += w;
			sumx += x;
			sumy += y;
			sumz += z;

			TRACE("x=%d, y=%d, z=%d, w=%d, wh=0x%x\n", x, y, z, w, wh);

			fingerCount++;
		}
	if (fingerCount > 0) {
		info->xPosition = sumx / fingerCount;
		info->yPosition = sumy / fingerCount;
		info->zPressure = sumz / fingerCount;
		info->fingerWidth = MIN(12, sumw / fingerCount);
	}

	TRACE("Resulting position=[%d, %d, %d, %d]\n",
		info->xPosition, info->yPosition, info->zPressure,
		info->fingerWidth);

	return B_OK;
}


void
ELANDevice::_UnstallCallback(void *cookie, status_t status, void *data,
	size_t actualLength)
{
	ELANDevice *device = (ELANDevice *)cookie;
	if (status != B_OK) {
		TRACE_ALWAYS("Unable to unstall device: %s\n", strerror(status));
	}

	// Now report the original failure, since we're ready to retry
	_TransferCallback(cookie, B_ERROR, device->fTransferBuffer, 0);
}


void
ELANDevice::_TransferCallback(void *cookie, status_t status, void *data,
	size_t actualLength)
{
	ELANDevice *device = (ELANDevice *)cookie;

	atomic_set(&device->fTransferScheduled, 0);
	device->_SetReport(status, device->fTransferBuffer, actualLength);
}


status_t
ELANDevice::_Reset()
{
	CALLED();
	status_t status = _SetPower(I2C_HID_POWER_ON);
	if (status != B_OK)
		return status;

	snooze(1000);

	uint8 cmd[] = {
		(uint8)(fDescriptor.wCommandRegister & 0xff),
		(uint8)(fDescriptor.wCommandRegister >> 8),
		0,
		I2C_HID_CMD_RESET,
	};

	status = _ExecCommand(I2C_OP_WRITE_STOP, cmd, sizeof(cmd), NULL, 0);
	if (status != B_OK) {
		_SetPower(I2C_HID_POWER_OFF);
		return status;
	}

	snooze(1000);
	return B_OK;
}


status_t
ELANDevice::_SetPower(uint8 power)
{
	CALLED();
	uint8 cmd[] = {
		(uint8)(fDescriptor.wCommandRegister & 0xff),
		(uint8)(fDescriptor.wCommandRegister >> 8),
		power,
		I2C_HID_CMD_SET_POWER
	};

	return _ExecCommand(I2C_OP_WRITE_STOP, cmd, sizeof(cmd), NULL, 0);
}


status_t
ELANDevice::_ReadRegister(uint16_t reg, size_t length, void *value)
{
	uint8_t cmd[2] = {
		(uint8_t) (reg & 0xff), (uint8_t) ((reg >> 8) & 0xff) };
	status_t status = _FetchBuffer(cmd, sizeof(cmd), fTransferBuffer,
		length);
	TRACE("Read register 0x%04x with value 0x%02x 0x%02x status=%d\n",
		reg, fTransferBuffer[0], fTransferBuffer[1], status);
	if (status != B_OK)
		return status;
	memcpy(value, fTransferBuffer, length);
	return B_OK;
}


status_t
ELANDevice::_WriteRegister(uint16_t reg, uint16_t value)
{
	uint8_t cmd[4] = { (uint8_t) (reg & 0xff),
		(uint8_t) ((reg >> 8) & 0xff),
		(uint8_t) (value & 0xff),
		(uint8_t) ((value >> 8) & 0xff) };
	TRACE("Write register 0x%04x with value 0x%04x\n", reg, value);
	status_t status = _FetchBuffer(cmd, sizeof(cmd), fTransferBuffer, 0);
	TRACE("status=%d\n", status);
	return status;
}


status_t
ELANDevice::_FetchReport(uint8 type, uint8 id, size_t reportSize)
{
	uint8 reportId = id > 15 ? 15 : id;
	size_t cmdLength = 6;
	uint8 cmd[] = {
		(uint8)(fDescriptor.wCommandRegister & 0xff),
		(uint8)(fDescriptor.wCommandRegister >> 8),
		(uint8)(reportId | (type << 4)),
		I2C_HID_CMD_GET_REPORT,
		0, 0, 0,
	};

	int dataOffset = 4;
	int reportIdLength = 1;
	if (reportId == 15) {
		cmd[dataOffset++] = id;
		cmdLength++;
		reportIdLength++;
	}
	cmd[dataOffset++] = fDescriptor.wDataRegister & 0xff;
	cmd[dataOffset++] = fDescriptor.wDataRegister >> 8;

	size_t bufferLength = reportSize + reportIdLength + 2;

	status_t status = _FetchBuffer(cmd, cmdLength, fTransferBuffer,
		bufferLength);
	if (status != B_OK) {
		atomic_set(&fTransferScheduled, 0);
		return status;
	}

	uint16 actualLength = fTransferBuffer[0] | (fTransferBuffer[1] << 8);
	TRACE("_FetchReport %" B_PRIuSIZE " %" B_PRIu16 "\n", reportSize,
		actualLength);

	if (actualLength <= 2 || actualLength == 0xffff || bufferLength == 0)
		actualLength = 0;
	else
		actualLength -= 2;

	atomic_set(&fTransferScheduled, 0);
	_SetReport(status,
		(uint8*)((addr_t)fTransferBuffer + 2), actualLength);
	return B_OK;
}


void
ELANDevice::_SetReport(status_t status, uint8 *report, size_t length)
{
	if (status != B_OK) {
		report = NULL;
		length = 0;
	}

	if (length < MIN_ELAN_REPORT && length != 0 && status == B_OK)
		status = B_ERROR;

	if (status == B_OK && length != 0 && report[0] != fReportID) {
		report = NULL;
		length = 0;
		status = B_BUSY;
	}

	if (report && length) {
		report++;
		length--;
	}

	fReportStatus = status;
	fCurrentReportLength = length;
	memset(fCurrentReport, 0, sizeof(fCurrentReport));
	if (report && status == B_OK)
		memcpy(fCurrentReport, report, MIN(sizeof(fCurrentReport), length));
	fConditionVariable.NotifyAll();
}


status_t
ELANDevice::_FetchBuffer(uint8* cmd, size_t cmdLength, void* buffer,
	size_t bufferLength)
{
	return _ExecCommand(I2C_OP_READ_STOP, cmd, cmdLength,
		buffer, bufferLength);
}


status_t
ELANDevice::_ExecCommand(i2c_op op, uint8* cmd, size_t cmdLength, void* buffer,
	size_t bufferLength)
{
	status_t status = fI2C->acquire_bus(fI2CCookie);
	if (status != B_OK)
		return status;
	status = fI2C->exec_command(fI2CCookie, I2C_OP_READ_STOP, cmd, cmdLength,
		buffer, bufferLength);
	fI2C->release_bus(fI2CCookie);
	return status;
}