/*      $NetBSD: aps.c,v 1.20 2025/02/17 22:56:46 andvar Exp $  */
/*      $OpenBSD: aps.c,v 1.15 2007/05/19 19:14:11 tedu Exp $   */
/*      $OpenBSD: aps.c,v 1.17 2008/06/27 06:08:43 canacar Exp $        */
/*
* Copyright (c) 2005 Jonathan Gray <[email protected]>
* Copyright (c) 2008 Can Erkin Acar <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

/*
* A driver for the ThinkPad Active Protection System based on notes from
* http://www.almaden.ibm.com/cs/people/marksmith/tpaps.html
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: aps.c,v 1.20 2025/02/17 22:56:46 andvar Exp $");

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/kernel.h>
#include <sys/callout.h>
#include <sys/module.h>

#include <sys/bus.h>

#include <dev/sysmon/sysmonvar.h>

#include <dev/isa/isareg.h>
#include <dev/isa/isavar.h>

#if defined(APSDEBUG)
#define DPRINTF(x)              do { printf x; } while (0)
#else
#define DPRINTF(x)
#endif


/*
* EC interface on Thinkpad Laptops, from Linux HDAPS driver notes.
* From Renesas H8S/2140B Group Hardware Manual
* http://documentation.renesas.com/eng/products/mpumcu/rej09b0300_2140bhm.pdf
*
* EC uses LPC Channel 3 registers TWR0..15
*/

/* STR3 status register */
#define APS_STR3                0x04

#define APS_STR3_IBF3B  0x80    /* Input buffer full (host->slave) */
#define APS_STR3_OBF3B  0x40    /* Output buffer full (slave->host)*/
#define APS_STR3_MWMF   0x20    /* Master write mode */
#define APS_STR3_SWMF   0x10    /* Slave write mode */


/* Base address of TWR registers */
#define APS_TWR_BASE            0x10
#define APS_TWR_RET             0x1f

/* TWR registers */
#define APS_CMD                 0x00
#define APS_ARG1                0x01
#define APS_ARG2                0x02
#define APS_ARG3                0x03
#define APS_RET                 0x0f

/* Sensor values */
#define APS_STATE               0x01
#define APS_XACCEL              0x02
#define APS_YACCEL              0x04
#define APS_TEMP                0x06
#define APS_XVAR                0x07
#define APS_YVAR                0x09
#define APS_TEMP2               0x0b
#define APS_UNKNOWN             0x0c
#define APS_INPUT               0x0d

/* write masks for I/O, send command + 0-3 arguments*/
#define APS_WRITE_0             0x0001
#define APS_WRITE_1             0x0003
#define APS_WRITE_2             0x0007
#define APS_WRITE_3             0x000f

/* read masks for I/O, read 0-3 values (skip command byte) */
#define APS_READ_0              0x0000
#define APS_READ_1              0x0002
#define APS_READ_2              0x0006
#define APS_READ_3              0x000e

#define APS_READ_RET            0x8000
#define APS_READ_ALL            0xffff

/* Bit definitions for APS_INPUT value */
#define APS_INPUT_KB            (1 << 5)
#define APS_INPUT_MS            (1 << 6)
#define APS_INPUT_LIDOPEN       (1 << 7)

#define APS_ADDR_SIZE           0x1f

struct sensor_rec {
       uint8_t         state;
       uint16_t        x_accel;
       uint16_t        y_accel;
       uint8_t         temp1;
       uint16_t        x_var;
       uint16_t        y_var;
       uint8_t         temp2;
       uint8_t         unk;
       uint8_t         input;
};

enum aps_sensors {
       APS_SENSOR_XACCEL = 0,
       APS_SENSOR_YACCEL,
       APS_SENSOR_XVAR,
       APS_SENSOR_YVAR,
       APS_SENSOR_TEMP1,
       APS_SENSOR_TEMP2,
       APS_SENSOR_KBACT,
       APS_SENSOR_MSACT,
       APS_SENSOR_LIDOPEN,
       APS_NUM_SENSORS
};

