from micropython import const
import asyncio
import aioble
import bluetooth
import machine
import neopixel
import ujson as json
import time
import _thread

from machine import Pin, I2C
import bme680

# ----------------- NeoPixel -----------------
NEO_PIN = 8
NEO_COUNT = 1
np = neopixel.NeoPixel(machine.Pin(NEO_PIN), NEO_COUNT)
np[0] = (127, 0, 127)
np.write()

def set_led(cmd: int):
    if cmd == 0:
        np[0] = (0, 0, 0)
    elif cmd == 1:
        np[0] = (255, 0, 0)
    elif cmd == 2:
        np[0] = (0, 255, 0)
    elif cmd == 3:
        np[0] = (0, 0, 255)
    else:
        return
    np.write()

def decode_cmd(data: bytes):
    try:
        if data is None:
            return None
        return int.from_bytes(data, "big")
    except:
        return None

# ----------------- I2C + BME680 -----------------
# Your wiring: GPIO5=SCL, GPIO4=SDA
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400_000)
sensor = bme680.BME680(i2c)

# Shared sensor data (written by thread, read by asyncio)
_lock = _thread.allocate_lock()
_latest = {
    "t_c": None,
    "h_pct": None,
    "p_hpa": None,
    "gas_ohms": None,
    "gas_valid": True,
    "ts": 0,
}

def bme680_thread():
    i = 0
    while True:
        i += 1

        # Gas is slow: do it every 10 seconds
        gas_on = (i % 10 == 0)

        # Fast settings for demo
        sensor.measure(
            gas=gas_on,
            t_os=1, p_os=1, h_os=1,
            iir_filter=0,
            gas_temp=300,
            gas_ms=80
        )
        #sensor.measure(gas=True)
        t = sensor.temperature()
        h = sensor.humidity()
        p = sensor.pressure()
        g = sensor.gas() if gas_on else {"ohms": None, "valid": False}

        with _lock:
            _latest["t_c"] = round(t, 2)
            _latest["h_pct"] = round(h, 2)
            _latest["p_hpa"] = round(p, 2)
            _latest["gas_ohms"] = g["ohms"]
            _latest["gas_valid"] = bool(g["valid"])
            _latest["ts"] = time.time()

        time.sleep(1)

_thread.start_new_thread(bme680_thread, ())

# ----------------- BLE UUIDs -----------------
_BLE_SERVICE_UUID     = bluetooth.UUID("19b10000-e8f2-537e-4f6c-d104768a1214")
_BLE_SENSOR_CHAR_UUID = bluetooth.UUID("19b10001-e8f2-537e-4f6c-d104768a1214")  # notify
_BLE_LED_CHAR_UUID    = bluetooth.UUID("19b10002-e8f2-537e-4f6c-d104768a1214")  # write

_ADV_INTERVAL_MS = const(250_000)

ble_service = aioble.Service(_BLE_SERVICE_UUID)
sensor_characteristic = aioble.Characteristic(ble_service, _BLE_SENSOR_CHAR_UUID, read=True, notify=True)
led_characteristic    = aioble.Characteristic(ble_service, _BLE_LED_CHAR_UUID, read=True, write=True, capture=True)
aioble.register_services(ble_service)

def encode_json(obj) -> bytes:
    return json.dumps(obj).encode("utf-8")

# ----------------- BLE tasks -----------------
async def notify_task():
    last = None
    while True:
        with _lock:
            payload = dict(_latest)

        b = encode_json(payload)
        if b != last:
            sensor_characteristic.write(b, send_update=True)
            last = b

        await asyncio.sleep_ms(500)  # UI feels live at 2 Hz

async def peripheral_task():
    while True:
        try:
            async with await aioble.advertise(
                _ADV_INTERVAL_MS,
                name="ESP32-BME680",
                services=[_BLE_SERVICE_UUID],
            ) as connection:
                print("Connection from", connection.device)
                await connection.disconnected()
                print("Disconnected")
        except Exception as e:
            print("peripheral_task error:", e)
            await asyncio.sleep_ms(200)

async def led_write_task():
    while True:
        try:
            connection, data = await led_characteristic.written()
            cmd = decode_cmd(data)
            print("LED cmd:", cmd, "from", connection.device)
            if cmd is not None:
                set_led(cmd)
        except Exception as e:
            print("led_write_task error:", e)
            await asyncio.sleep_ms(100)

async def main():
    await asyncio.gather(
        notify_task(),
        peripheral_task(),
        led_write_task()
    )

asyncio.run(main())

