| /** |
| * @file nest_doorbell_chime.c |
| * |
| * @remark Copyright 2017 NestLabs |
| * |
| * @author Dmitry Yatsushkevich <dmitryya@google.com> |
| */ |
| |
| #include <linux/module.h> |
| |
| #include <linux/kernel.h> |
| #include <linux/slab.h> |
| #include <linux/notifier.h> |
| #include <linux/init.h> |
| #include <linux/hrtimer.h> |
| #include <linux/delay.h> |
| #include <linux/platform_device.h> |
| #include <linux/gpio.h> |
| #include <linux/of_platform.h> |
| #include <linux/of_gpio.h> |
| #include <linux/spinlock.h> |
| #include <linux/sysctl.h> |
| #include <linux/power/nest_rq_battery.h> |
| #include <linux/delay.h> |
| #include <linux/string.h> |
| #include <linux/input.h> |
| #include <linux/ktime.h> |
| #include <linux/nest_gpio_keys.h> |
| |
| #define DEFAULT_TIMEOUT 500 |
| #define VBRIDGE_RESTORE_TIMEOUT 500 |
| #define MAX_CMD_LEN 1024 |
| |
| #define BATTERY_TEMP_LOW_LIMIT 10000 // 10C |
| #define BATTERY_TEMP_LOW 18000 // 18C |
| #define BATTERY_TEMP_HIGH 23000 // 23C |
| #define BATTERY_TEMP_HIGH_LIMIT 80000 // 80C |
| #define BATTERY_VOLTAGE_LOW_LIMIT 3800 // 3.80V |
| #define BATTERY_VOLTAGE_LOW 3950 // 3.95V |
| #define BATTERY_VOLTAGE_HIGH 4150 // 4.15V |
| |
| struct doorbell_chime_drvdata { |
| struct device *dev; |
| const char *name; |
| int gpio; |
| struct hrtimer ring_timer; |
| struct hrtimer vbridge_timer; |
| unsigned long duration_ms; |
| long max_battery_temp_mc; |
| long min_battery_temp_mc; |
| unsigned long min_battery_voltage_mv; |
| struct work_struct work; |
| struct work_struct callback_work; |
| char cmd_callback[MAX_CMD_LEN]; |
| atomic_t ongoing; |
| atomic_t work_ongoing; |
| atomic_t callback_ongoing; |
| atomic_t skipped_chime_ring; |
| int disabled; |
| int doorbell_key_code; |
| struct input_handler event_handler; |
| struct input_handle event_handle; |
| struct workqueue_struct *workqueue; |
| }; |
| |
| //#define DEBUG 1 |
| |
| #ifdef DEBUG |
| ktime_t ring_start_time; |
| ktime_t ring_end_time; |
| #endif // DEBUG |
| |
| static enum hrtimer_restart vbridge_timer_handler(struct hrtimer *handle) |
| { |
| struct doorbell_chime_drvdata *pdata = |
| container_of(handle, struct doorbell_chime_drvdata, vbridge_timer); |
| |
| nest_rq_battery_vbridge_disable(nest_rq_battery, 0); |
| |
| #ifdef DEBUG |
| { |
| ktime_t vb_diff_time, r_diff_time, all_diff_time; |
| r_diff_time = ktime_sub(ring_end_time, ring_start_time); |
| vb_diff_time = ktime_sub(ktime_get(), ring_end_time); |
| all_diff_time = ktime_sub(ktime_get(), ring_start_time); |
| |
| dev_dbg(pdata->dev, |
| " ring duration %llu nsec, vbridge low period %llu nsec, all duration %llu nsec\n", |
| ktime_to_ns(r_diff_time), ktime_to_ns(vb_diff_time), |
| ktime_to_ns(all_diff_time)); |
| } |
| #endif // DEBUG |
| |
| atomic_set(&pdata->ongoing, 0); |
| return HRTIMER_NORESTART; |
| } |
| |
| static enum hrtimer_restart ring_timer_handler(struct hrtimer *handle) |
| { |
| struct doorbell_chime_drvdata *pdata = |
| container_of(handle, struct doorbell_chime_drvdata, ring_timer); |
| ktime_t kt; |
| |
| kt = ktime_set(VBRIDGE_RESTORE_TIMEOUT / 1000 /* sec */ , |
| (VBRIDGE_RESTORE_TIMEOUT % 1000) * 1000000 /* nsec */ ); |
| |
| gpio_set_value(pdata->gpio, 0); |
| |
| #ifdef DEBUG |
| ring_end_time = ktime_get(); |
| #endif // DEBUG |
| |
| hrtimer_start(&pdata->vbridge_timer, kt, HRTIMER_MODE_REL); |
| return HRTIMER_NORESTART; |
| } |
| |
| static void stop_ring(struct doorbell_chime_drvdata *pdata) |
| { |
| hrtimer_cancel(&pdata->vbridge_timer); |
| hrtimer_cancel(&pdata->ring_timer); |
| gpio_set_value(pdata->gpio, 0); |
| nest_rq_battery_vbridge_disable(nest_rq_battery, 0); |
| atomic_set(&pdata->ongoing, 0); |
| } |
| |
| static void run_cmd_callback(struct doorbell_chime_drvdata *pdata) |
| { |
| char **argv = NULL; |
| static char *envp[] = { |
| "HOME=/", |
| "TERM=linux", |
| "PATH=/sbin:/usr/sbin:/bin:/usr/bin", |
| NULL |
| }; |
| |
| if (!strlen(pdata->cmd_callback)) { |
| dev_warn(pdata->dev, " callback command is not present."); |
| goto fail; |
| } |
| |
| argv = argv_split(GFP_KERNEL, pdata->cmd_callback, NULL); |
| if (!argv) { |
| dev_warn(pdata->dev, " failed to allocate memory\n"); |
| goto fail; |
| } |
| |
| if (call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC)) { |
| dev_err(pdata->dev, " failed to run callback commamd: '%s'\n", pdata->cmd_callback); |
| } |
| |
| fail: |
| atomic_set(&pdata->callback_ongoing, 0); |
| if (argv) |
| argv_free(argv); |
| dev_info(pdata->dev, " callback ended."); |
| } |
| |
| #define BATT_OK 0 |
| #define BATT_TEMP_INVALID 1 |
| #define BATT_TEMP_TOO_HOT 2 |
| #define BATT_TEMP_TOO_COLD 3 |
| #define BATT_VOLTS_INVALID 4 |
| #define BATT_VOLTS_LOW 5 |
| |
| static int is_safe_to_run_from_battery(struct doorbell_chime_drvdata *pdata) |
| { |
| int batt_temp, batt_voltage; |
| int reason = BATT_OK; |
| |
| int ret = nest_rq_battery_get_temp(nest_rq_battery, &batt_temp); // temp in tenth of C |
| batt_voltage = nest_rq_battery_get_voltage(nest_rq_battery); // in uV |
| |
| dev_dbg(pdata->dev, " battery temp %d mC and battery voltage %d mV", |
| batt_temp, batt_voltage); |
| |
| if (ret == -1) { |
| dev_warn(pdata->dev, "battery temp is not valid. Running from battery is not safe"); |
| return BATT_TEMP_INVALID; |
| } |
| |
| batt_temp *= 100; // tenth of C to mC |
| // RQ-1916: Any VBAT, TBAT < 10°C or TBAT > 80°C, don't ring the house chime |
| if (batt_temp < pdata->min_battery_temp_mc) { |
| reason = BATT_TEMP_TOO_COLD; |
| } else if (batt_temp > pdata->max_battery_temp_mc) { |
| reason = BATT_TEMP_TOO_HOT; |
| } |
| |
| if (reason != BATT_OK) { |
| dev_warn(pdata->dev, |
| "Battery temperature %dmC is not in safe range > %ldmC and < %ldmC. Running from battery is not safe", |
| batt_temp, |
| pdata->min_battery_temp_mc, |
| pdata->max_battery_temp_mc); |
| return reason; |
| } |
| |
| if (batt_voltage == -1) { |
| dev_warn(pdata->dev, "battery voltage is not valid. Running from battery is not safe"); |
| return BATT_VOLTS_INVALID; |
| } |
| |
| batt_voltage /=1000; // uV -> mV |
| |
| // RQ-1916: VBAT < 3.80V, any battery temperature: don't ring the house chime |
| if (batt_voltage < pdata->min_battery_voltage_mv) { |
| dev_warn(pdata->dev, "Battery voltage %dmV is too low. Running from battery is not safe", |
| batt_voltage); |
| return BATT_VOLTS_LOW; |
| } |
| |
| // RQ-1916: 3.80V ≤ VBAT < 3.95V, TBAT < 23°C: don't ring the house chime |
| if ((batt_temp <= BATTERY_TEMP_HIGH) && (batt_voltage <= BATTERY_VOLTAGE_LOW)) { |
| dev_warn(pdata->dev, "Battery temperature and voltage is not in the safe range: %dmC, %dmV.", |
| batt_temp, batt_voltage); |
| return BATT_TEMP_TOO_COLD; |
| } |
| // RQ-1916: 3.95V ≤ VBAT < 4.15V, TBAT < 18°C: don't ring the house chime |
| if ((batt_temp <= BATTERY_TEMP_LOW) && (batt_voltage <= BATTERY_VOLTAGE_HIGH)) { |
| dev_warn(pdata->dev, "Battery temperature and voltage is not in the safe range: %dmC, %dmV.", |
| batt_temp, batt_voltage); |
| return BATT_TEMP_TOO_COLD; |
| } |
| |
| return BATT_OK; |
| } |
| |
| static void do_ring(struct doorbell_chime_drvdata *pdata) |
| { |
| int rv; |
| if (!atomic_xchg(&pdata->callback_ongoing, 1)) { |
| queue_work(pdata->workqueue, &pdata->callback_work); |
| } else { |
| dev_warn(pdata->dev, " chime callback is in progress\n"); |
| } |
| |
| if (pdata->disabled) { |
| dev_warn(pdata->dev, "Chime is disabled. Ignore ring."); |
| return; |
| } |
| |
| if (!atomic_xchg(&pdata->ongoing, 1)) { |
| ktime_t kt; |
| |
| kt = ktime_set(pdata->duration_ms / 1000 /* sec */ , |
| (pdata->duration_ms % 1000) * 1000000 /* nsec */ ); |
| |
| #ifdef DEBUG |
| ring_start_time = ktime_get(); |
| #endif // DEBUG |
| |
| rv = is_safe_to_run_from_battery(pdata); |
| if (rv != BATT_OK) { |
| dev_warn(pdata->dev, "is not safe to ring a chime.\n"); |
| atomic_set(&pdata->skipped_chime_ring, rv); |
| atomic_set(&pdata->ongoing, 0); |
| return; |
| } |
| |
| atomic_set(&pdata->skipped_chime_ring, 0); |
| |
| dev_info(pdata->dev, "the chime is ringing...\n"); |
| nest_rq_battery_vbridge_disable(nest_rq_battery, 1); |
| gpio_set_value(pdata->gpio, 1); |
| |
| hrtimer_start(&pdata->ring_timer, kt, HRTIMER_MODE_REL); |
| } else { |
| dev_warn(pdata->dev, "chime ring is in progress....\n"); |
| } |
| |
| } |
| |
| static void doorbell_chime_work_func(struct work_struct *work) |
| { |
| struct doorbell_chime_drvdata *pdata = |
| container_of(work, struct doorbell_chime_drvdata, work); |
| |
| dev_dbg(pdata->dev, " run doorbell chime work\n"); |
| do_ring(pdata); |
| atomic_set(&pdata->work_ongoing, 0); |
| } |
| |
| static void doorbell_chime_callback_work_func(struct work_struct *work) |
| { |
| struct doorbell_chime_drvdata *pdata = |
| container_of(work, struct doorbell_chime_drvdata, callback_work); |
| |
| dev_dbg(pdata->dev, " run doorbell chime callback work\n"); |
| run_cmd_callback(pdata); |
| } |
| |
| // sysfs interfaces |
| |
| ssize_t doorbell_chime_show_duration(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| return sprintf(buf, "%lu\n", pdata->duration_ms); |
| } |
| |
| ssize_t doorbell_chime_store_duration(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| unsigned long val; |
| if (sscanf(buf, "%lu", &val) != 1) |
| return -EINVAL; |
| |
| pdata->duration_ms = val; |
| dev_info(dev, " setting 'duration_ms':%lu\n", pdata->duration_ms); |
| return count; |
| } |
| |
| ssize_t doorbell_chime_show_max_battery_temp(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| return sprintf(buf, "%ld\n", pdata->max_battery_temp_mc); |
| } |
| |
| ssize_t doorbell_chime_store_max_battery_temp(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| unsigned long val; |
| if (sscanf(buf, "%ld", &val) != 1) |
| return -EINVAL; |
| |
| pdata->max_battery_temp_mc = val; |
| dev_info(dev, " setting 'max_battery_temp_mc':%lu\n", pdata->max_battery_temp_mc); |
| return count; |
| } |
| |
| ssize_t doorbell_chime_show_min_battery_temp(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| return sprintf(buf, "%ld\n", pdata->min_battery_temp_mc); |
| } |
| |
| ssize_t doorbell_chime_store_min_battery_temp(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| unsigned long val; |
| if (sscanf(buf, "%ld", &val) != 1) |
| return -EINVAL; |
| |
| pdata->min_battery_temp_mc = val; |
| dev_info(dev, " setting 'min_battery_temp_mc':%lu\n", pdata->min_battery_temp_mc); |
| return count; |
| } |
| |
| ssize_t doorbell_chime_show_min_battery_voltage(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| return sprintf(buf, "%lu\n", pdata->min_battery_voltage_mv); |
| } |
| |
| ssize_t doorbell_chime_store_min_battery_voltage(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| unsigned long val; |
| if (sscanf(buf, "%lu", &val) != 1) |
| return -EINVAL; |
| |
| pdata->min_battery_voltage_mv = val; |
| dev_info(dev, " setting 'min_battery_voltage_mv':%lu\n", pdata->min_battery_voltage_mv); |
| return count; |
| } |
| |
| ssize_t doorbell_chime_show_skipped_chime_ring(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| int val = atomic_xchg(&pdata->skipped_chime_ring, -1); |
| |
| if (val == -1) |
| return sprintf(buf, "\n"); |
| |
| return sprintf(buf, "%d\n", val); |
| } |
| |
| ssize_t doorbell_chime_show_ongoing(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| int val = atomic_read(&pdata->ongoing); |
| |
| return sprintf(buf, "%d\n", val); |
| } |
| |
| ssize_t doorbell_chime_show_disable(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| const char *STATE[] = { "Enabled", "Disabled" }; |
| |
| return sprintf(buf, "%s\n", STATE[pdata->disabled]); |
| } |
| |
| ssize_t doorbell_chime_store_disable(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| if (sysfs_streq(buf, "0")) |
| pdata->disabled = 0; |
| else if (sysfs_streq(buf, "1")) |
| pdata->disabled = 1; |
| else { |
| dev_warn(dev, " unsupported value: %s\n", buf); |
| return -1; |
| } |
| |
| dev_info(dev, " setting 'disabled':%s\n", |
| pdata->disabled ? "true" : "false"); |
| return count; |
| } |
| |
| ssize_t doorbell_chime_store_ring(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| do_ring(pdata); |
| |
| return count; |
| } |
| |
| ssize_t doorbell_chime_store_cancel(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| if (!sysfs_streq(buf, "0")) { |
| dev_info(dev, " cancel ringing chime\n"); |
| stop_ring(pdata); |
| } |
| |
| return count; |
| } |
| |
| ssize_t doorbell_chime_show_callback(struct device * dev, |
| struct device_attribute * attr, char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| return sprintf(buf, "%s\n", pdata->cmd_callback); |
| } |
| |
| ssize_t doorbell_chime_store_callback(struct device * dev, |
| struct device_attribute * attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| size_t len = count; |
| |
| if (len > MAX_CMD_LEN - 1) { |
| len = MAX_CMD_LEN - 1; |
| } |
| |
| strncpy(pdata->cmd_callback, buf, len); |
| pdata->cmd_callback[len] = '\0'; |
| |
| dev_info(dev, " setting 'callback':%s\n", |
| pdata->cmd_callback); |
| |
| return len; |
| } |
| |
| static DEVICE_ATTR(skipped_chime_ring, S_IRUGO, doorbell_chime_show_skipped_chime_ring, NULL); |
| static DEVICE_ATTR(duration_ms, S_IWUSR | S_IRUGO, |
| doorbell_chime_show_duration, doorbell_chime_store_duration); |
| static DEVICE_ATTR(ongoing, S_IRUGO, doorbell_chime_show_ongoing, NULL); |
| static DEVICE_ATTR(disable, S_IWUSR | S_IRUGO, |
| doorbell_chime_show_disable, doorbell_chime_store_disable); |
| static DEVICE_ATTR(ring, S_IWUSR | S_IRUGO, NULL, doorbell_chime_store_ring); |
| static DEVICE_ATTR(cancel, S_IWUSR | S_IRUGO, |
| NULL, doorbell_chime_store_cancel); |
| static DEVICE_ATTR(callback, S_IWUSR | S_IRUGO, |
| doorbell_chime_show_callback, doorbell_chime_store_callback); |
| static DEVICE_ATTR(max_battery_temp_mc, S_IWUSR | S_IRUGO, |
| doorbell_chime_show_max_battery_temp, doorbell_chime_store_max_battery_temp); |
| static DEVICE_ATTR(min_battery_temp_mc, S_IWUSR | S_IRUGO, |
| doorbell_chime_show_min_battery_temp, doorbell_chime_store_min_battery_temp); |
| static DEVICE_ATTR(min_battery_voltage_mv, S_IWUSR | S_IRUGO, |
| doorbell_chime_show_min_battery_voltage, doorbell_chime_store_min_battery_voltage); |
| |
| static struct attribute *doorbell_chime_attrs[] = { |
| &dev_attr_skipped_chime_ring.attr, |
| &dev_attr_duration_ms.attr, |
| &dev_attr_ongoing.attr, |
| &dev_attr_disable.attr, |
| &dev_attr_ring.attr, |
| &dev_attr_cancel.attr, |
| &dev_attr_callback.attr, |
| &dev_attr_max_battery_temp_mc.attr, |
| &dev_attr_min_battery_temp_mc.attr, |
| &dev_attr_min_battery_voltage_mv.attr, |
| NULL, |
| }; |
| |
| static struct attribute_group doorbell_chime_attr_group = { |
| .attrs = doorbell_chime_attrs, |
| }; |
| |
| static struct doorbell_chime_drvdata *doorbell_chime_get_devtree_pdata(struct device *dev) |
| { |
| struct device_node *node; |
| struct doorbell_chime_drvdata *pdata; |
| int error; |
| int gpio; |
| enum of_gpio_flags flags; |
| |
| node = dev->of_node; |
| if (!node) { |
| error = -ENODEV; |
| goto err_out; |
| } |
| |
| pdata = kzalloc(sizeof(*pdata), GFP_KERNEL); |
| if (!pdata) { |
| error = -ENOMEM; |
| goto err_out; |
| } |
| |
| if (!of_find_property(node, "gpios", NULL)) { |
| dev_err(dev, "GPIOs is not defined\n"); |
| error = -EINVAL; |
| goto err_free_pdata; |
| } |
| |
| gpio = of_get_gpio_flags(node, 0, &flags); |
| if (gpio < 0) { |
| error = gpio; |
| if (error != -EPROBE_DEFER) |
| dev_err(dev, |
| "Failed to get gpio flags, error: %d\n", error); |
| goto err_free_pdata; |
| } |
| |
| if (of_property_read_u32(node, "doorbell-key-code", &pdata->doorbell_key_code)) { |
| dev_err(dev, |
| "doorbell-key-code is not defined\n"); |
| goto err_free_pdata; |
| } |
| |
| pdata->gpio = gpio; |
| |
| return pdata; |
| |
| err_free_pdata: |
| kfree(pdata); |
| err_out: |
| return ERR_PTR(error); |
| } |
| |
| static void nest_input_event(struct input_handle *handle, |
| unsigned int type, unsigned int code, int value) |
| { |
| struct doorbell_chime_drvdata *pdata = handle->private; |
| |
| dev_dbg(pdata->dev, "an input event is detected: type %d code %d value %d\n", |
| type, code, value); |
| |
| if ((type == EV_KEY) && (code == pdata->doorbell_key_code) && (value == 1)) { |
| dev_dbg(pdata->dev, " doorbell button (code %d) press is detected\n", code); |
| |
| if (!atomic_xchg(&pdata->work_ongoing, 1)) { |
| dev_dbg(pdata->dev, " schedule the work"); |
| atomic_set(&pdata->skipped_chime_ring, -1); |
| queue_work(pdata->workqueue, &pdata->work); |
| } else { |
| dev_dbg(pdata->dev, " the work is already scheduled....\n"); |
| } |
| } |
| } |
| |
| static int nest_input_connect(struct input_handler *handler, struct input_dev *dev, |
| const struct input_device_id *id) |
| { |
| int error; |
| struct doorbell_chime_drvdata *pdata = |
| container_of(handler, struct doorbell_chime_drvdata, event_handler); |
| |
| pdata->event_handle.dev = input_get_device(dev); |
| pdata->event_handle.name = "nest_doorbell"; |
| pdata->event_handle.handler = handler; |
| pdata->event_handle.private = pdata; |
| |
| error = input_register_handle(&pdata->event_handle); |
| dev_info(pdata->dev, "connected to '%s'\n", dev->name); |
| |
| if (error) { |
| dev_err(pdata->dev, |
| "Failed to register input handle, error: %d\n", error); |
| return error; |
| } |
| |
| error = input_open_device(&pdata->event_handle); |
| if (error) { |
| dev_err(pdata->dev, |
| "Failed to open input device, error: %d\n", error); |
| } |
| |
| return error; |
| } |
| |
| static void nest_input_disconnect(struct input_handle *handle) |
| { |
| input_close_device(handle); |
| input_unregister_handle(handle); |
| } |
| |
| static const struct input_device_id nest_ids[] = { |
| { .flags = INPUT_DEVICE_ID_MATCH_VENDOR, |
| .vendor = NEST_ID }, /* Matches NEST devices */ |
| { }, /* Terminating zero entry */ |
| }; |
| |
| |
| #define NEST_MINOR_BASE 13 |
| static int doorbell_chime_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct doorbell_chime_drvdata *pdata = dev_get_platdata(dev); |
| int error; |
| |
| if (!pdata) { |
| pdata = doorbell_chime_get_devtree_pdata(dev); |
| if (IS_ERR(pdata)) |
| return PTR_ERR(pdata); |
| } |
| |
| pdata->dev = dev; |
| |
| pdata->disabled = 0; |
| atomic_set(&pdata->ongoing, 0); |
| atomic_set(&pdata->work_ongoing, 0); |
| atomic_set(&pdata->callback_ongoing, 0); |
| atomic_set(&pdata->skipped_chime_ring, -1); |
| pdata->duration_ms = DEFAULT_TIMEOUT; |
| pdata->max_battery_temp_mc = BATTERY_TEMP_HIGH_LIMIT; |
| pdata->min_battery_temp_mc = BATTERY_TEMP_LOW_LIMIT; |
| pdata->min_battery_voltage_mv = BATTERY_VOLTAGE_LOW_LIMIT; |
| |
| hrtimer_init(&pdata->ring_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); |
| pdata->ring_timer.function = ring_timer_handler; |
| |
| hrtimer_init(&pdata->vbridge_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); |
| pdata->vbridge_timer.function = vbridge_timer_handler; |
| |
| pdata->event_handler.event = nest_input_event; |
| pdata->event_handler.connect = nest_input_connect; |
| pdata->event_handler.disconnect = nest_input_disconnect; |
| pdata->event_handler.legacy_minors = true; |
| pdata->event_handler.minor = NEST_MINOR_BASE; |
| pdata->event_handler.name = "nest_doorbell"; |
| pdata->event_handler.id_table = nest_ids; |
| |
| platform_set_drvdata(pdev, pdata); |
| |
| error = sysfs_create_group(&pdev->dev.kobj, &doorbell_chime_attr_group); |
| if (error) { |
| dev_err(dev, "Unable to export keys/switches, error: %d\n", |
| error); |
| goto fail; |
| } |
| |
| error = input_register_handler(&pdata->event_handler); |
| if (error) { |
| dev_err(dev, "Unable to register input handler: %d\n", error); |
| goto fail; |
| } |
| |
| /* used for unplugging and affects IO latency/throughput - HIGHPRI */ |
| pdata->workqueue = alloc_workqueue("kdoorbell", |
| WQ_MEM_RECLAIM | WQ_HIGHPRI, 0); |
| if (!pdata->workqueue) |
| panic("Failed to create kblockd\n"); |
| |
| INIT_WORK(&pdata->work, doorbell_chime_work_func); |
| INIT_WORK(&pdata->callback_work, doorbell_chime_callback_work_func); |
| |
| return 0; |
| |
| fail: |
| platform_set_drvdata(pdev, NULL); |
| kfree(pdata); |
| |
| return error; |
| } |
| |
| static int doorbell_chime_remove(struct platform_device *pdev) |
| { |
| struct doorbell_chime_drvdata *pdata = platform_get_drvdata(pdev); |
| |
| cancel_work_sync(&pdata->work); |
| cancel_work_sync(&pdata->callback_work); |
| atomic_set(&pdata->work_ongoing, 0); |
| atomic_set(&pdata->callback_ongoing, 0); |
| atomic_set(&pdata->skipped_chime_ring, -1); |
| input_unregister_handler(&pdata->event_handler); |
| stop_ring(pdata); |
| sysfs_remove_group(&pdev->dev.kobj, &doorbell_chime_attr_group); |
| destroy_workqueue(pdata->workqueue); |
| if (gpio_is_valid(pdata->gpio)) |
| gpio_free(pdata->gpio); |
| kfree(pdata); |
| |
| return 0; |
| } |
| |
| static struct of_device_id doorbell_chime_of_match[] = { |
| {.compatible = "nest-doorbell-chime",}, |
| {}, |
| }; |
| |
| MODULE_DEVICE_TABLE(of, doorbell_chime_of_match); |
| |
| static struct platform_driver doorbell_chime_device_driver = { |
| .probe = doorbell_chime_probe, |
| .remove = doorbell_chime_remove, |
| .driver = { |
| .name = "nest_doorbell_chime", |
| .owner = THIS_MODULE, |
| .of_match_table = of_match_ptr(doorbell_chime_of_match), |
| } |
| }; |
| |
| static int __init doorbell_chime_init(void) |
| { |
| return platform_driver_register(&doorbell_chime_device_driver); |
| } |
| |
| static void __exit doorbell_chime_exit(void) |
| { |
| platform_driver_unregister(&doorbell_chime_device_driver); |
| } |
| |
| late_initcall(doorbell_chime_init); |
| module_exit(doorbell_chime_exit); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Dmitry Yatsushkevich <dmitryya@google.com>"); |
| MODULE_DESCRIPTION("Nest doorbell chime driver"); |
| MODULE_ALIAS("platform:nest-doorbell-chime"); |