struct aps_softc {
       bus_space_tag_t sc_iot;
       bus_space_handle_t sc_ioh;
       bool sc_bus_space_valid;

       struct sysmon_envsys *sc_sme;
       envsys_data_t sc_sensor[APS_NUM_SENSORS];
       struct callout sc_callout;

       struct sensor_rec aps_data;
};

static int      aps_match(device_t, cfdata_t, void *);
static void     aps_attach(device_t, device_t, void *);
static int      aps_detach(device_t, int);

static int      aps_init(struct aps_softc *);
static int      aps_read_data(struct aps_softc *);
static void     aps_refresh_sensor_data(struct aps_softc *);
static void     aps_refresh(void *);
static int      aps_do_io(bus_space_tag_t, bus_space_handle_t,
                         unsigned char *, int, int);
static bool     aps_suspend(device_t, const pmf_qual_t *);
static bool     aps_resume(device_t, const pmf_qual_t *);

CFATTACH_DECL_NEW(aps, sizeof(struct aps_softc),
             aps_match, aps_attach, aps_detach, NULL);

/* properly communicate with the controller, writing a set of memory
* locations and reading back another set  */
static int
aps_do_io(bus_space_tag_t iot, bus_space_handle_t ioh,
         unsigned char *buf, int wmask, int rmask)
{
       int bp, stat, n;

       DPRINTF(("aps_do_io: CMD: 0x%02x, wmask: 0x%04x, rmask: 0x%04x\n",
           buf[0], wmask, rmask));

       /* write init byte using arbitration */
       for (n = 0; n < 100; n++) {
               stat = bus_space_read_1(iot, ioh, APS_STR3);
               if (stat & (APS_STR3_OBF3B | APS_STR3_SWMF)) {
                       bus_space_read_1(iot, ioh, APS_TWR_RET);
                       continue;
               }
               bus_space_write_1(iot, ioh, APS_TWR_BASE, buf[0]);
               stat = bus_space_read_1(iot, ioh, APS_STR3);
               if (stat & (APS_STR3_MWMF))
                       break;
               delay(1);
       }

       if (n == 100) {
               DPRINTF(("aps_do_io: Failed to get bus\n"));
               return 1;
       }

       /* write data bytes, init already sent */
       /* make sure last byte is always written as this will trigger slave */
       wmask |= APS_READ_RET;
       buf[APS_RET] = 0x01;

       for (n = 1, bp = 2; n < 16; bp <<= 1, n++) {
               if (wmask & bp) {
                       bus_space_write_1(iot, ioh, APS_TWR_BASE + n, buf[n]);
                       DPRINTF(("aps_do_io:  write %2d 0x%02x\n", n, buf[n]));
               }
       }

       for (n = 0; n < 100; n++) {
               stat = bus_space_read_1(iot, ioh, APS_STR3);
               if (stat & (APS_STR3_OBF3B))
                       break;
               delay(5 * 100);
       }

       if (n == 100) {
               DPRINTF(("aps_do_io: timeout waiting response\n"));
               return 1;
       }
       /* wait for data available */
       /* make sure to read the final byte to clear status */
       rmask |= APS_READ_RET;

       /* read cmd and data bytes */
       for (n = 0, bp = 1; n < 16; bp <<= 1, n++) {
               if (rmask & bp) {
                       buf[n] = bus_space_read_1(iot, ioh, APS_TWR_BASE + n);
                       DPRINTF(("aps_do_io:  read %2d 0x%02x\n", n, buf[n]));
               }
       }

       return 0;
}

