/* $NetBSD: aibs_acpi.c,v 1.7 2021/01/29 15:49:55 thorpej Exp $ */

/*-
* Copyright (c) 2011 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Jukka Ruohonen.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

/*      $OpenBSD: atk0110.c,v 1.1 2009/07/23 01:38:16 cnst Exp $        */
/*
* Copyright (c) 2009 Constantine A. Murenin <[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.
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: aibs_acpi.c,v 1.7 2021/01/29 15:49:55 thorpej Exp $");

#include <sys/param.h>
#include <sys/kmem.h>
#include <sys/module.h>

#include <dev/acpi/acpireg.h>
#include <dev/acpi/acpivar.h>

/*
* ASUSTeK AI Booster (ACPI ASOC ATK0110).
*
* This code was originally written for OpenBSD after the techniques
* described in the Linux's asus_atk0110.c and FreeBSD's acpi_aiboost.c
* were verified to be accurate on the actual hardware kindly provided by
* Sam Fourman Jr.  It was subsequently ported from OpenBSD to DragonFly BSD,
* and then to the NetBSD's sysmon_envsys(9) framework.
*
*                                -- Constantine A. Murenin <http://cnst.su/>
*/

#define _COMPONENT               ACPI_RESOURCE_COMPONENT
ACPI_MODULE_NAME                 ("acpi_aibs")

#define AIBS_MUX_HWMON           0x00000006
#define AIBS_MUX_MGMT            0x00000011

#define AIBS_TYPE(x)             (((x) >> 16) & 0xff)
#define AIBS_TYPE_VOLT           2
#define AIBS_TYPE_TEMP           3
#define AIBS_TYPE_FAN            4

struct aibs_sensor {
       envsys_data_t                    as_sensor;
       uint64_t                         as_type;
       uint64_t                         as_liml;
       uint64_t                         as_limh;

       SIMPLEQ_ENTRY(aibs_sensor)       as_list;
};

struct aibs_softc {
       device_t                         sc_dev;
       struct acpi_devnode             *sc_node;
       struct sysmon_envsys            *sc_sme;
       bool                             sc_model;      /* new model = true */

       SIMPLEQ_HEAD(, aibs_sensor)      as_head;
};

static int      aibs_match(device_t, cfdata_t, void *);
static void     aibs_attach(device_t, device_t, void *);
static int      aibs_detach(device_t, int);

static void     aibs_init(device_t);
static void     aibs_init_new(device_t);
static void     aibs_init_old(device_t, int);

static void     aibs_sensor_add(device_t, ACPI_OBJECT *);
static bool     aibs_sensor_value(device_t, struct aibs_sensor *, uint64_t *);
static void     aibs_sensor_refresh(struct sysmon_envsys *, envsys_data_t *);
static void     aibs_sensor_limits(struct sysmon_envsys *, envsys_data_t *,
                                  sysmon_envsys_lim_t *, uint32_t *);

CFATTACH_DECL_NEW(aibs, sizeof(struct aibs_softc),
   aibs_match, aibs_attach, aibs_detach, NULL);

static const struct device_compatible_entry compat_data[] = {
       { .compat = "ATK0110" },
       DEVICE_COMPAT_EOL
};

static int
aibs_match(device_t parent, cfdata_t match, void *aux)
{
       struct acpi_attach_args *aa = aux;

       return acpi_compatible_match(aa, compat_data);
}

