# ESP32-S3 MicroPython: Modbus RTU (RS485) + DFPlayer Mini (HW UART) merged
# RS485: UART1  TX=GPIO9  RX=GPIO10  DIR=GPIO18
# DFPlayer: UART2 TX=GPIO40 RX=GPIO41 BUSY=GPIO42 (BUSY typically LOW while playing)

from machine import UART, Pin
import time, struct

# =========================
# DFPLAYER (UART2)
# =========================
DF_UART_ID = 2
DF_TX_PIN  = 21   # ESP32 TX  -> DFPlayer RX
DF_RX_PIN  = 20   # ESP32 RX  <- DFPlayer TX (optional, but harmless)
DF_BUSY_PIN = 47  # DFPlayer BUSY -> ESP32 GPIO (usually LOW when playing)
DF_BAUD = 9600

_START = 0x7E
_VER   = 0xFF
_LEN   = 0x06
_ACK   = 0x00
_END   = 0xEF

CMD_SET_VOLUME  = 0x06
CMD_PLAY_TRACK  = 0x03
CMD_PLAY_FOLDER = 0x0F
CMD_STOP        = 0x16
CMD_RESET       = 0x0C

def _checksum(cmd, p1, p2):
    s = _VER + _LEN + cmd + _ACK + p1 + p2
    c = (0 - s) & 0xFFFF
    return (c >> 8) & 0xFF, c & 0xFF

class DFPlayer:
    def __init__(self, uart: UART, busy_pin=None):
        self.uart = uart
        self.busy = Pin(busy_pin, Pin.IN, Pin.PULL_UP) if busy_pin is not None else None
        time.sleep_ms(800)

    def _send(self, cmd, param=0):
        p1 = (param >> 8) & 0xFF
        p2 = param & 0xFF
        ch, cl = _checksum(cmd, p1, p2)
        frame = bytes([_START, _VER, _LEN, cmd, _ACK, p1, p2, ch, cl, _END])
        self.uart.write(frame)

    def reset(self):
        self._send(CMD_RESET, 0)
        time.sleep_ms(1200)

    def volume(self, level):
        level = max(0, min(30, int(level)))
        self._send(CMD_SET_VOLUME, level)

    def stop(self):
        self._send(CMD_STOP, 0)

    def play_track(self, track_num):
        self._send(CMD_PLAY_TRACK, int(track_num))

    def play_folder_file(self, folder, file):
        folder = max(1, min(99, int(folder)))
        file = max(1, min(255, int(file)))
        self._send(CMD_PLAY_FOLDER, (folder << 8) | file)

    def is_playing(self):
        if self.busy is None:
            return None
        # Typical DFPlayer: BUSY LOW = playing, HIGH = idle
        return self.busy.value() == 0

    def wait_done(self, timeout_ms=20000):
        """Wait for BUSY to go LOW (start) then HIGH (done)."""
        if self.busy is None:
            return False

        t0 = time.ticks_ms()

        # wait for playback to begin (BUSY goes LOW)
        while self.busy.value() == 1:
            if time.ticks_diff(time.ticks_ms(), t0) > 1000:
                return False
            time.sleep_ms(5)

        # wait for playback to finish (BUSY goes HIGH)
        while self.busy.value() == 0:
            if time.ticks_diff(time.ticks_ms(), t0) > timeout_ms:
                return False
            time.sleep_ms(10)

        return True


df_uart = UART(
    DF_UART_ID,
    baudrate=DF_BAUD,
    bits=8,
    parity=None,
    stop=1,
    tx=Pin(DF_TX_PIN),
    rx=Pin(DF_RX_PIN),
    timeout=100
)
player = DFPlayer(df_uart, busy_pin=DF_BUSY_PIN)


# =========================
# MODBUS RTU (RS485 UART1)
# =========================
MB_UART_ID = 1
MB_TX_PIN  = 45
MB_RX_PIN  = 0
MB_BAUD    = 9600

SLAVE_ID = 10

# RS485 direction pin (DE/RE tied together)
RS485_DIR_PIN = 18
rs485_dir = Pin(RS485_DIR_PIN, Pin.OUT)
rs485_dir.value(0)  # receive mode

mb_uart = UART(
    MB_UART_ID,
    baudrate=MB_BAUD,
    bits=8,
    parity=None,
    stop=1,
    tx=Pin(MB_TX_PIN),
    rx=Pin(MB_RX_PIN),
    timeout=200
)

def crc16(data):
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if (crc & 1) else (crc >> 1)
    return crc

def read_holding(start_reg, count):
    frame = bytearray(6)
    frame[0] = SLAVE_ID
    frame[1] = 3  # FC03
    frame[2] = start_reg >> 8
    frame[3] = start_reg & 0xFF
    frame[4] = count >> 8
    frame[5] = count & 0xFF

    frame += struct.pack("<H", crc16(frame))

    # ---- transmit ----
    rs485_dir.value(1)
    time.sleep_us(200)

    mb_uart.write(frame)
    mb_uart.flush()

    time.sleep_us(200)
    rs485_dir.value(0)

    # ---- receive ----
    time.sleep_ms(100)
    resp = mb_uart.read()

    if not resp or len(resp) < 5:
        return None

    # basic sanity
    if resp[0] != SLAVE_ID or resp[1] != 3:
        return None

    byte_count = resp[2]
    if len(resp) < (3 + byte_count + 2):
        return None

    regs = []
    for i in range(0, byte_count, 2):
        regs.append((resp[3+i] << 8) | resp[4+i])
    return regs


# =========================
# APP LOGIC
# =========================
RunOnceOver = False
RunOnceOver1 = False

def decode_env(regs):
    global RunOnceOver
    global RunOnceOver1
    
    counter = regs[0]
    status  = regs[1]
    mode    = regs[2]
    errors  = regs[3]

    temp = struct.unpack(">h", struct.pack(">H", regs[4]))[0] / 100
    rh = regs[5] / 100
    pressure = regs[6] / 10
    iaq = regs[7]

    print("------------")
    print("Counter :", counter)
    print("Status  :", bin(status))
    print("Mode    :", "SIM" if mode else "LIVE")
    print("Errors  :", errors)
    print("Temp    :", temp, "C")
    print("RH      :", rh, "%")
    print("Press   :", pressure, "hPa")
    print("IAQ     :", iaq)
    print("------------\n")

    # Example trigger: if IAQ > 10, play one file once
    if iaq > 100 and not RunOnceOver:
        player.volume(50)
        time.sleep_ms(50)
        print("play")
        # folder=2, file=105 must exist as numeric file in /02/
        player.play_folder_file(2, 105)

        ok = player.wait_done(timeout_ms=25000)
        time.sleep(1)
        print("Audio done:", ok)

        RunOnceOver = True
        RunOnceOver1 = False
        
    if iaq < 100 and not RunOnceOver1:
        player.volume(15)
        time.sleep_ms(50)
        print("play")
        # folder=2, file=105 must exist as numeric file in /02/
        player.play_folder_file(2, 106)

        ok = player.wait_done(timeout_ms=25000)
        time.sleep(1)
        print("Audio done:", ok)
        RunOnceOver = False
        RunOnceOver1 = True

# =========================
# MAIN LOOP
# =========================
while True:
    #player.play_folder_file(2, 106)
    #time.sleep(5)
    regs = read_holding(0, 8)
    if regs:
        decode_env(regs)
    #time.sleep_ms(100)