static int
aps_match(device_t parent, cfdata_t match, void *aux)
{
       struct isa_attach_args *ia = aux;
       bus_space_tag_t iot = ia->ia_iot;
       bus_space_handle_t ioh;
       unsigned char iobuf[16];
       int iobase;
       uint8_t cr;

       /* Must supply an address */
       if (ia->ia_nio < 1)
               return 0;

       if (ISA_DIRECT_CONFIG(ia))
               return 0;

       if (ia->ia_io[0].ir_addr == ISA_UNKNOWN_PORT)
               return 0;

       iobase = ia->ia_io[0].ir_addr;

       if (bus_space_map(iot, iobase, APS_ADDR_SIZE, 0, &ioh)) {
               aprint_error("aps: can't map i/o space\n");
               return 0;
       }


       /* See if this machine has APS */

       /* get APS mode */
       iobuf[APS_CMD] = 0x13;
       if (aps_do_io(iot, ioh, iobuf, APS_WRITE_0, APS_READ_1)) {
               bus_space_unmap(iot, ioh, APS_ADDR_SIZE);
               return 0;
       }

       /*
        * Observed values from Linux driver:
        * 0x01: T42
        * 0x02: chip already initialised
        * 0x03: T41
        * 0x05: T61
        */

       cr = iobuf[APS_ARG1];

       bus_space_unmap(iot, ioh, APS_ADDR_SIZE);
       DPRINTF(("aps: state register 0x%x\n", cr));

       if (iobuf[APS_RET] != 0 || cr < 1 || cr > 5) {
               DPRINTF(("aps0: unsupported state %d\n", cr));
               return 0;
       }

       ia->ia_nio = 1;
       ia->ia_io[0].ir_size = APS_ADDR_SIZE;
       ia->ia_niomem = 0;
       ia->ia_nirq = 0;
       ia->ia_ndrq = 0;

       return 1;
}

static void
aps_attach(device_t parent, device_t self, void *aux)
{
       struct aps_softc *sc = device_private(self);
       struct isa_attach_args *ia = aux;
       int iobase, i;

       sc->sc_iot = ia->ia_iot;
       iobase = ia->ia_io[0].ir_addr;

       callout_init(&sc->sc_callout, 0);
       callout_setfunc(&sc->sc_callout, aps_refresh, sc);

       if (bus_space_map(sc->sc_iot, iobase, APS_ADDR_SIZE, 0, &sc->sc_ioh)) {
               aprint_error(": can't map i/o space\n");
               return;
       }
       sc->sc_bus_space_valid = true;

       aprint_naive("\n");
       aprint_normal(": Thinkpad Active Protection System\n");

       if (aps_init(sc)) {
               aprint_error_dev(self, "failed to initialize\n");
               goto out;
       }

       /* Initialize sensors */
#define INITDATA(idx, unit, string)                                     \
       sc->sc_sensor[idx].units = unit;                                \
       strlcpy(sc->sc_sensor[idx].desc, string,                        \
           sizeof(sc->sc_sensor[idx].desc));

       INITDATA(APS_SENSOR_XACCEL, ENVSYS_INTEGER, "x-acceleration");
       INITDATA(APS_SENSOR_YACCEL, ENVSYS_INTEGER, "y-acceleration");
       INITDATA(APS_SENSOR_TEMP1, ENVSYS_STEMP, "temperature 1");
       INITDATA(APS_SENSOR_TEMP2, ENVSYS_STEMP, "temperature 2");
       INITDATA(APS_SENSOR_XVAR, ENVSYS_INTEGER, "x-variable");
       INITDATA(APS_SENSOR_YVAR, ENVSYS_INTEGER, "y-variable");
       INITDATA(APS_SENSOR_KBACT, ENVSYS_INDICATOR, "keyboard active");
       INITDATA(APS_SENSOR_MSACT, ENVSYS_INDICATOR, "mouse active");
       INITDATA(APS_SENSOR_LIDOPEN, ENVSYS_INDICATOR, "lid open");

       sc->sc_sme = sysmon_envsys_create();

       for (i = 0; i < APS_NUM_SENSORS; i++) {

               sc->sc_sensor[i].state = ENVSYS_SVALID;

               if (sc->sc_sensor[i].units == ENVSYS_INTEGER)
                       sc->sc_sensor[i].flags = ENVSYS_FHAS_ENTROPY;

               if (sysmon_envsys_sensor_attach(sc->sc_sme,
                       &sc->sc_sensor[i])) {
                       sysmon_envsys_destroy(sc->sc_sme);
                       sc->sc_sme = NULL;
                       goto out;
               }
       }
       /*
        * Register with the sysmon_envsys(9) framework.
        */
       sc->sc_sme->sme_name = device_xname(self);
       sc->sc_sme->sme_flags = SME_DISABLE_REFRESH;

       if ((i = sysmon_envsys_register(sc->sc_sme))) {
               aprint_error_dev(self,
                   "unable to register with sysmon (%d)\n", i);
               sysmon_envsys_destroy(sc->sc_sme);
               sc->sc_sme = NULL;
               goto out;
       }

       if (!pmf_device_register(self, aps_suspend, aps_resume))
               aprint_error_dev(self, "couldn't establish power handler\n");

       /* Refresh sensor data every 0.5 seconds */
       callout_schedule(&sc->sc_callout, (hz) / 2);

       return;

out:
       bus_space_unmap(sc->sc_iot, sc->sc_ioh, APS_ADDR_SIZE);
}

