| /* |
| * Synaptics TCM touchscreen driver |
| * |
| * Copyright (C) 2017-2019 Synaptics Incorporated. All rights reserved. |
| * |
| * Copyright (C) 2017-2019 Scott Lin <scott.lin@tw.synaptics.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * INFORMATION CONTAINED IN THIS DOCUMENT IS PROVIDED "AS-IS," AND SYNAPTICS |
| * EXPRESSLY DISCLAIMS ALL EXPRESS AND IMPLIED WARRANTIES, INCLUDING ANY |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, |
| * AND ANY WARRANTIES OF NON-INFRINGEMENT OF ANY INTELLECTUAL PROPERTY RIGHTS. |
| * IN NO EVENT SHALL SYNAPTICS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN CONNECTION |
| * WITH THE USE OF THE INFORMATION CONTAINED IN THIS DOCUMENT, HOWEVER CAUSED |
| * AND BASED ON ANY THEORY OF LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| * NEGLIGENCE OR OTHER TORTIOUS ACTION, AND EVEN IF SYNAPTICS WAS ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. IF A TRIBUNAL OF COMPETENT JURISDICTION DOES |
| * NOT PERMIT THE DISCLAIMER OF DIRECT DAMAGES OR ANY OTHER DAMAGES, SYNAPTICS' |
| * TOTAL CUMULATIVE LIABILITY TO ANY PARTY SHALL NOT EXCEED ONE HUNDRED U.S. |
| * DOLLARS. |
| */ |
| |
| #include <linux/sched/signal.h> |
| #include "synaptics_tcm_core.h" |
| |
| #define SYSFS_DIR_NAME "diagnostics" |
| |
| enum pingpong_state { |
| PING = 0, |
| PONG = 1, |
| }; |
| |
| struct diag_hcd { |
| pid_t pid; |
| unsigned char report_type; |
| enum pingpong_state state; |
| struct kobject *sysfs_dir; |
| struct siginfo sigio; |
| struct task_struct *task; |
| struct syna_tcm_buffer ping; |
| struct syna_tcm_buffer pong; |
| struct syna_tcm_hcd *tcm_hcd; |
| }; |
| |
| DECLARE_COMPLETION(diag_remove_complete); |
| |
| static struct diag_hcd *diag_hcd; |
| |
| STORE_PROTOTYPE(diag, pid); |
| SHOW_PROTOTYPE(diag, size); |
| STORE_PROTOTYPE(diag, type); |
| SHOW_PROTOTYPE(diag, rows); |
| SHOW_PROTOTYPE(diag, cols); |
| SHOW_PROTOTYPE(diag, hybrid); |
| SHOW_PROTOTYPE(diag, buttons); |
| |
| static struct device_attribute *attrs[] = { |
| ATTRIFY(pid), |
| ATTRIFY(size), |
| ATTRIFY(type), |
| ATTRIFY(rows), |
| ATTRIFY(cols), |
| ATTRIFY(hybrid), |
| ATTRIFY(buttons), |
| }; |
| |
| static ssize_t diag_sysfs_data_show(struct file *data_file, |
| struct kobject *kobj, struct bin_attribute *attributes, |
| char *buf, loff_t pos, size_t count); |
| |
| static struct bin_attribute bin_attr = { |
| .attr = { |
| .name = "data", |
| .mode = 0444, |
| }, |
| .size = 0, |
| .read = diag_sysfs_data_show, |
| }; |
| |
| static ssize_t diag_sysfs_pid_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| int retval; |
| unsigned int input; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| if (kstrtouint(buf, 10, &input)) |
| return -EINVAL; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| diag_hcd->pid = input; |
| |
| if (diag_hcd->pid) { |
| diag_hcd->task = pid_task(find_vpid(diag_hcd->pid), |
| PIDTYPE_PID); |
| if (!diag_hcd->task) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to locate task\n"); |
| retval = -EINVAL; |
| goto exit; |
| } |
| } |
| |
| retval = count; |
| |
| exit: |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static ssize_t diag_sysfs_size_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int retval; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| if (diag_hcd->state == PING) { |
| LOCK_BUFFER(diag_hcd->ping); |
| |
| retval = snprintf(buf, PAGE_SIZE, |
| "%u\n", |
| diag_hcd->ping.data_length); |
| |
| UNLOCK_BUFFER(diag_hcd->ping); |
| } else { |
| LOCK_BUFFER(diag_hcd->pong); |
| |
| retval = snprintf(buf, PAGE_SIZE, |
| "%u\n", |
| diag_hcd->pong.data_length); |
| |
| UNLOCK_BUFFER(diag_hcd->pong); |
| } |
| |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static ssize_t diag_sysfs_type_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| unsigned int input; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| if (kstrtouint(buf, 10, &input)) |
| return -EINVAL; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| diag_hcd->report_type = (unsigned char)input; |
| |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return count; |
| } |
| |
| static ssize_t diag_sysfs_rows_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int retval; |
| unsigned int rows; |
| struct syna_tcm_app_info *app_info; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| if (tcm_hcd->id_info.mode != MODE_APPLICATION || |
| tcm_hcd->app_status != APP_STATUS_OK) { |
| retval = -ENODEV; |
| goto exit; |
| } |
| |
| app_info = &tcm_hcd->app_info; |
| rows = le2_to_uint(app_info->num_of_image_rows); |
| |
| retval = snprintf(buf, PAGE_SIZE, "%u\n", rows); |
| |
| exit: |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static ssize_t diag_sysfs_cols_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int retval; |
| unsigned int cols; |
| struct syna_tcm_app_info *app_info; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| if (tcm_hcd->id_info.mode != MODE_APPLICATION || |
| tcm_hcd->app_status != APP_STATUS_OK) { |
| retval = -ENODEV; |
| goto exit; |
| } |
| |
| app_info = &tcm_hcd->app_info; |
| cols = le2_to_uint(app_info->num_of_image_cols); |
| |
| retval = snprintf(buf, PAGE_SIZE, "%u\n", cols); |
| |
| exit: |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static ssize_t diag_sysfs_hybrid_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int retval; |
| unsigned int hybrid; |
| struct syna_tcm_app_info *app_info; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| if (tcm_hcd->id_info.mode != MODE_APPLICATION || |
| tcm_hcd->app_status != APP_STATUS_OK) { |
| retval = -ENODEV; |
| goto exit; |
| } |
| |
| app_info = &tcm_hcd->app_info; |
| hybrid = le2_to_uint(app_info->has_hybrid_data); |
| |
| retval = snprintf(buf, PAGE_SIZE, "%u\n", hybrid); |
| |
| exit: |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static ssize_t diag_sysfs_buttons_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int retval; |
| unsigned int buttons; |
| struct syna_tcm_app_info *app_info; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| if (tcm_hcd->id_info.mode != MODE_APPLICATION || |
| tcm_hcd->app_status != APP_STATUS_OK) { |
| retval = -ENODEV; |
| goto exit; |
| } |
| |
| app_info = &tcm_hcd->app_info; |
| buttons = le2_to_uint(app_info->num_of_buttons); |
| |
| retval = snprintf(buf, PAGE_SIZE, "%u\n", buttons); |
| |
| exit: |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static ssize_t diag_sysfs_data_show(struct file *data_file, |
| struct kobject *kobj, struct bin_attribute *attributes, |
| char *buf, loff_t pos, size_t count) |
| { |
| int retval; |
| unsigned int readlen; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| mutex_lock(&tcm_hcd->extif_mutex); |
| |
| retval = 0; |
| |
| if (diag_hcd->state == PING) { |
| LOCK_BUFFER(diag_hcd->ping); |
| |
| if (diag_hcd->ping.data_length == 0) { |
| readlen = 0; |
| goto exit; |
| } |
| |
| readlen = MIN(count, diag_hcd->ping.data_length - pos); |
| |
| if (diag_hcd->ping.data_length) { |
| retval = secure_memcpy(buf, |
| count, |
| &diag_hcd->ping.buf[pos], |
| diag_hcd->ping.buf_size - pos, |
| readlen); |
| } |
| |
| UNLOCK_BUFFER(diag_hcd->ping); |
| } else { |
| LOCK_BUFFER(diag_hcd->pong); |
| |
| if (diag_hcd->pong.data_length == 0) { |
| readlen = 0; |
| goto exit; |
| } |
| |
| readlen = MIN(count, diag_hcd->pong.data_length - pos); |
| |
| if (diag_hcd->pong.data_length) { |
| retval = secure_memcpy(buf, |
| count, |
| &diag_hcd->pong.buf[pos], |
| diag_hcd->pong.buf_size - pos, |
| readlen); |
| } |
| |
| UNLOCK_BUFFER(diag_hcd->pong); |
| } |
| |
| exit: |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to copy report data\n"); |
| } else { |
| retval = readlen; |
| } |
| |
| mutex_unlock(&tcm_hcd->extif_mutex); |
| |
| return retval; |
| } |
| |
| static void diag_report(void) |
| { |
| int retval; |
| static enum pingpong_state state = PING; |
| struct syna_tcm_hcd *tcm_hcd = diag_hcd->tcm_hcd; |
| |
| if (state == PING) { |
| LOCK_BUFFER(diag_hcd->ping); |
| |
| retval = syna_tcm_alloc_mem(tcm_hcd, |
| &diag_hcd->ping, |
| tcm_hcd->report.buffer.data_length); |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to allocate memory for ping.buf\n"); |
| UNLOCK_BUFFER(diag_hcd->ping); |
| return; |
| } |
| |
| retval = secure_memcpy(diag_hcd->ping.buf, |
| diag_hcd->ping.buf_size, |
| tcm_hcd->report.buffer.buf, |
| tcm_hcd->report.buffer.buf_size, |
| tcm_hcd->report.buffer.data_length); |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to copy report data\n"); |
| UNLOCK_BUFFER(diag_hcd->ping); |
| return; |
| } |
| |
| diag_hcd->ping.data_length = tcm_hcd->report.buffer.data_length; |
| |
| UNLOCK_BUFFER(diag_hcd->ping); |
| |
| diag_hcd->state = state; |
| state = PONG; |
| } else { |
| LOCK_BUFFER(diag_hcd->pong); |
| |
| retval = syna_tcm_alloc_mem(tcm_hcd, |
| &diag_hcd->pong, |
| tcm_hcd->report.buffer.data_length); |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to allocate memory for pong.buf\n"); |
| UNLOCK_BUFFER(diag_hcd->pong); |
| return; |
| } |
| |
| retval = secure_memcpy(diag_hcd->pong.buf, |
| diag_hcd->pong.buf_size, |
| tcm_hcd->report.buffer.buf, |
| tcm_hcd->report.buffer.buf_size, |
| tcm_hcd->report.buffer.data_length); |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to copy report data\n"); |
| UNLOCK_BUFFER(diag_hcd->pong); |
| return; |
| } |
| |
| diag_hcd->pong.data_length = tcm_hcd->report.buffer.data_length; |
| |
| UNLOCK_BUFFER(diag_hcd->pong); |
| |
| diag_hcd->state = state; |
| state = PING; |
| } |
| |
| if (diag_hcd->pid) |
| send_sig_info(SIGIO, &diag_hcd->sigio, diag_hcd->task); |
| } |
| |
| static int diag_init(struct syna_tcm_hcd *tcm_hcd) |
| { |
| int retval; |
| int idx; |
| |
| diag_hcd = kzalloc(sizeof(*diag_hcd), GFP_KERNEL); |
| if (!diag_hcd) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to allocate memory for diag_hcd\n"); |
| return -ENOMEM; |
| } |
| |
| diag_hcd->tcm_hcd = tcm_hcd; |
| diag_hcd->state = PING; |
| |
| INIT_BUFFER(diag_hcd->ping, false); |
| INIT_BUFFER(diag_hcd->pong, false); |
| |
| memset(&diag_hcd->sigio, 0x00, sizeof(diag_hcd->sigio)); |
| diag_hcd->sigio.si_signo = SIGIO; |
| diag_hcd->sigio.si_code = SI_USER; |
| |
| diag_hcd->sysfs_dir = kobject_create_and_add(SYSFS_DIR_NAME, |
| tcm_hcd->sysfs_dir); |
| if (!diag_hcd->sysfs_dir) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to create sysfs directory\n"); |
| retval = -EINVAL; |
| goto err_sysfs_create_dir; |
| } |
| |
| for (idx = 0; idx < ARRAY_SIZE(attrs); idx++) { |
| retval = sysfs_create_file(diag_hcd->sysfs_dir, |
| &(*attrs[idx]).attr); |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to create sysfs file\n"); |
| goto err_sysfs_create_file; |
| } |
| } |
| |
| retval = sysfs_create_bin_file(diag_hcd->sysfs_dir, &bin_attr); |
| if (retval < 0) { |
| LOGE(tcm_hcd->pdev->dev.parent, |
| "Failed to create sysfs bin file\n"); |
| goto err_sysfs_create_bin_file; |
| } |
| |
| return 0; |
| |
| err_sysfs_create_bin_file: |
| err_sysfs_create_file: |
| for (idx--; idx >= 0; idx--) |
| sysfs_remove_file(diag_hcd->sysfs_dir, &(*attrs[idx]).attr); |
| |
| kobject_put(diag_hcd->sysfs_dir); |
| |
| err_sysfs_create_dir: |
| RELEASE_BUFFER(diag_hcd->pong); |
| RELEASE_BUFFER(diag_hcd->ping); |
| |
| kfree(diag_hcd); |
| diag_hcd = NULL; |
| |
| return retval; |
| } |
| |
| static int diag_remove(struct syna_tcm_hcd *tcm_hcd) |
| { |
| int idx; |
| |
| if (!diag_hcd) |
| goto exit; |
| |
| sysfs_remove_bin_file(diag_hcd->sysfs_dir, &bin_attr); |
| |
| for (idx = 0; idx < ARRAY_SIZE(attrs); idx++) |
| sysfs_remove_file(diag_hcd->sysfs_dir, &(*attrs[idx]).attr); |
| |
| kobject_put(diag_hcd->sysfs_dir); |
| |
| RELEASE_BUFFER(diag_hcd->pong); |
| RELEASE_BUFFER(diag_hcd->ping); |
| |
| kfree(diag_hcd); |
| diag_hcd = NULL; |
| |
| exit: |
| complete(&diag_remove_complete); |
| |
| return 0; |
| } |
| |
| static int diag_syncbox(struct syna_tcm_hcd *tcm_hcd) |
| { |
| if (!diag_hcd) |
| return 0; |
| |
| if (tcm_hcd->report.id == diag_hcd->report_type) |
| diag_report(); |
| |
| return 0; |
| } |
| |
| static int diag_reset(struct syna_tcm_hcd *tcm_hcd) |
| { |
| int retval; |
| |
| if (!diag_hcd) { |
| retval = diag_init(tcm_hcd); |
| return retval; |
| } |
| |
| return 0; |
| } |
| |
| static struct syna_tcm_module_cb diag_module = { |
| .type = TCM_DIAGNOSTICS, |
| .init = diag_init, |
| .remove = diag_remove, |
| .syncbox = diag_syncbox, |
| .asyncbox = NULL, |
| .reset = diag_reset, |
| .suspend = NULL, |
| .resume = NULL, |
| .early_suspend = NULL, |
| }; |
| |
| static int __init diag_module_init(void) |
| { |
| return syna_tcm_add_module(&diag_module, true); |
| } |
| |
| static void __exit diag_module_exit(void) |
| { |
| syna_tcm_add_module(&diag_module, false); |
| |
| wait_for_completion(&diag_remove_complete); |
| } |
| |
| module_init(diag_module_init); |
| module_exit(diag_module_exit); |
| |
| MODULE_AUTHOR("Synaptics, Inc."); |
| MODULE_DESCRIPTION("Synaptics TCM Diagnostics Module"); |
| MODULE_LICENSE("GPL v2"); |