/*
 * Copyright (C) 2019 University of Applied Sciences Emden / Leer
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @defgroup    drivers_ph_oem pH OEM sensor device driver
 * @ingroup     drivers_sensors
 * @ingroup     drivers_saul
 * @brief       Device driver for Atlas Scientific pH OEM sensor with SMBus/I2C interface
 *
 * The Atlas Scientific pH OEM sensor can be used with or without the interrupt
 * pin. Per default this pin is mapped to @ref GPIO_UNDEF if not otherwise defined
 * in your makefile.
 *
 * If you use an electrical isolation for most accurate readings
 * e.g. with the ADM3260, keep in mind that its not recommended to use the
 * interrupt pin without also isolating it somehow. The preferred method,
 * if not using an isolation on the interrupt line, would be polling. In this case
 * leave the interrupt pin undefined.
 *
 * The Sensor has no integrated temperature sensor and for the highest possible
 * precision it requires another device to provide the temperature for error
 * compensation.
 *
 * Once the pH OEM is powered on it will be ready to receive commands and take
 * readings after 1ms.
 *
 * @note This driver provides @ref drivers_saul capabilities.
 * Reading (@ref saul_driver_t.read) from the device returns the current pH value.
 * Writing (@ref saul_driver_t.write) a temperature value in celsius to the
 * device sets the temperature compensation. A valid temperature range is
 * 1 - 20000 (0.01 °C  to  200.0 °C)
 *
 * @note Communication is done using SMBus/I2C protocol at speeds
 * of 10-100 kHz. Set your board I2C speed to @ref I2C_SPEED_LOW or
 * @ref I2C_SPEED_NORMAL
 *
 * @{
 *
 * @file
 * @brief       Device driver for Atlas Scientific pH OEM Sensor with SMBus/I2C interface

 * @author      Igor Knippenberg <igor.knippenberg@gmail.com>
 */

#ifndef PH_OEM_H
#define PH_OEM_H