static int
aps_init(struct aps_softc *sc)
{
       unsigned char iobuf[16];

       /* command 0x17/0x81: check EC */
       iobuf[APS_CMD] = 0x17;
       iobuf[APS_ARG1] = 0x81;

       if (aps_do_io(sc->sc_iot, sc->sc_ioh, iobuf, APS_WRITE_1, APS_READ_3))
               return 1;
       if (iobuf[APS_RET] != 0 ||iobuf[APS_ARG3] != 0)
               return 1;

       /* Test values from the Linux driver */
       if ((iobuf[APS_ARG1] != 0 || iobuf[APS_ARG2] != 0x60) &&
           (iobuf[APS_ARG1] != 1 || iobuf[APS_ARG2] != 0))
               return 1;

       /* command 0x14: set power */
       iobuf[APS_CMD] = 0x14;
       iobuf[APS_ARG1] = 0x01;

       if (aps_do_io(sc->sc_iot, sc->sc_ioh, iobuf, APS_WRITE_1, APS_READ_0))
               return 1;

       if (iobuf[APS_RET] != 0)
               return 1;

       /* command 0x10: set config (sample rate and order) */
       iobuf[APS_CMD] = 0x10;
       iobuf[APS_ARG1] = 0xc8;
       iobuf[APS_ARG2] = 0x00;
       iobuf[APS_ARG3] = 0x02;

       if (aps_do_io(sc->sc_iot, sc->sc_ioh, iobuf, APS_WRITE_3, APS_READ_0))
               return 1;

       /* command 0x11: refresh data */
       iobuf[APS_CMD] = 0x11;
       if (aps_do_io(sc->sc_iot, sc->sc_ioh, iobuf, APS_WRITE_0, APS_READ_1))
               return 1;
       if (iobuf[APS_ARG1] != 0)
               return 1;

       return 0;
}

static int
aps_detach(device_t self, int flags)
{
       struct aps_softc *sc = device_private(self);

       callout_halt(&sc->sc_callout, NULL);
       callout_destroy(&sc->sc_callout);

       if (sc->sc_sme)
               sysmon_envsys_unregister(sc->sc_sme);
       if (sc->sc_bus_space_valid == true)
               bus_space_unmap(sc->sc_iot, sc->sc_ioh, APS_ADDR_SIZE);

       return 0;
}

static int
aps_read_data(struct aps_softc *sc)
{
       unsigned char iobuf[16];

       iobuf[APS_CMD] = 0x11;
       if (aps_do_io(sc->sc_iot, sc->sc_ioh, iobuf, APS_WRITE_0, APS_READ_ALL))
               return 1;

       sc->aps_data.state = iobuf[APS_STATE];
       sc->aps_data.x_accel = iobuf[APS_XACCEL] + 256 * iobuf[APS_XACCEL + 1];
       sc->aps_data.y_accel = iobuf[APS_YACCEL] + 256 * iobuf[APS_YACCEL + 1];
       sc->aps_data.temp1 = iobuf[APS_TEMP];
       sc->aps_data.x_var = iobuf[APS_XVAR] + 256 * iobuf[APS_XVAR + 1];
       sc->aps_data.y_var = iobuf[APS_YVAR] + 256 * iobuf[APS_YVAR + 1];
       sc->aps_data.temp2 = iobuf[APS_TEMP2];
       sc->aps_data.input = iobuf[APS_INPUT];

       return 0;
}