static void
aibs_attach(device_t parent, device_t self, void *aux)
{
       struct aibs_softc *sc = device_private(self);
       struct acpi_attach_args *aa = aux;

       sc->sc_dev = self;
       sc->sc_node = aa->aa_node;

       aprint_naive("\n");
       aprint_normal(": ASUSTeK AI Booster\n");

       sc->sc_sme = sysmon_envsys_create();

       sc->sc_sme->sme_cookie = sc;
       sc->sc_sme->sme_name = device_xname(self);
       sc->sc_sme->sme_refresh = aibs_sensor_refresh;
       sc->sc_sme->sme_get_limits = aibs_sensor_limits;

       aibs_init(self);
       SIMPLEQ_INIT(&sc->as_head);

       if (sc->sc_model != false)
               aibs_init_new(self);
       else {
               aibs_init_old(self, AIBS_TYPE_FAN);
               aibs_init_old(self, AIBS_TYPE_TEMP);
               aibs_init_old(self, AIBS_TYPE_VOLT);
       }

       (void)pmf_device_register(self, NULL, NULL);

       if (sc->sc_sme->sme_nsensors == 0) {
               aprint_error_dev(self, "no sensors found\n");
               sysmon_envsys_destroy(sc->sc_sme);
               sc->sc_sme = NULL;
               return;
       }

       if (sysmon_envsys_register(sc->sc_sme) != 0)
               aprint_error_dev(self, "failed to register with sysmon\n");
}

static int
aibs_detach(device_t self, int flags)
{
       struct aibs_softc *sc = device_private(self);
       struct aibs_sensor *as;

       pmf_device_deregister(self);

       if (sc->sc_sme != NULL)
               sysmon_envsys_unregister(sc->sc_sme);

       while (SIMPLEQ_FIRST(&sc->as_head) != NULL) {
               as = SIMPLEQ_FIRST(&sc->as_head);
               SIMPLEQ_REMOVE_HEAD(&sc->as_head, as_list);
               kmem_free(as, sizeof(*as));
       }

       return 0;
}

static void
aibs_init(device_t self)
{
       struct aibs_softc *sc = device_private(self);
       ACPI_HANDLE tmp;
       ACPI_STATUS rv;

       /*
        * Old model uses the tuple { TSIF, VSIF, FSIF } to
        * enumerate the sensors and { RTMP, RVLT, RFAN }
        * to obtain the values. New mode uses GGRP for the
        * enumeration and { GITM, SITM } as accessors.
        */
       rv = AcpiGetHandle(sc->sc_node->ad_handle, "GGRP", &tmp);

       if (ACPI_FAILURE(rv)) {
               sc->sc_model = false;
               return;
       }

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "GITM", &tmp);

       if (ACPI_FAILURE(rv)) {
               sc->sc_model = false;
               return;
       }

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "SITM", &tmp);

       if (ACPI_FAILURE(rv)) {
               sc->sc_model = false;
               return;
       }

       sc->sc_model = true;

       /*
        * If both the new and the old methods are present, prefer
        * the old one; GGRP/GITM may not be functional in this case.
        */
       rv = AcpiGetHandle(sc->sc_node->ad_handle, "FSIF", &tmp);

       if (ACPI_FAILURE(rv))
               return;

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "TSIF", &tmp);

       if (ACPI_FAILURE(rv))
               return;

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "VSIF", &tmp);

       if (ACPI_FAILURE(rv))
               return;

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "RFAN", &tmp);

       if (ACPI_FAILURE(rv))
               return;

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "RTMP", &tmp);

       if (ACPI_FAILURE(rv))
               return;

       rv = AcpiGetHandle(sc->sc_node->ad_handle, "RVLT", &tmp);

       if (ACPI_FAILURE(rv))
               return;

       sc->sc_model = false;
}

static void
aibs_init_new(device_t self)
{
       struct aibs_softc *sc = device_private(self);
       ACPI_OBJECT_LIST arg;
       ACPI_OBJECT id, *obj;
       ACPI_BUFFER buf;
       ACPI_STATUS rv;
       uint32_t i, n;

       arg.Count = 1;
       arg.Pointer = &id;

       id.Type = ACPI_TYPE_INTEGER;
       id.Integer.Value = AIBS_MUX_HWMON;

       buf.Pointer = NULL;
       buf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

       rv = AcpiEvaluateObject(sc->sc_node->ad_handle, "GGRP", &arg, &buf);

       if (ACPI_FAILURE(rv))
               goto out;

       obj = buf.Pointer;

       if (obj->Type != ACPI_TYPE_PACKAGE) {
               rv = AE_TYPE;
               goto out;
       }

       if (obj->Package.Count > UINT32_MAX) {
               rv = AE_AML_NUMERIC_OVERFLOW;
               goto out;
       }

       n = obj->Package.Count;

       if (n == 0) {
               rv = AE_NOT_EXIST;
               goto out;
       }

       for (i = 0; i < n; i++)
               aibs_sensor_add(self, &obj->Package.Elements[i]);

out:
       if (buf.Pointer != NULL)
               ACPI_FREE(buf.Pointer);

       if (ACPI_FAILURE(rv)) {

               aprint_error_dev(self, "failed to evaluate "
                   "GGRP: %s\n", AcpiFormatException(rv));
       }
}

