## Transceptor SX1262 para MicroPython VF (modo LoRa, compatible con Meshtastic)
from utime import sleep_ms, sleep_us, ticks_ms, ticks_us, ticks_diff
from machine import SPI, Pin
from transceptor1262_const import *

def _afirmar(estado):
    if estado != ERR_NONE:
        raise AssertionError(ERROR.get(estado, "ERR_UNKNOWN"))

def _ceder():
    sleep_ms(1)

class Transceptor1262:
    def __init__(self, spi_bus, clk, mosi, miso, cs, irq, rst, gpio):
        self.spi = SPI(spi_bus, baudrate=2000000, sck=Pin(clk), mosi=Pin(mosi), miso=Pin(miso))
        self.cs = Pin(cs, Pin.OUT)
        self.irq = Pin(irq, Pin.IN)
        self.rst = Pin(rst, Pin.OUT)
        self.gpio = Pin(gpio, Pin.IN)
        self._frecuencia = 0.0
        self._bw_khz = 125.0
        self._sf = 9
        self._cr = 7
        self._crc_activado = True
        self._long_preambulo = 8
        self._tx_iq = False
        self._rx_iq = False
        self._tcxo_voltaje = 0.0
        self._usar_ldo = False

    # --- Funciones internas de bajo nivel ---

    def _reiniciar(self):
        self.rst.value(1)
        sleep_us(150)
        self.rst.value(0)
        sleep_us(150)
        self.rst.value(1)
        sleep_us(150)
        inicio = ticks_ms()
        while True:
            estado = self._modo_standby()
            if estado == ERR_NONE:
                return ERR_NONE
            if abs(ticks_diff(inicio, ticks_ms())) >= 3000:
                return estado
            sleep_ms(10)

    def _modo_standby(self):
        return self._escribir_comando([CMD_SET_STANDBY], [STANDBY_RC])

    def _escribir_comando(self, cmd, datos, esperar_ocupado=True):
        self.cs.value(0)
        inicio = ticks_ms()
        while self.gpio.value():
            if abs(ticks_diff(inicio, ticks_ms())) >= 3000:
                self.cs.value(1)
                return ERR_SPI_CMD_TIMEOUT
            _ceder()
        for b in cmd:
            self.spi.write(bytes([b]))
        for b in datos:
            self.spi.write(bytes([b]))
        self.cs.value(1)
        if esperar_ocupado:
            sleep_us(1)
            inicio = ticks_ms()
            while self.gpio.value():
                if abs(ticks_diff(inicio, ticks_ms())) >= 3000:
                    return ERR_SPI_CMD_TIMEOUT
                _ceder()
        return ERR_NONE

    def _leer_comando(self, comando, nbytes):
        while self.gpio.value():
            _ceder()
        self.cs.value(0)
        self.spi.write(bytearray(comando))
        self.spi.write(b'\x00')
        respuesta = bytearray(nbytes)
        self.spi.readinto(respuesta)
        self.cs.value(1)
        while self.gpio.value():
            _ceder()
        return respuesta

    def _escribir_registro(self, addr, datos):
        cmd = [CMD_WRITE_REGISTER, (addr >> 8) & 0xFF, addr & 0xFF]
        return self._escribir_comando(cmd, datos)

    def _leer_registro(self, addr, num_bytes):
        cmd = [CMD_READ_REGISTER, (addr >> 8) & 0xFF, addr & 0xFF]
        return self._leer_comando(cmd, num_bytes)

    def _escribir_buffer(self, datos):
        cmd = [CMD_WRITE_BUFFER, 0x00]
        return self._escribir_comando(cmd, datos)

    def _leer_buffer(self, longitud):
        cmd = [CMD_READ_BUFFER, CMD_NOP]
        return self._leer_comando(cmd, longitud)

    # --- Configuración y calibración ---

    def _establecer_frecuencia_rf(self, frf):
        datos = [(frf >> 24) & 0xFF, (frf >> 16) & 0xFF, (frf >> 8) & 0xFF, frf & 0xFF]
        return self._escribir_comando([CMD_SET_RF_FREQUENCY], datos)

    def _iniciar_chip(self):
        estado = self._modo_standby()
        _afirmar(estado)
        estado = self._escribir_comando([CMD_SET_PACKET_TYPE], [PACKET_TYPE_LORA])
        _afirmar(estado)
        estado = self._escribir_comando([CMD_SET_RX_TX_FALLBACK_MODE], [RX_TX_FALLBACK_MODE_STDBY_RC])
        _afirmar(estado)
        modo_reg = REGULATOR_LDO if self._usar_ldo else REGULATOR_DC_DC
        estado = self._escribir_comando([CMD_SET_REGULATOR_MODE], [modo_reg])
        _afirmar(estado)
        estado = self._escribir_comando([CMD_CALIBRATE], [0x7F])
        _afirmar(estado)
        sleep_ms(5)
        while self.gpio.value():
            _ceder()
        estado = self._escribir_comando([CMD_SET_DIO2_AS_RF_SWITCH_CTRL], [DIO2_AS_RF_SWITCH])
        _afirmar(estado)
        return ERR_NONE

    def _establecer_parametros_modulacion(self):
        bw_map = {125: LORA_BW_125_0, 250: LORA_BW_250_0, 500: LORA_BW_500_0}
        if self._bw_khz not in bw_map:
            return ERR_INVALID_BANDWIDTH
        bw_val = bw_map[self._bw_khz]
        cr_val = self._cr - 4
        if not (1 <= cr_val <= 4):
            return ERR_INVALID_CODING_RATE
        if not (5 <= self._sf <= 12):
            return ERR_INVALID_SPREADING_FACTOR
        ldro = LORA_LOW_DATA_RATE_OPTIMIZE_ON if ((1 << self._sf) / self._bw_khz) >= 16.0 else LORA_LOW_DATA_RATE_OPTIMIZE_OFF
        datos = [self._sf, bw_val, cr_val, ldro]
        return self._escribir_comando([CMD_SET_MODULATION_PARAMS], datos)

    def _establecer_parametros_paquete(self, longitud=0):
        crc = LORA_CRC_ON if self._crc_activado else LORA_CRC_OFF
        header = LORA_HEADER_EXPLICIT
        iq = LORA_IQ_INVERTED if self._rx_iq else LORA_IQ_STANDARD
        reg_iq = self._leer_registro(REG_IQ_CONFIG, 1)[0]
        if self._rx_iq:
            reg_iq |= 0x04
        else:
            reg_iq &= 0xFB
        self._escribir_registro(REG_IQ_CONFIG, [reg_iq])
        datos = [(self._long_preambulo >> 8) & 0xFF, self._long_preambulo & 0xFF,
                 header, longitud, crc, iq]
        return self._escribir_comando([CMD_SET_PACKET_PARAMS], datos)

    def _establecer_pa(self, potencia):
        estado = self._escribir_comando([CMD_SET_PA_CONFIG],
                                        [0x04, PA_CONFIG_HP_MAX, PA_CONFIG_SX1262, PA_CONFIG_PA_LUT])
        _afirmar(estado)
        return self._escribir_comando([CMD_SET_TX_PARAMS], [potencia, PA_RAMP_200U])

    def _configurar_tcxo(self, voltaje):
        if voltaje <= 0.0:
            return ERR_NONE
        tabla = {
            1.6: DIO3_OUTPUT_1_6,
            1.7: DIO3_OUTPUT_1_7,
            1.8: DIO3_OUTPUT_1_8,
            2.2: DIO3_OUTPUT_2_2,
            2.4: DIO3_OUTPUT_2_4,
            2.7: DIO3_OUTPUT_2_7,
            3.0: DIO3_OUTPUT_3_0,
            3.3: DIO3_OUTPUT_3_3
        }
        if voltaje not in tabla:
            return ERR_INVALID_TCXO_VOLTAGE
        retardo = int(5000 / 15.625)
        datos = [tabla[voltaje], (retardo >> 16) & 0xFF, (retardo >> 8) & 0xFF, retardo & 0xFF]
        return self._escribir_comando([CMD_SET_DIO3_AS_TCXO_CTRL], datos)

    def _establecer_limite_corriente(self, mA):
        if not (0 <= mA <= 140):
            return ERR_INVALID_CURRENT_LIMIT
        raw = int(mA / 2.5)
        return self._escribir_registro(REG_OCP_CONFIGURATION, [raw])

    def _calibrar_imagen(self, freq):
        if freq > 900:
            cal = [CAL_IMG_902_MHZ_1, CAL_IMG_902_MHZ_2]
        elif freq > 850:
            cal = [CAL_IMG_863_MHZ_1, CAL_IMG_863_MHZ_2]
        elif freq > 770:
            cal = [CAL_IMG_779_MHZ_1, CAL_IMG_779_MHZ_2]
        elif freq > 460:
            cal = [CAL_IMG_470_MHZ_1, CAL_IMG_470_MHZ_2]
        else:
            cal = [CAL_IMG_430_MHZ_1, CAL_IMG_430_MHZ_2]
        return self._escribir_comando([CMD_CALIBRATE_IMAGE], cal)

    def _corregir_sujecion_pa(self):
        reg = self._leer_registro(REG_TX_CLAMP_CONFIG, 1)[0]
        reg |= 0x1E
        return self._escribir_registro(REG_TX_CLAMP_CONFIG, [reg])

    def _corregir_sensibilidad(self):
        reg = self._leer_registro(REG_SENSITIVITY_CONFIG, 1)[0]
        if abs(self._bw_khz - 500.0) <= 0.001:
            reg &= 0xFB
        else:
            reg |= 0x04
        return self._escribir_registro(REG_SENSITIVITY_CONFIG, [reg])

    # --- Inicialización completa ---

    def iniciar(self, freq=915.0, bw=125.0, sf=9, cr=7, potencia=14,
                limite_corriente=60.0, longitud_preambulo=8,
                tcxo_voltaje=1.6, usar_ldo=False):

        self._frecuencia = freq
        self._bw_khz = bw
        self._sf = sf
        self._cr = cr
        self._long_preambulo = longitud_preambulo
        self._tcxo_voltaje = tcxo_voltaje
        self._usar_ldo = usar_ldo

        estado = self._reiniciar()
        _afirmar(estado)

        if tcxo_voltaje > 0.0:
            estado = self._configurar_tcxo(tcxo_voltaje)
            _afirmar(estado)

        estado = self._iniciar_chip()
        _afirmar(estado)
        estado = self._establecer_limite_corriente(limite_corriente)
        _afirmar(estado)
        estado = self._establecer_parametros_modulacion()
        _afirmar(estado)
        estado = self._establecer_parametros_paquete()
        _afirmar(estado)
        estado = self._calibrar_imagen(freq)
        _afirmar(estado)
        frf = int((freq * (1 << DIV_EXPONENT)) / CRYSTAL_FREQ)
        estado = self._establecer_frecuencia_rf(frf)
        _afirmar(estado)
        estado = self._establecer_pa(potencia)
        _afirmar(estado)
        estado = self._corregir_sujecion_pa()
        _afirmar(estado)
        estado = self._corregir_sensibilidad()
        _afirmar(estado)

        return ERR_NONE

    # --- Transmisión y recepción ---

    def enviar(self, datos):
        if not isinstance(datos, (bytes, bytearray)):
            return 0, ERR_INVALID_PACKET_TYPE
        if len(datos) > MAX_PACKET_LENGTH:
            return 0, ERR_PACKET_TOO_LONG
        estado = self._modo_standby()
        _afirmar(estado)
        estado = self._establecer_parametros_paquete(len(datos))
        _afirmar(estado)
        irq_mascara = [(IRQ_TX_DONE | IRQ_TIMEOUT) >> 8, (IRQ_TX_DONE | IRQ_TIMEOUT) & 0xFF]
        irq_dio1 = [IRQ_TX_DONE >> 8, IRQ_TX_DONE & 0xFF]
        estado = self._escribir_comando([CMD_SET_DIO_IRQ_PARAMS], irq_mascara + irq_dio1 + [0, 0, 0, 0])
        _afirmar(estado)
        self._escribir_comando([CMD_CLEAR_IRQ_STATUS], [0xFF, 0xFF])
        estado = self._escribir_buffer(list(datos))
        _afirmar(estado)
        estado = self._escribir_comando([CMD_SET_TX], [0x00, 0x00, 0x00])
        _afirmar(estado)
        inicio = ticks_us()
        timeout = int((self._tiempo_aire(len(datos)) * 3) / 2)
        while not self.irq.value():
            if abs(ticks_diff(inicio, ticks_us())) > timeout:
                self._escribir_comando([CMD_SET_STANDBY], [STANDBY_RC])
                return 0, ERR_TX_TIMEOUT
            _ceder()
        self._escribir_comando([CMD_CLEAR_IRQ_STATUS], [0xFF, 0xFF])
        self._modo_standby()
        return len(datos), ERR_NONE

    def recibir(self, longitud=0, timeout_ms=0):
        estado = self._modo_standby()
        _afirmar(estado)
        self._escribir_comando([CMD_SET_PACKET_TYPE], [PACKET_TYPE_LORA])
        if longitud == 0:
            longitud = MAX_PACKET_LENGTH
        estado = self._establecer_parametros_paquete(longitud)
        _afirmar(estado)
        irq_mask = IRQ_RX_DONE | IRQ_TIMEOUT | IRQ_CRC_ERR | IRQ_HEADER_ERR
        irq_mascara = [irq_mask >> 8, irq_mask & 0xFF]
        irq_dio1 = [IRQ_RX_DONE >> 8, IRQ_RX_DONE & 0xFF]
        estado = self._escribir_comando([CMD_SET_DIO_IRQ_PARAMS], irq_mascara + irq_dio1 + [0, 0, 0, 0])
        _afirmar(estado)
        estado = self._escribir_comando([CMD_SET_BUFFER_BASE_ADDRESS], [0x00, 0x00])
        _afirmar(estado)
        self._escribir_comando([CMD_CLEAR_IRQ_STATUS], [0xFF, 0xFF])
        timeout_raw = RX_TIMEOUT_INF if timeout_ms == 0 else int((timeout_ms * 1000) / 15.625)
        estado = self._escribir_comando(
            [CMD_SET_RX],
            [(timeout_raw >> 16) & 0xFF, (timeout_raw >> 8) & 0xFF, timeout_raw & 0xFF]
        )
        _afirmar(estado)
        inicio = ticks_ms()
        while not self.irq.value():
            if timeout_ms > 0 and abs(ticks_diff(inicio, ticks_ms())) > timeout_ms:
                self._modo_standby()
                return b'', ERR_RX_TIMEOUT
            _ceder()
        irq_status = self._leer_comando([CMD_GET_IRQ_STATUS], 2)
        irq_val = (irq_status[0] << 8) | irq_status[1]
        self._escribir_comando([CMD_CLEAR_IRQ_STATUS], [0xFF, 0xFF])
        if irq_val & (IRQ_CRC_ERR | IRQ_HEADER_ERR):
            self._modo_standby()
            return b'', ERR_CRC_MISMATCH
        buf_status = self._leer_comando([CMD_GET_RX_BUFFER_STATUS], 2)
        real_len = buf_status[0]
        read_len = min(longitud, real_len)
        datos = self._leer_buffer(read_len)
        self._modo_standby()
        return bytes(datos), ERR_NONE

    # --- Utilidad interna ---

    def _tiempo_aire(self, longitud):
        symbol_us = int(((1000 * 10) << self._sf) / (self._bw_khz * 10))
        sfCoeff1_x4 = 25 if self._sf in (5, 6) else 17
        sfCoeff2 = 0 if self._sf in (5, 6) else 8
        sfDiv = 4 * (self._sf - 2) if symbol_us >= 16000 else 4 * self._sf
        bits_crc = 16 if self._crc_activado else 0
        n_header = 20
        bit_count = 8 * longitud + bits_crc - 4 * self._sf + sfCoeff2 + n_header
        if bit_count < 0:
            bit_count = 0
        n_precoded = (bit_count + sfDiv - 1) // sfDiv
        n_sym_x4 = (self._long_preambulo + 8) * 4 + sfCoeff1_x4 + n_precoded * (self._cr + 4) * 4
        return (symbol_us * n_sym_x4) // 4
