from machine import I2C, Pin, ADC
import time
import ustruct
import machine

# --- Test Duration ---
TEST_DURATION = 120  # Total duration of test in seconds
start_time = time.time()  # Record start time

# --- Sensor Setup ---
adc = ADC()  # Initialize ADC
soil_adc = adc.channel(pin='P13', attn=ADC.ATTN_11DB)   # Soil moisture sensor
light_adc = adc.channel(pin='P18', attn=ADC.ATTN_11DB)  # Light sensor
water_adc = adc.channel(pin='P14', attn=ADC.ATTN_11DB)  # Water level sensor

# Setup relay and LED pins
relay = Pin('P2', mode=Pin.OUT, value=0)  # Relay control (initially OFF)
led_di = Pin('P23', mode=Pin.OUT)         # Data pin for LED bar (MY9221)
led_dcki = Pin('P21', mode=Pin.OUT)       # Clock pin for LED bar

# --- LED Bar v2.1 (MY9221) Control Functions ---
def send_data(data):
    """Send 8-bit data serially to LED bar (LSB first)."""
    for i in range(8):
        led_dcki.value(0)
        led_di.value((data >> i) & 0x01)
        led_dcki.value(1)

def led_bar_display_v21(level):
    """Update LED bar display with level from 0 to 10."""
    level = max(0, min(level, 10))  # Clamp level between 0–10
    # Start frame (4 zero bytes)
    for _ in range(4):
        send_data(0x00)
    # Light up 'level' segments, turn off others
    for i in range(10):
        if i < level:
            send_data(0xFF)  # High byte
            send_data(0xFF)  # Low byte
        else:
            send_data(0x00)
            send_data(0x00)
    # Latch frame
    led_di.value(0)
    time.sleep_us(200)
    for _ in range(8):
        led_dcki.value(1)
        led_dcki.value(0)

# --- I2C Initialization for BME280 ---
print("Initializing I2C...")
i2c = None
try:
    i2c = I2C(0)
    machine.idle()
    devices = i2c.scan()
    print("I2C Devices Found:", [hex(x) for x in devices])
except Exception as e:
    print("I2C Init Failed:", e)

# --- Threshold Constants ---
DRY_SOIL_THRESHOLD = 100        # Below this = soil is wet
LOW_LIGHT_THRESHOLD = 1000      # Light below this = low light condition
HIGH_TEMP_THRESHOLD = 30        # Temp above this triggers longer watering
WATER_LEVEL_ALERT = 500         # Water tank below this = low water

# --- BME280 Temperature Sensor Class ---
class BME280:
    def __init__(self, i2c, address=0x77):
        self.i2c = i2c
        self.address = address
        try:
            self._load_calibration()  # Read calibration data
            machine.idle()
            self._configure()         # Set configuration
            print("BME280 initialized")
        except Exception as e:
            print("BME280 Init Error:", e)

    def _load_calibration(self):
        """Read temperature calibration data from sensor."""
        calib = self.i2c.readfrom_mem(self.address, 0x88, 24)
        self.dig_T1, self.dig_T2, self.dig_T3 = ustruct.unpack("<Hhh", calib[:6])

    def _configure(self):
        """Write sensor config (normal mode, oversampling x1)."""
        self.i2c.writeto_mem(self.address, 0xF4, bytes([0x27]))

    def read_temp(self):
        """Read temperature in Celsius."""
        try:
            self.i2c.writeto_mem(self.address, 0xF4, bytes([0x27]))
            time.sleep_ms(10)
            data = self.i2c.readfrom_mem(self.address, 0xF7, 3)
            raw_t = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
            var1 = (raw_t / 16384.0 - self.dig_T1 / 1024.0) * self.dig_T2
            var2 = ((raw_t / 131072.0 - self.dig_T1 / 8192.0) ** 2) * self.dig_T3
            return (var1 + var2) / 5120.0
        except Exception as e:
            print("Temp Read Error:", e)
            return 25.0  # Default fallback value

# --- Main Execution ---
try:
    # Initialize BME280 if I2C is available
    bme = BME280(i2c) if i2c else None
    print("Starting 2-minute test...")

    def safe_read(sensor):
        """Safely read sensor value, clamp max to 4095."""
        try:
            return min(sensor.value(), 4095)
        except:
            return 0

    # Define lambda readers
    read_soil = lambda: safe_read(soil_adc)
    read_light = lambda: safe_read(light_adc)
    read_water_level = lambda: safe_read(water_adc)

    def safe_delay(sec):
        """Sleep safely while allowing for interruption or exit."""
        start = time.time()
        while time.time() - start < sec:
            time.sleep(0.3)

    # --- Relay test startup ---
    print("🔧 Forcing Relay ON for 5 seconds for test...")
    relay.value(1)
    time.sleep(5)
    relay.value(0)
    print("✅ Relay Test Complete.\n")

    # --- Main loop ---
    while time.time() - start_time < TEST_DURATION:
        # Read all sensor data
        soil = read_soil()
        light = read_light()
        water = read_water_level()
        temp = bme.read_temp() if bme else 25.0

        # Show soil moisture as LED bar level (scaled 0–10)
        soil_level = int((soil / 4095) * 10)
        led_bar_display_v21(soil_level)

        # Print sensor info
        print("⏱ %ds left | Soil:%d | Light:%d | Water:%d | Temp:%.1f°C" % (
            max(0, int(TEST_DURATION - (time.time() - start_time))),
            soil, light, water, temp
        ))

        # Check water tank
        if water < WATER_LEVEL_ALERT:
            print("⚠️ LOW WATER WARNING!")
        else:
            # Check if soil is dry
            if soil > DRY_SOIL_THRESHOLD:
                print("🌱 Dry soil detected.")
                # Determine watering duration based on light/temp
                duration = 5 if (light > LOW_LIGHT_THRESHOLD or temp > HIGH_TEMP_THRESHOLD) else 2
                print("💧 Watering for %ds" % duration)
                relay.value(1)
                safe_delay(duration)
                relay.value(0)
                print("✅ Watering complete.")
            else:
                print("👌 Soil is wet enough, skipping watering.")

        safe_delay(1)  # Delay 1 second before next reading

    # End of test
    print("\n✅ Test completed")
    relay.value(0)  # Make sure relay is OFF

except Exception as e:
    # Catch and handle any fatal errors
    print("❌ Fatal Error:", e)
    for _ in range(10):
        machine.idle()
        time.sleep(1)
    machine.reset()  # Restart system