static void
aibs_init_old(device_t self, int type)
{
       struct aibs_softc *sc = device_private(self);
       char path[] = "?SIF";
       ACPI_OBJECT *elm, *obj;
       ACPI_BUFFER buf;
       ACPI_STATUS rv;
       uint32_t i, n;

       switch (type) {

       case AIBS_TYPE_FAN:
               path[0] = 'F';
               break;

       case AIBS_TYPE_TEMP:
               path[0] = 'T';
               break;

       case AIBS_TYPE_VOLT:
               path[0] = 'V';
               break;

       default:
               return;
       }

       rv = acpi_eval_struct(sc->sc_node->ad_handle, path, &buf);

       if (ACPI_FAILURE(rv))
               goto out;

       obj = buf.Pointer;

       if (obj->Type != ACPI_TYPE_PACKAGE) {
               rv = AE_TYPE;
               goto out;
       }

       elm = obj->Package.Elements;

       if (elm[0].Type != ACPI_TYPE_INTEGER) {
               rv = AE_TYPE;
               goto out;
       }

       if (elm[0].Integer.Value > UINT32_MAX) {
               rv = AE_AML_NUMERIC_OVERFLOW;
               goto out;
       }

       n = elm[0].Integer.Value;

       if (n == 0) {
               rv = AE_NOT_EXIST;
               goto out;
       }

       if (obj->Package.Count - 1 != n) {
               rv = AE_BAD_VALUE;
               goto out;
       }

       for (i = 1; i < obj->Package.Count; i++) {

               if (elm[i].Type != ACPI_TYPE_PACKAGE)
                       continue;

               aibs_sensor_add(self, &elm[i]);
       }

out:
       if (buf.Pointer != NULL)
               ACPI_FREE(buf.Pointer);

       if (ACPI_FAILURE(rv)) {

               aprint_error_dev(self, "failed to evaluate "
                   "%s: %s\n", path, AcpiFormatException(rv));
       }
}

static void
aibs_sensor_add(device_t self, ACPI_OBJECT *obj)
{
       struct aibs_softc *sc = device_private(self);
       struct aibs_sensor *as;
       int ena, len, lhi, llo;
       const char *name;
       ACPI_STATUS rv;

       as = NULL;
       rv = AE_OK;

       if (obj->Type != ACPI_TYPE_PACKAGE) {
               rv = AE_TYPE;
               goto out;
       }

       /*
        * The known formats are:
        *
        *      index           type            old             new
        *      -----           ----            ---             ---
        *      0               integer         flags           flags
        *      1               string          name            name
        *      2               integer         limit1          unknown
        *      3               integer         limit2          unknown
        *      4               integer         enable          limit1
        *      5               integer         -               limit2
        *      6               integer         -               enable
        */
       if (sc->sc_model != false) {
               len = 7;
               llo = 4;
               lhi = 5;
               ena = 6;
       } else {
               len = 5;
               llo = 2;
               lhi = 3;
               ena = 4;
       }

       if (obj->Package.Count != (uint32_t)len) {
               rv = AE_LIMIT;
               goto out;
       }

       if (obj->Package.Elements[0].Type != ACPI_TYPE_INTEGER ||
           obj->Package.Elements[1].Type != ACPI_TYPE_STRING ||
           obj->Package.Elements[llo].Type != ACPI_TYPE_INTEGER ||
           obj->Package.Elements[lhi].Type != ACPI_TYPE_INTEGER ||
           obj->Package.Elements[ena].Type != ACPI_TYPE_INTEGER) {
               rv = AE_TYPE;
               goto out;
       }

       as = kmem_zalloc(sizeof(*as), KM_SLEEP);

       name = obj->Package.Elements[1].String.Pointer;

       as->as_type = obj->Package.Elements[0].Integer.Value;
       as->as_liml = obj->Package.Elements[llo].Integer.Value;
       as->as_limh = obj->Package.Elements[lhi].Integer.Value;

       if (sc->sc_model != false)
               as->as_limh += as->as_liml;     /* A range in the new model. */

       as->as_sensor.state = ENVSYS_SINVALID;

       switch (AIBS_TYPE(as->as_type)) {

       case AIBS_TYPE_FAN:
               as->as_sensor.units = ENVSYS_SFANRPM;
               as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
               break;

       case AIBS_TYPE_TEMP:
               as->as_sensor.units = ENVSYS_STEMP;
               as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
               break;

       case AIBS_TYPE_VOLT:
               as->as_sensor.units = ENVSYS_SVOLTS_DC;
               as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
               break;

       default:
               rv = AE_TYPE;
               goto out;
       }

       (void)strlcpy(as->as_sensor.desc, name, sizeof(as->as_sensor.desc));

       if (sysmon_envsys_sensor_attach(sc->sc_sme, &as->as_sensor) != 0) {
               rv = AE_AML_INTERNAL;
               goto out;
       }

       SIMPLEQ_INSERT_TAIL(&sc->as_head, as, as_list);

out:
       if (ACPI_FAILURE(rv)) {

               if (as != NULL)
                       kmem_free(as, sizeof(*as));

               aprint_error_dev(self, "failed to add "
                   "sensor: %s\n",  AcpiFormatException(rv));
       }
}