#ifdef __cplusplus
extern "C"
{
#endif

#include "periph/i2c.h"
#include "periph/gpio.h"

/**
 * @brief   Named return values
 */
typedef enum {
    PH_OEM_OK                   =  0,   /**< Everything was fine */
    PH_OEM_NODEV                = -1,   /**< No device found on the bus */
    PH_OEM_READ_ERR             = -2,   /**< Reading to device failed*/
    PH_OEM_WRITE_ERR            = -3,   /**< Writing to device failed */
    PH_OEM_NOT_PH               = -4,   /**< Not an Atlas Scientific pH OEM device */
    PH_OEM_INTERRUPT_GPIO_UNDEF = -5,   /**< Interrupt pin is @ref GPIO_UNDEF */
    PH_OEM_GPIO_INIT_ERR        = -6,   /**< Error while initializing GPIO PIN */
    PH_OEM_TEMP_OUT_OF_RANGE    = -7    /**< Temperature is out of range */
} ph_oem_named_returns_t;

/**
 * @brief   LED state values
 */
typedef enum {
    PH_OEM_LED_ON   = 0x01, /**< LED on state */
    PH_OEM_LED_OFF  = 0x00, /**< LED off state */
} ph_oem_led_state_t;

/**
 * @brief   Device state values
 */
typedef enum {
    PH_OEM_TAKE_READINGS    = 0x01, /**< Device active state */
    PH_OEM_STOP_READINGS    = 0x00, /**< Device hibernate state */
} ph_oem_device_state_t;
/**
 * @brief   Interrupt pin option values
 */
typedef enum {
    PH_OEM_IRQ_RISING   = 0x02, /**< Pin high on new reading (manually reset) */
    PH_OEM_IRQ_FALLING  = 0x04, /**< Pin low on new reading (manually reset) */
    PH_OEM_IRQ_BOTH     = 0x08, /**< Invert state on new reading (automatically reset) */
} ph_oem_irq_option_t;

/**
 * @brief   Calibration option values
 */
typedef enum {
    PH_OEM_CALIBRATE_LOW_POINT  = 0x02,     /**< Low point calibration option */
    PH_OEM_CALIBRATE_MID_POINT  = 0x03,     /**< Mid point calibration option */
    PH_OEM_CALIBRATE_HIGH_POINT = 0x04,     /**< High point calibration option */
} ph_oem_calibration_option_t;

/**
 * @brief   pH OEM sensor params
 */
typedef struct ph_oem_params {
    i2c_t i2c;                      /**< I2C device the sensor is connected to */
    uint8_t addr;                   /**< the slave address of the sensor on the I2C bus */
    gpio_t interrupt_pin;           /**< interrupt pin (@ref GPIO_UNDEF if not defined) */
    gpio_mode_t gpio_mode;          /**< gpio mode of the interrupt pin */
    ph_oem_irq_option_t irq_option; /**< behavior of the interrupt pin, disabled by default */
} ph_oem_params_t;

/**
 * @brief   pH OEM interrupt pin callback
 */
typedef void (*ph_oem_interrupt_pin_cb_t)(void *);

/**
 * @brief   pH OEM device descriptor
 */
typedef struct ph_oem {
    ph_oem_params_t params;         /**< device driver configuration */
    ph_oem_interrupt_pin_cb_t cb;   /**< interrupt pin callback */
    void *arg;                      /**< interrupt pin callback param */
} ph_oem_t;

/**
 * @brief   Initialize a pH OEM sensor
 *
 * @param[in,out]   dev      device descriptor
 * @param[in]       params   device configuration
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_NODEV if no device is found on the bus
 * @return @ref PH_OEM_NOT_PH if the device found at the address is not a pH OEM device
 * @return
 */
int ph_oem_init(ph_oem_t *dev, const ph_oem_params_t *params);

/**
 * @brief   Sets a new address to the pH OEM device by unlocking the
 *          @ref PH_OEM_REG_UNLOCK register and  writing a new address to
 *          the @ref PH_OEM_REG_ADDRESS register.
 *          The device address will also be updated in the device descriptor so
 *          it is still usable.
 *
 *          Settings are retained in the sensor if the power is cut.
 *
 *          The address in the device descriptor will reverse to the default
 *          address you provided through PH_OEM_PARAM_ADDR after the
 *          microcontroller restarts
 *
 * @param[in] dev   device descriptor
 * @param[in] addr  new address for the device. Range: 0x01 - 0x7f
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 */
int ph_oem_set_i2c_address(ph_oem_t *dev, uint8_t addr);

/**
 * @brief   Enable the pH OEM interrupt pin if @ref ph_oem_params_t.interrupt_pin
 *          is defined.
 *          @note @ref ph_oem_reset_interrupt_pin needs to be called in the
 *          callback if you use @ref PH_OEM_IRQ_FALLING or @ref PH_OEM_IRQ_RISING
 *
 *          @note Provide the PH_OEM_PARAM_INTERRUPT_OPTION flag in your
 *          makefile. Valid options see: @ref ph_oem_irq_option_t.
 *          The default is @ref PH_OEM_IRQ_BOTH.
 *
 *          @note Also provide the @ref gpio_mode_t as a CFLAG in your makefile.
 *          Most likely @ref GPIO_IN. If the pin is to sensitive use
 *          @ref GPIO_IN_PU for @ref PH_OEM_IRQ_FALLING or
 *          @ref GPIO_IN_PD for @ref PH_OEM_IRQ_RISING and
 *          @ref PH_OEM_IRQ_BOTH. The default is @ref GPIO_IN_PD
 *
 *
 * @param[in] dev       device descriptor
 * @param[in] cb        callback called when the pH OEM interrupt pin fires
 * @param[in] arg       callback argument
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 * @return @ref PH_OEM_INTERRUPT_GPIO_UNDEF if the interrupt pin is undefined
 * @return @ref PH_OEM_GPIO_INIT_ERR if initializing the interrupt gpio pin failed
 */
int ph_oem_enable_interrupt(ph_oem_t *dev, ph_oem_interrupt_pin_cb_t cb,
                            void *arg);

/**
 * @brief   The interrupt pin will not auto reset on option @ref PH_OEM_IRQ_RISING
 *          and @ref PH_OEM_IRQ_FALLING after interrupt fires,
 *          so call this function again to reset the pin state.
 *
 * @note    The interrupt settings are not retained if the power is cut,
 *          so you have to call this function again after powering on the device.
 *
 * @param[in] dev    device descriptor
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 */
int ph_oem_reset_interrupt_pin(const ph_oem_t *dev);

/**
 * @brief   Set the LED state of the pH OEM sensor by writing to the
 *          @ref PH_OEM_REG_LED register
 *
 * @param[in] dev       device descriptor
 * @param[in] state     @ref ph_oem_led_state_t
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 */
int ph_oem_set_led_state(const ph_oem_t *dev, ph_oem_led_state_t state);

/**
 * @brief   Sets the device state (active/hibernate) of the pH OEM sensor by
 *          writing to the @ref PH_OEM_REG_HIBERNATE register.
 *
 *          @note Once the device has been woken up it will continuously take
 *          readings every 420ms. Waking the device is the only way to take a
 *          reading. Hibernating the device is the only way to stop taking readings.
 *
 * @param[in] dev   device descriptor
 * @param[in] state @ref ph_oem_device_state_t
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 */
int ph_oem_set_device_state(const ph_oem_t *dev, ph_oem_device_state_t state);

/**
 * @brief   Starts a new reading by setting the device state to
 *          @ref PH_OEM_TAKE_READINGS.
 *
 * @note    If the @ref ph_oem_params_t.interrupt_pin is @ref GPIO_UNDEF
 *          this function will poll every 20ms till a reading is done (~420ms)
 *          and stop the device from taking further readings
 *
 * @param[in] dev   device descriptor
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 * @return @ref PH_OEM_READ_ERR if reading from the device failed
 */
int ph_oem_start_new_reading(const ph_oem_t *dev);

/**
 * @brief   Clears all calibrations previously done
 *
 * @param[in] dev   device descriptor
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 * @return @ref PH_OEM_READ_ERR if reading from the device failed
 */
int ph_oem_clear_calibration(const ph_oem_t *dev);

/**
 * @brief   Sets the @ref PH_OEM_REG_CALIBRATION_BASE register to the
 *          calibration_value which the pH OEM sensor will be
 *          calibrated to. Multiply the floating point calibration value of your
 *          solution by 1000 e.g. pH calibration solution => 7.002 * 1000 = 7002 = 0x00001B5A
 *
 *          The calibration value will be saved based on the given
 *          @ref ph_oem_calibration_option_t and retained after the power is cut.
 *
 * @note    Calibrating with @ref PH_OEM_CALIBRATE_MID_POINT will reset the
 *          previous calibrations.
 *          Always start with @ref PH_OEM_CALIBRATE_MID_POINT if you doing
 *          2 or 3 point calibration
 *
 * @param[in] dev                 device descriptor
 * @param[in] calibration_value   pH value multiplied by 1000 e.g 7,002 * 1000 = 7002
 * @param[in] option              @ref ph_oem_calibration_option_t
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 * @return @ref PH_OEM_READ_ERR if reading from the device failed
 */
int ph_oem_set_calibration(const ph_oem_t *dev, uint16_t calibration_value,
                           ph_oem_calibration_option_t option);

/**
 * @brief   Read the @ref PH_OEM_REG_CALIBRATION_CONFIRM register.
 *          After a calibration event has been successfully carried out, the
 *          calibration confirmation register will reflect what calibration has
 *          been done, by setting bits 0 - 2.
 *
 * @param[in]  dev                 device descriptor
 * @param[out] calibration_state   calibration state reflected by bits 0 - 2 <br>
 *                                 (0 = low, 1 = mid, 2 = high)
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_READ_ERR if reading from the device failed
 */
int ph_oem_read_calibration_state(const ph_oem_t *dev, uint16_t *calibration_state);

/**
 * @brief   Sets the @ref PH_OEM_REG_TEMP_COMPENSATION_BASE register to the
 *          temperature_compensation value which the pH OEM sensor will use
 *          to compensate the reading error.
 *          Multiply the floating point temperature value by 100
 *          e.g. temperature in degree Celsius = 34.26 * 100 = 3426
 *
 *  @note   The temperature compensation will not be retained if the power is cut.
 *
 * @param[in] dev                        device descriptor
 * @param[in] temperature_compensation   valid temperature range is
 *                                       1 - 20000 (0.01 °C  to  200.0 °C)
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_WRITE_ERR if writing to the device failed
 * @return @ref PH_OEM_TEMP_OUT_OF_RANGE if the temperature_compensation is not in
 *                                       the valid range
 */
int ph_oem_set_compensation(const ph_oem_t *dev,
                            uint16_t temperature_compensation);

/**
 * @brief   Reads the @ref PH_OEM_REG_TEMP_CONFIRMATION_BASE register to verify
 *          the temperature compensation value that was used to take the pH
 *          reading is set to the correct temperature.
 *
 * @param[in]  dev                       device descriptor
 * @param[out] temperature_compensation  raw temperature compensation value. <br>
 *                                       Divide by 100 for floating point <br>
 *                                       e.g 3426 / 100 = 34.26
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_READ_ERR if reading from the device failed
 */
int ph_oem_read_compensation(const ph_oem_t *dev,
                             uint16_t *temperature_compensation);

/**
 * @brief   Reads the @ref PH_OEM_REG_PH_READING_BASE register to get the current
 *          pH reading.
 *
 * @param[in]  dev        device descriptor
 * @param[out] ph_value   raw pH value <br>
 *                        divide by 1000 for floating point <br>
 *                        e.g 8347 / 1000 = 8.347
 *
 * @return @ref PH_OEM_OK on success
 * @return @ref PH_OEM_READ_ERR if reading from the device failed
 */
int ph_oem_read_ph(const ph_oem_t *dev, uint16_t *ph_value);

#ifdef __cplusplus
}
#endif

#endif /* PH_OEM_H */
/** @} */