# Support for common SPI based thermocouple and RTD temperature sensors
#
# Copyright (C) 2018  Petri Honkala <cruwaller@gmail.com>
# Copyright (C) 2018  Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math

# Sensor types defined in the micro-controller code (thermocouple.c)
TS_CHIP_MAX31855 = 1 << 0
TS_CHIP_MAX31856 = 1 << 1
TS_CHIP_MAX31865 = 1 << 2
TS_CHIP_MAX6675  = 1 << 3


######################################################################
# SensorBase
######################################################################

REPORT_TIME = 0.300

class SensorBase:
    def __init__(self, config, chip_type, config_cmd=None):
        self.printer = config.get_printer()
        self._callback = None
        self.min_sample_value = self.max_sample_value = 0
        self._report_clock = 0
        ppins = config.get_printer().lookup_object('pins')
        pin_params = ppins.lookup_pin(config.get('sensor_pin'))
        self.mcu = mcu = pin_params['chip']
        pin = pin_params['pin']
        # SPI bus configuration
        spi_oid = mcu.create_oid()
        spi_speed = config.getint('spi_speed', 4000000, minval=1)
        mcu.add_config_cmd(
            "config_spi oid=%u bus=0 pin=%s mode=1 rate=%u shutdown_msg=" % (
                spi_oid, pin, spi_speed))
        if config_cmd is not None:
            config_cmd = "".join("%02x" % b for b in config_cmd)
            mcu.add_config_cmd("spi_send oid=%u data=%s" % (
                spi_oid, config_cmd), is_init=True)
        # Reader chip configuration
        self.oid = oid = mcu.create_oid()
        mcu.add_config_cmd(
            "config_thermocouple oid=%u spi_oid=%u chip_type=%u" % (
                oid, spi_oid, chip_type))
        mcu.register_msg(self._handle_spi_response,
            "thermocouple_result", oid)
        mcu.register_config_callback(self._build_config)
    def setup_minmax(self, min_temp, max_temp):
        adc_range = [self.calc_adc(min_temp), self.calc_adc(max_temp)]
        self.min_sample_value = min(adc_range)
        self.max_sample_value = max(adc_range)
    def setup_callback(self, cb):
        self._callback = cb
    def get_report_time_delta(self):
        return REPORT_TIME
    def _build_config(self):
        clock = self.mcu.get_query_slot(self.oid)
        self._report_clock = self.mcu.seconds_to_clock(REPORT_TIME)
        self.mcu.add_config_cmd(
            "query_thermocouple oid=%u clock=%u rest_ticks=%u"
            " min_value=%u max_value=%u" % (
                self.oid, clock, self._report_clock,
                self.min_sample_value, self.max_sample_value), is_init=True)
    def _handle_spi_response(self, params):
        temp = self.calc_temp(params['value'], params['fault'])
        next_clock      = self.mcu.clock32_to_clock64(params['next_clock'])
        last_read_clock = next_clock - self._report_clock
        last_read_time  = self.mcu.clock_to_print_time(last_read_clock)
        self._callback(last_read_time, temp)
    def fault(self, msg):
        self.printer.invoke_async_shutdown(msg)


######################################################################
# MAX31856 thermocouple
######################################################################

MAX31856_CR0_REG           = 0x00
MAX31856_CR0_AUTOCONVERT   = 0x80
MAX31856_CR0_1SHOT         = 0x40
MAX31856_CR0_OCFAULT1      = 0x20
MAX31856_CR0_OCFAULT0      = 0x10
MAX31856_CR0_CJ            = 0x08
MAX31856_CR0_FAULT         = 0x04
MAX31856_CR0_FAULTCLR      = 0x02
MAX31856_CR0_FILT50HZ      = 0x01
MAX31856_CR0_FILT60HZ      = 0x00

MAX31856_CR1_REG           = 0x01
MAX31856_CR1_AVGSEL1       = 0x00
MAX31856_CR1_AVGSEL2       = 0x10
MAX31856_CR1_AVGSEL4       = 0x20
MAX31856_CR1_AVGSEL8       = 0x30
MAX31856_CR1_AVGSEL16      = 0x70