static bool
aibs_sensor_value(device_t self, struct aibs_sensor *as, uint64_t *val)
{
       struct aibs_softc *sc = device_private(self);
       uint32_t type, *ret, cmb[3];
       ACPI_OBJECT_LIST arg;
       ACPI_OBJECT cmi, tmp;
       ACPI_OBJECT *obj;
       ACPI_BUFFER buf;
       ACPI_STATUS rv;
       const char *path;

       if (sc->sc_model != false) {

               path = "GITM";

               cmb[0] = as->as_type;
               cmb[1] = 0;
               cmb[2] = 0;

               arg.Count = 1;
               arg.Pointer = &tmp;

               tmp.Buffer.Length = sizeof(cmb);
               tmp.Buffer.Pointer = (uint8_t *)cmb;
               tmp.Type = type = ACPI_TYPE_BUFFER;

       } else {

               arg.Count = 1;
               arg.Pointer = &cmi;

               cmi.Integer.Value = as->as_type;
               cmi.Type = type = ACPI_TYPE_INTEGER;

               switch (AIBS_TYPE(as->as_type)) {

               case AIBS_TYPE_FAN:
                       path = "RFAN";
                       break;

               case AIBS_TYPE_TEMP:
                       path = "RTMP";
                       break;

               case AIBS_TYPE_VOLT:
                       path = "RVLT";
                       break;

               default:
                       return false;
               }
       }

       buf.Pointer = NULL;
       buf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

       rv = AcpiEvaluateObject(sc->sc_node->ad_handle, path, &arg, &buf);

       if (ACPI_FAILURE(rv))
               goto out;

       obj = buf.Pointer;

       if (obj->Type != type) {
               rv = AE_TYPE;
               goto out;
       }

       if (sc->sc_model != true)
               *val = obj->Integer.Value;
       else {
               /*
                * The return buffer contains at least:
                *
                *      uint32_t buf[0]  flags
                *      uint32_t buf[1]  return value
                *      uint8_t  buf[2-] unknown
                */
               if (obj->Buffer.Length < 8) {
                       rv = AE_BUFFER_OVERFLOW;
                       goto out;
               }

               ret = (uint32_t *)obj->Buffer.Pointer;

               if (ret[0] == 0) {
                       rv = AE_BAD_VALUE;
                       goto out;
               }

               *val = ret[1];
       }

out:
       if (buf.Pointer != NULL)
               ACPI_FREE(buf.Pointer);

       if (ACPI_FAILURE(rv)) {

               aprint_error_dev(self, "failed to evaluate "
                   "%s: %s\n", path, AcpiFormatException(rv));

               return false;
       }

       return true;
}