static void
aps_refresh_sensor_data(struct aps_softc *sc)
{
       int64_t temp;

       if (aps_read_data(sc)) {
               printf("aps0: read data failed\n");
               return;
       }

       sc->sc_sensor[APS_SENSOR_XACCEL].value_cur = sc->aps_data.x_accel;
       sc->sc_sensor[APS_SENSOR_YACCEL].value_cur = sc->aps_data.y_accel;

       if (sc->aps_data.temp1 == 0xff)
               sc->sc_sensor[APS_SENSOR_TEMP1].state = ENVSYS_SINVALID;
       else {
               /* convert to micro (mu) degrees */
               temp = sc->aps_data.temp1 * 1000000;
               /* convert to kelvin */
               temp += 273150000;
               sc->sc_sensor[APS_SENSOR_TEMP1].value_cur = temp;
               sc->sc_sensor[APS_SENSOR_TEMP1].state = ENVSYS_SVALID;
       }

       if (sc->aps_data.temp2 == 0xff)
               sc->sc_sensor[APS_SENSOR_TEMP2].state = ENVSYS_SINVALID;
       else {
               /* convert to micro (mu) degrees */
               temp = sc->aps_data.temp2 * 1000000;
               /* convert to kelvin */
               temp += 273150000;
               sc->sc_sensor[APS_SENSOR_TEMP2].value_cur = temp;
               sc->sc_sensor[APS_SENSOR_TEMP2].state = ENVSYS_SVALID;
       }

       sc->sc_sensor[APS_SENSOR_XVAR].value_cur = sc->aps_data.x_var;
       sc->sc_sensor[APS_SENSOR_YVAR].value_cur = sc->aps_data.y_var;
       sc->sc_sensor[APS_SENSOR_KBACT].value_cur =
           (sc->aps_data.input &  APS_INPUT_KB) ? 1 : 0;
       sc->sc_sensor[APS_SENSOR_MSACT].value_cur =
           (sc->aps_data.input & APS_INPUT_MS) ? 1 : 0;
       sc->sc_sensor[APS_SENSOR_LIDOPEN].value_cur =
           (sc->aps_data.input & APS_INPUT_LIDOPEN) ? 1 : 0;
}

static void
aps_refresh(void *arg)
{
       struct aps_softc *sc = arg;

       aps_refresh_sensor_data(sc);
       callout_schedule(&sc->sc_callout, (hz) / 2);
}

static bool
aps_suspend(device_t dv, const pmf_qual_t *qual)
{
       struct aps_softc *sc = device_private(dv);

       callout_stop(&sc->sc_callout);

       return true;
}

static bool
aps_resume(device_t dv, const pmf_qual_t *qual)
{
       struct aps_softc *sc = device_private(dv);
       unsigned char iobuf[16];

       /*
        * Redo the init sequence on resume, because APS is
        * as forgetful as it is deaf.
        */

       /* get APS mode */
       iobuf[APS_CMD] = 0x13;
       if (aps_do_io(sc->sc_iot, sc->sc_ioh, iobuf, APS_WRITE_0, APS_READ_1)
           || aps_init(sc))
               aprint_error_dev(dv, "failed to wake up\n");
       else
               callout_schedule(&sc->sc_callout, (hz) / 2);

       return true;
}

MODULE(MODULE_CLASS_DRIVER, aps, "sysmon_envsys");

#ifdef _MODULE
#include "ioconf.c"
#endif

static int
aps_modcmd(modcmd_t cmd, void *opaque)
{
       switch (cmd) {
       case MODULE_CMD_INIT:
#ifdef _MODULE
               return config_init_component(cfdriver_ioconf_aps,
                   cfattach_ioconf_aps, cfdata_ioconf_aps);
#else
               return 0;
#endif
       case MODULE_CMD_FINI:
#ifdef _MODULE
               return config_fini_component(cfdriver_ioconf_aps,
                   cfattach_ioconf_aps, cfdata_ioconf_aps);
#else
               return 0;
#endif
       default:
               return ENOTTY;
       }
}