MAX31856_MASK_REG                          = 0x02
MAX31856_MASK_COLD_JUNCTION_HIGH_FAULT     = 0x20
MAX31856_MASK_COLD_JUNCTION_LOW_FAULT      = 0x10
MAX31856_MASK_THERMOCOUPLE_HIGH_FAULT      = 0x08
MAX31856_MASK_THERMOCOUPLE_LOW_FAULT       = 0x04
MAX31856_MASK_VOLTAGE_UNDER_OVER_FAULT     = 0x02
MAX31856_MASK_THERMOCOUPLE_OPEN_FAULT      = 0x01

MAX31856_CJHF_REG          = 0x03
MAX31856_CJLF_REG          = 0x04
MAX31856_LTHFTH_REG        = 0x05
MAX31856_LTHFTL_REG        = 0x06
MAX31856_LTLFTH_REG        = 0x07
MAX31856_LTLFTL_REG        = 0x08
MAX31856_CJTO_REG          = 0x09
MAX31856_CJTH_REG          = 0x0A
MAX31856_CJTL_REG          = 0x0B
MAX31856_LTCBH_REG         = 0x0C
MAX31856_LTCBM_REG         = 0x0D
MAX31856_LTCBL_REG         = 0x0E

MAX31856_SR_REG            = 0x0F
MAX31856_FAULT_CJRANGE     = 0x80  # Cold Junction out of range
MAX31856_FAULT_TCRANGE     = 0x40  # Thermocouple out of range
MAX31856_FAULT_CJHIGH      = 0x20  # Cold Junction High
MAX31856_FAULT_CJLOW       = 0x10  # Cold Junction Low
MAX31856_FAULT_TCHIGH      = 0x08  # Thermocouple Low
MAX31856_FAULT_TCLOW       = 0x04  # Thermocouple Low
MAX31856_FAULT_OVUV        = 0x02  # Under Over Voltage
MAX31856_FAULT_OPEN        = 0x01

MAX31856_SCALE = 5
MAX31856_MULT = 0.0078125

class MAX31856(SensorBase):
    def __init__(self, config):
        SensorBase.__init__(self, config, TS_CHIP_MAX31856,
                            self.build_spi_init(config))
    def calc_temp(self, adc, fault):
        if fault & MAX31856_FAULT_CJRANGE:
            self.fault("Max31856: Cold Junction Range Fault")
        if fault & MAX31856_FAULT_TCRANGE:
            self.fault("Max31856: Thermocouple Range Fault")
        if fault & MAX31856_FAULT_CJHIGH:
            self.fault("Max31856: Cold Junction High Fault")
        if fault & MAX31856_FAULT_CJLOW:
            self.fault("Max31856: Cold Junction Low Fault")
        if fault & MAX31856_FAULT_TCHIGH:
            self.fault("Max31856: Thermocouple High Fault")
        if fault & MAX31856_FAULT_TCLOW:
            self.fault("Max31856: Thermocouple Low Fault")
        if fault & MAX31856_FAULT_OVUV:
            self.fault("Max31856: Over/Under Voltage Fault")
        if fault & MAX31856_FAULT_OPEN:
            self.fault("Max31856: Thermocouple Open Fault")
        adc = adc >> MAX31856_SCALE
        # Fix sign bit:
        if adc & 0x40000:
            adc = ((adc & 0x3FFFF) + 1) * -1
        temp = MAX31856_MULT * adc
        return temp
    def calc_adc(self, temp):
        adc = int( ( temp / MAX31856_MULT ) + 0.5 ) # convert to ADC value
        adc = adc << MAX31856_SCALE
        return adc
    def build_spi_init(self, config):
        cmds = []
        value = MAX31856_CR0_AUTOCONVERT
        if config.getboolean('tc_use_50Hz_filter', False):
            value |= MAX31856_CR0_FILT50HZ
        cmds.append(0x80 + MAX31856_CR0_REG)
        cmds.append(value)

        types = {
            "B" : 0b0000,
            "E" : 0b0001,
            "J" : 0b0010,
            "K" : 0b0011,
            "N" : 0b0100,
            "R" : 0b0101,
            "S" : 0b0110,
            "T" : 0b0111,
        }
        value = config.getchoice('tc_type', types, default="K")
        averages = {
            "1"  : MAX31856_CR1_AVGSEL1,
            "2"  : MAX31856_CR1_AVGSEL2,
            "4"  : MAX31856_CR1_AVGSEL4,
            "8"  : MAX31856_CR1_AVGSEL8,
            "16" : MAX31856_CR1_AVGSEL16
        }
        value |= config.getchoice('tc_averaging_count', averages, "1")
        cmds.append(0x80 + MAX31856_CR1_REG)
        cmds.append(value)

        value = (MAX31856_MASK_VOLTAGE_UNDER_OVER_FAULT |
                 MAX31856_MASK_THERMOCOUPLE_OPEN_FAULT)
        cmds.append(0x80 + MAX31856_MASK_REG)
        cmds.append(value)
        return cmds