static void
aibs_sensor_refresh(struct sysmon_envsys *sme, envsys_data_t *edata)
{
       struct aibs_softc *sc = sme->sme_cookie;
       struct aibs_sensor *tmp, *as = NULL;
       envsys_data_t *s = edata;
       uint64_t val = 0;

       SIMPLEQ_FOREACH(tmp, &sc->as_head, as_list) {

               if (tmp->as_sensor.sensor == s->sensor) {
                       as = tmp;
                       break;
               }
       }

       if (as == NULL) {
               aprint_debug_dev(sc->sc_dev, "failed to find sensor\n");
               return;
       }

       as->as_sensor.state = ENVSYS_SINVALID;
       as->as_sensor.flags |= ENVSYS_FMONNOTSUPP;

       if (aibs_sensor_value(sc->sc_dev, as, &val) != true)
               return;

       switch (as->as_sensor.units) {

       case ENVSYS_SFANRPM:
               as->as_sensor.value_cur = val;
               break;

       case ENVSYS_STEMP:

               if (val == 0)
                       return;

               as->as_sensor.value_cur = val * 100 * 1000 + 273150000;
               break;

       case ENVSYS_SVOLTS_DC:
               as->as_sensor.value_cur = val * 1000;
               break;

       default:
               return;
       }

       as->as_sensor.state = ENVSYS_SVALID;
       as->as_sensor.flags &= ~ENVSYS_FMONNOTSUPP;
}

static void
aibs_sensor_limits(struct sysmon_envsys *sme, envsys_data_t *edata,
   sysmon_envsys_lim_t *limits, uint32_t *props)
{
       struct aibs_softc *sc = sme->sme_cookie;
       struct aibs_sensor *tmp, *as = NULL;
       sysmon_envsys_lim_t *lim = limits;
       envsys_data_t *s = edata;

       SIMPLEQ_FOREACH(tmp, &sc->as_head, as_list) {

               if (tmp->as_sensor.sensor == s->sensor) {
                       as = tmp;
                       break;
               }
       }

       if (as == NULL) {
               aprint_debug_dev(sc->sc_dev, "failed to find sensor\n");
               return;
       }

       switch (as->as_sensor.units) {

       case ENVSYS_SFANRPM:

               /*
                * Some boards have strange limits for fans.
                */
               if (as->as_liml == 0) {
                       lim->sel_warnmin = as->as_limh;
                       *props = PROP_WARNMIN;

               } else {
                       lim->sel_warnmin = as->as_liml;
                       lim->sel_warnmax = as->as_limh;
                       *props = PROP_WARNMIN | PROP_WARNMAX;
               }

               break;

       case ENVSYS_STEMP:
               lim->sel_critmax = as->as_limh * 100 * 1000 + 273150000;
               lim->sel_warnmax = as->as_liml * 100 * 1000 + 273150000;

               *props = PROP_CRITMAX | PROP_WARNMAX;
               break;

       case ENVSYS_SVOLTS_DC:
               lim->sel_critmin = as->as_liml * 1000;
               lim->sel_critmax = as->as_limh * 1000;
               *props = PROP_CRITMIN | PROP_CRITMAX;
               break;

       default:
               return;
       }
}

MODULE(MODULE_CLASS_DRIVER, aibs, "sysmon_envsys");

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

static int
aibs_modcmd(modcmd_t cmd, void *aux)
{
       int rv = 0;

       switch (cmd) {

       case MODULE_CMD_INIT:

#ifdef _MODULE
               rv = config_init_component(cfdriver_ioconf_aibs,
                   cfattach_ioconf_aibs, cfdata_ioconf_aibs);
#endif
               break;

       case MODULE_CMD_FINI:

#ifdef _MODULE
               rv = config_fini_component(cfdriver_ioconf_aibs,
                   cfattach_ioconf_aibs, cfdata_ioconf_aibs);
#endif
               break;

       default:
               rv = ENOTTY;
       }

       return rv;
}