#!/usr/bin/python3

"""
Code associated with the Adafruit Snake Eyes Bonnet for Raspberry Pi.
PYTHON 3 ONLY. Provides convenience functions for reading and filtering
the board's ADC inputs, can run in background as a thread.
Requires adafruit-blinka (CircuitPython APIs for Python on big hardware)
and adafruit-circuitpython-ads1x15.
Does NOT handle button inputs or writing to displays -- other code handles
those tasks. Former is basic GPIO stuff, latter is done by the fbx2 code.
"""

import time
from threading import Thread
import board
import busio
import adafruit_ads1x15.ads1015 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

class AdcChannel():
    """Corresponds to ONE CHANNEL of the Snake Eye Bonnet's ADS1015
       analog-to-digital converter. Provides clipping, optional inversion
       and noise filtering. Output range ('value' element) is always in
       range 0.0 to 1.0."""
    def __init__(self, channel):
        self.channel = channel # AnalogIn P0-P3
        self.enabled = False   # Disabled by default (until config() called)
        self.min_v = 0.0       # Min expected input voltage
        self.max_v = 3.3       # Max expected input voltage
        self.reverse = False   # If True, reverse output range (0.0 -> 1.0)
        self.filter = 0.0      # Noise reduction (0.0 <= n < 1.0)
        self.value = 0.5       # Initial state

    def config(self, **kwargs):
        """Reconfigure one channel of the Snake Eyes ADC. Accepts several
           keyword arguments that override default values/behaviors:
           min_v: Minimum voltage expected from ADC (e.g. 0.0)
           max_v: Maximum voltage expected from ADC (e.g. 3.3)
           reverse: If True, output range will be reversed.
           filter: Weighting applied to old vs new ADC reading. A value of
                   0.0 (the default) means no filtering will be applied.
                   Values approaching 1.0 make new readings slower on the
                   uptake (reducing minor noise) -- a value of 1.0 would
                   just make the original value stick permanently.
           Calling this function will make the corresponding ADC channel
           active (it will be polled in the SnakeEyesBonnet class run()
           function). There is no corresponding disable function."""
        self.enabled = True
        for key, value in kwargs.items():
            if key == "min_v":
                self.min_v = value
            elif key == "max_v":
                self.max_v = value
            elif key == "reverse":
                self.reverse = value
            elif key == "filter":
                self.filter = min(max(value, 0.0), 1.0)
        if self.min_v > self.max_v:
            self.min_v, self.max_v = self.max_v, self.min_v

    def read(self):
        """Poll ADC channel, applying scaling and filtering,
           store in 'value' member as well as return value."""
        if self.enabled:
            voltage = self.channel.voltage
            clipped = min(max(voltage, self.min_v), self.max_v)
            scaled = (clipped - self.min_v) / (self.max_v - self.min_v)
            if self.reverse:
                scaled = 1.0 - scaled
            self.value = (self.value * (self.filter) +
                          scaled * (1.0 - self.filter))
        return self.value

class SnakeEyesBonnet(Thread):
    """SnakeEyesBonnet encapsulates various analog-to-digital converter
       functionality of the Adafruit Snake Eyes Bonnet, providing up to
       four channels of analog input with clipping and filtering, with
       output ranging from 0.0 to 1.0 (rather than specific voltages or
       integer units)."""
    channel_dict = {
        0: ADS.P0,
        1: ADS.P1,
        2: ADS.P2,
        3: ADS.P3
    }

    def __init__(self, *args, **kwargs):
        """SnakeEyesBonnet constructor."""
        super(SnakeEyesBonnet, self).__init__(*args, **kwargs) # Thread
        self.i2c = busio.I2C(board.SCL, board.SDA)
        self.ads = ADS.ADS1015(self.i2c)
        self.ads.gain = 1
        self.period = 1.0 / 60.0  # Polling inverval = 1/60 sec default
        self.print_values = False # Don't print values by default
        self.channel = []
        for index in range(4):
            self.channel.append(AdcChannel(
                                AnalogIn(self.ads, self.channel_dict[index])))

    def setup_channel(self, channel, **kwargs):
        """Configure one ADC channel of the Snake Eyes Bonnet. Pass channel
           number (0 to 3) as well as optional keyword arguments documented
           in AdcChannel.config()."""
        if 0 <= channel <= 3:
            self.channel[channel].config(**kwargs)

    def run(self):
        """Run in loop, polling active Snake Eyes Bonnet ADC channels and
           optionally printing results. Pass 'True' to print output, else
           it will run silently (updating the channel[].value member values).
           Default is False, so if invoked via threading it runs in the
           background. Polling interval is set using the SnakeEyesBonnet
           constructor with optional 'period' keyword argument.
           This function does not return. DO NOT rename this function,
           it's so named to work with Python 3's threading class."""
        while True:
            for channel in self.channel:
                if channel.enabled:
                    channel.read()
                    if self.print_values:
                        print("%.6f" % channel.value, end="  ")
            if self.print_values:
                print(flush=True)
            # Note that the 'period' value is not strictly speaking the
            # polling period, since there will be some overhead for the
            # code above to run, plus occasional garbage collection and
            # such. Period is really just the sleep time here...
            time.sleep(self.period)


# If script is invoked standalone, not imported by another script,
# run example test...
if __name__ == "__main__":
    BONNET = SnakeEyesBonnet()
    BONNET.period = 0.25        # Set up 1/4 second polling interval
    BONNET.print_values = True  # Display output as we go
    # Configure four ADC channels...
    BONNET.setup_channel(0, reverse=False, filter=0.0)
    BONNET.setup_channel(1, reverse=False, filter=0.0)
    BONNET.setup_channel(2, reverse=False, filter=0.0)
    BONNET.setup_channel(3, reverse=False, filter=0.0)
    # Run polling loop "manually" (not as thread)...
    BONNET.run()