######################################################################
# MAX31855 thermocouple
######################################################################

MAX31855_SCALE = 18
MAX31855_MULT = 0.25

class MAX31855(SensorBase):
    def __init__(self, config):
        SensorBase.__init__(self, config, TS_CHIP_MAX31855)
    def calc_temp(self, adc, fault):
        if adc & 0x1:
            self.fault("MAX31855 : Open Circuit")
        if adc & 0x2:
            self.fault("MAX31855 : Short to GND")
        if adc & 0x4:
            self.fault("MAX31855 : Short to Vcc")
        adc = adc >> MAX31855_SCALE
        # Fix sign bit:
        if adc & 0x2000:
            adc = ((adc & 0x1FFF) + 1) * -1
        temp = MAX31855_MULT * adc
        return temp
    def calc_adc(self, temp):
        adc = int( ( temp / MAX31855_MULT ) + 0.5 ) # convert to ADC value
        adc = adc << MAX31855_SCALE
        return adc


######################################################################
# MAX6675 thermocouple
######################################################################

MAX6675_SCALE = 3
MAX6675_MULT = 0.25

class MAX6675(SensorBase):
    def __init__(self, config):
        SensorBase.__init__(self, config, TS_CHIP_MAX6675)
    def calc_temp(self, adc, fault):
        if adc & 0x02:
            self.fault("Max6675 : Device ID error")
        if adc & 0x04:
            self.fault("Max6675 : Thermocouple Open Fault")
        adc = adc >> MAX6675_SCALE
        # Fix sign bit:
        if adc & 0x2000:
            adc = ((adc & 0x1FFF) + 1) * -1
        temp = MAX6675_MULT * adc
        return temp
    def calc_adc(self, temp):
        adc = int( ( temp / MAX6675_MULT ) + 0.5 ) # convert to ADC value
        adc = adc << MAX6675_SCALE
        return adc


######################################################################
# MAX31865 (RTD sensor)
######################################################################

MAX31865_CONFIG_REG            = 0x00
MAX31865_RTDMSB_REG            = 0x01
MAX31865_RTDLSB_REG            = 0x02
MAX31865_HFAULTMSB_REG         = 0x03
MAX31865_HFAULTLSB_REG         = 0x04
MAX31865_LFAULTMSB_REG         = 0x05
MAX31865_LFAULTLSB_REG         = 0x06
MAX31865_FAULTSTAT_REG         = 0x07

MAX31865_CONFIG_BIAS           = 0x80
MAX31865_CONFIG_MODEAUTO       = 0x40
MAX31865_CONFIG_1SHOT          = 0x20
MAX31865_CONFIG_3WIRE          = 0x10
MAX31865_CONFIG_FAULTCLEAR     = 0x02
MAX31865_CONFIG_FILT50HZ       = 0x01

