/*
 *  dv6k_laptop.c - HP dv6000 series laptop driver
 *
 *
 *  Copyright (C) 2007 Angelo Arrifano <miknix@gmail.com>
 *
 *  Based on toshiba_acpi and msi_laptop
 *  Backlight EC registers discovered with help of acer_ec.pl
 *  Most of EC regs from disassembled dsdt table.
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *  TODO:
 *  - Allow brightness up/down keys to change brightness (looking at QBCTRL.exe on windows, it
 *    looks like is using HP WMI interface to do it. See _HID HPQ0006).
 *  - Only dv6500 models are supported. Also, I did only test on dv6535ep.
 *  - Fan control?
 */

#define DV6K_LAPTOP_VERSION	"0.03"
#define DV6K_LAPTOP_DESC "HP dv6000 series ACPI driver" 

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/backlight.h>
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/platform_device.h>

MODULE_AUTHOR("Angelo Arrifano <miknix@gmail.com>");
MODULE_DESCRIPTION(DV6K_LAPTOP_DESC);
MODULE_LICENSE("GPL");

/*
 * EC_FIELD  ADDR    BITMASK 	  DESC
 * LIDS      0x52    0x01       LID IS CLOSED
 * DT1I      0x52    0x02       WIRELESS BUTTON
 * RTMP      0x58    0xFF       CPU TEMP
 * DTMC      0xE2    0xFF       BACKLIGHT
 */
#define EC_BL             0xE2 /* Also, 0xE3 provides a fadout effect */
#define EC_BL_MASK        0xFF
#define EC_BL_VALMAX      0x5F /* Max value found in windowz vista */
#define EC_BL_VALMIN      0x00
#define EC_CPUTEMP        0x58
#define EC_CPUTEMP_MASK   0xFF
#define EC_WIFIBTN        0x52
#define EC_WIFIBTN_MASK   0x02
#define EC_LIDSTAT        0x52
#define EC_LIDSTAT_MASK   0x01

/*
 * Backlight structs
 */
static struct backlight_ops dv6k_bl_data;
static struct backlight_device *dv6k_bl_dev;


/*
 * DMI Structs
 */
static struct dmi_system_id __initdata dv6k_dmi_table[] = {
	{
		.ident = "HP DV6500",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "Hewlett-Packard"),
			DMI_MATCH(DMI_PRODUCT_NAME, "HP Pavilion dv6500 Notebook PC"),
		}
	},
	{ }
};

/*
 * Platform driver Structs
 */
static ssize_t dv6k_cputemp_show(struct device *dev,
					 struct device_attribute *attr, char *buf);
static ssize_t dv6k_wifibtn_show(struct device *dev,
					 struct device_attribute *attr, char *buf);
static ssize_t dv6k_lidstat_show(struct device *dev,
					 struct device_attribute *attr, char *buf);
static DEVICE_ATTR(cputemp, 0444, dv6k_cputemp_show, NULL);
static DEVICE_ATTR(wifibtn, 0444, dv6k_wifibtn_show, NULL);
static DEVICE_ATTR(lidstat, 0444, dv6k_lidstat_show, NULL);
static struct attribute *dv6k_attr[] = {
	&dev_attr_cputemp.attr,
	&dev_attr_wifibtn.attr,
	&dev_attr_lidstat.attr,
	NULL
};
static struct attribute_group dv6k_attrgrp = {
	.attrs = dv6k_attr
};
static struct platform_device *dv6k_pfdev;
static struct platform_driver dv6k_pfdrv = {
	.driver = {
		.name = "dv6k_laptop",
		.owner = THIS_MODULE,
	}
};

/*
 * Backlight functions
 */

static int dv6k_bl_get_brightness(struct backlight_device *bl_dev)
{
	u8 val = -1;

	if(ec_read((u8)EC_BL, &val))
		printk("dv6k_laptop: Cannot read from EC!\n");
	return val;
}


static int dv6k_bl_set_status(struct backlight_device *bl_dev)
{
	if(ec_write((u8)EC_BL, (u8)bl_dev->props.brightness))
	{
		printk("dv6k_laptop: Cannot write to EC!\n");
		return -1;
	}
	else
		return 0;
}

/*
 * Platform driver functions
 */

static inline u8 dv6k_read_ec(u8 addr, u8 mask){
	u8 val = -1;

	if(ec_read(addr, &val)){
		printk("dv6k_laptop: Cannot read cputemp from EC(%x)!\n", addr);
		return -1;
	}
	return val&mask;
}

static ssize_t dv6k_cputemp_show(struct device *dev,
					 struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%d C\n", dv6k_read_ec(EC_CPUTEMP, EC_CPUTEMP_MASK));
}

static ssize_t dv6k_lidstat_show(struct device *dev,
					 struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%d\n", !dv6k_read_ec(EC_LIDSTAT, EC_LIDSTAT_MASK));
}

static ssize_t dv6k_wifibtn_show(struct device *dev,
					 struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%d\n", dv6k_read_ec(EC_WIFIBTN, EC_WIFIBTN_MASK) == 0x02);
}

/*
 * Module Functions
 */

static void dv6k_laptop_exit(void)
{
	if(dv6k_bl_dev) backlight_device_unregister(dv6k_bl_dev);

	sysfs_remove_group(&dv6k_pfdev->dev.kobj, &dv6k_attrgrp);
	platform_device_unregister(dv6k_pfdev);
	platform_driver_unregister(&dv6k_pfdrv);

	return;
}

static int __init dv6k_laptop_init(void)
{
	int ret;

	printk("dv6k_laptop: %s v%s\n", DV6K_LAPTOP_DESC, DV6K_LAPTOP_VERSION);

	/* ACPI Check */
	if(acpi_disabled){
		printk("dv6k_laptop: ERROR: ACPI is disabled\n"); 
		return -ENODEV;
	}

	/* DMI Check */
	if(!dmi_check_system(dv6k_dmi_table)){
		printk("dv6k_laptop: ERROR: Laptop model not supported\n");
		return -ENODEV;
	}

	/* Backlight */
	dv6k_bl_data.get_brightness = dv6k_bl_get_brightness;
	dv6k_bl_data.update_status = dv6k_bl_set_status;

	dv6k_bl_dev = backlight_device_register("hp_dv6k", NULL, 
						 NULL, &dv6k_bl_data);
	if(IS_ERR(dv6k_bl_dev)){
		printk("dv6k_laptop: Could not register backlight device\n");
		dv6k_bl_dev = NULL;
		dv6k_laptop_exit();
		return -ENODEV;
	}
	dv6k_bl_dev->props.max_brightness = EC_BL_VALMAX;

	/* Platform driver */
	ret = platform_driver_register(&dv6k_pfdrv);
	if(ret) goto fail_pfregister;

	dv6k_pfdev = platform_device_alloc("dv6k_laptop", -1);
	if(!dv6k_pfdev) {
		ret = -ENOMEM;
		goto fail_pfalloc;
	}

	ret = platform_device_add(dv6k_pfdev);
	if(ret) goto fail_pfadd;

	ret = sysfs_create_group(&dv6k_pfdev->dev.kobj, &dv6k_attrgrp);
	if(ret) goto fail_pfgrp;

	return 0;

fail_pfgrp:
	platform_device_del(dv6k_pfdev);

fail_pfadd:
	platform_device_put(dv6k_pfdev);

fail_pfalloc:
	platform_driver_unregister(&dv6k_pfdrv);

fail_pfregister:
	backlight_device_unregister(dv6k_bl_dev);

	return ret;	
}

module_init(dv6k_laptop_init);
module_exit(dv6k_laptop_exit);