MAX31865_FAULT_HIGHTHRESH      = 0x80
MAX31865_FAULT_LOWTHRESH       = 0x40
MAX31865_FAULT_REFINLOW        = 0x20
MAX31865_FAULT_REFINHIGH       = 0x10
MAX31865_FAULT_RTDINLOW        = 0x08
MAX31865_FAULT_OVUV            = 0x04

VAL_A = 0.00390830
VAL_B = 0.0000005775
VAL_C = -0.00000000000418301
VAL_ADC_MAX = 32768.0 # 2^15

class MAX31865(SensorBase):
    def __init__(self, config):
        self.rtd_nominal_r = config.getint('rtd_nominal_r', 100)
        self.reference_r = config.getfloat('rtd_reference_r', 430., above=0.)
        SensorBase.__init__(self, config, TS_CHIP_MAX31865,
                            self.build_spi_init(config))
    def calc_temp(self, adc, fault):
        if fault & 0x80:
            self.fault("Max31865 RTD input is disconnected")
        if fault & 0x40:
            self.fault("Max31865 RTD input is shorted")
        if fault & 0x20:
            self.fault("Max31865 VREF- is greater than 0.85 * VBIAS, FORCE- open")
        if fault & 0x10:
            self.fault("Max31865 VREF- is less than 0.85 * VBIAS, FORCE- open")
        if fault & 0x08:
            self.fault("Max31865 VRTD- is less than 0.85 * VBIAS, FORCE- open")
        if fault & 0x04:
            self.fault("Max31865 Overvoltage or undervoltage fault")
        if fault & 0x03:
            self.fault("Max31865 Unspecified error")
        adc = adc >> 1 # remove fault bit
        R_rtd = (self.reference_r * adc) / VAL_ADC_MAX
        temp = (
            (( ( -1 * self.rtd_nominal_r ) * VAL_A ) +
             math.sqrt( ( self.rtd_nominal_r * self.rtd_nominal_r * VAL_A * VAL_A ) -
                        ( 4 * self.rtd_nominal_r * VAL_B * ( self.rtd_nominal_r - R_rtd ) )))
            / (2 * self.rtd_nominal_r * VAL_B))
        return temp
    def calc_adc(self, temp):
        R_rtd = temp * ( 2 * self.rtd_nominal_r * VAL_B )
        R_rtd = math.pow( ( R_rtd + ( self.rtd_nominal_r * VAL_A ) ), 2)
        R_rtd = -1 * ( R_rtd - ( self.rtd_nominal_r * self.rtd_nominal_r * VAL_A * VAL_A ) )
        R_rtd = R_rtd / ( 4 * self.rtd_nominal_r * VAL_B )
        R_rtd = ( -1 * R_rtd ) + self.rtd_nominal_r
        adc = int( ( ( R_rtd * VAL_ADC_MAX ) / self.reference_r) + 0.5 )
        adc = adc << 1 # Add fault bit
        return adc
    def build_spi_init(self, config):
        value = (MAX31865_CONFIG_BIAS |
                 MAX31865_CONFIG_MODEAUTO |
                 MAX31865_CONFIG_FAULTCLEAR)
        if config.getboolean('rtd_use_50Hz_filter', False):
            value |= MAX31865_CONFIG_FILT50HZ
        if config.getint('rtd_num_of_wires', 2) == 3:
            value |= MAX31865_CONFIG_3WIRE
        cmd = 0x80 + MAX31865_CONFIG_REG
        return [cmd, value]


######################################################################
# Sensor registration
######################################################################

Sensors = {
    "MAX6675": MAX6675,
    "MAX31855": MAX31855,
    "MAX31856": MAX31856,
    "MAX31865": MAX31865,
}

def load_config(config):
    # Register sensors
    pheater = config.get_printer().lookup_object("heater")
    for name, klass in Sensors.items():
        pheater.add_sensor(name, klass)
