Librería Transceptor1262


Presentación

La librería Transceptor1262 es una implementación en MicroPython para controlar el módulo de radio LoRa SX1262, diseñada con compatibilidad con Meshtastic y un enfoque en simplicidad, claridad y funcionalidad esencial. Está pensada para dispositivos embebidos con recursos limitados de las placas ESP32 y permite comunicación LoRa punto a punto usando el protocolo básico de paquetes.

Con este desarrollo se busca llenar una falta de librerías que permitan programar las placas LoRa con micropython, dado que si bien sus fabricantes como en el caso de Heltec https://heltec.org/ afirman que las mismas son programables con Micropython todo su soporte es dado para el uso de arduino, de hecho hasta donde he investigado no existen librerías oficiales para la SX1262 o similares para micropython

En la investigación previa y al realizar una sistemática búsqueda de posibles implementaciones para SX1262 solo pude localizar dos: micropySX126X y SX12XX-LoRa, pero ninguno de los dos proyectos satisfizo el objetivo de lograr una solución para la comunicación vía LoRa con las placas que tenía disponibles.

Para este proyecto utilice dos modelos distintos de placas, espero mas adelante probralo en otros modelos donde tambien funcione, desde ya si alguien lo implementa en otras placas y funciona podre agregar las placas en ese listado:

Vision Master E213
Vision Master E213
WiFi LoRa 32 (V3)
WiFi LoRa 32 V3

Esta librería esta diseñada para inicializar y configurar el chip SX1262 en modo LoRa, lo que permitirá enviar y recibir paquetes de datos por radiofrecuencia para lo cual permite ajustar parámetros de modulación (ancho de banda, factor de dispersión, tasa de codificación, etc.). Prevee el uso de configuraciones comunes (frecuencia, potencia, CRC, etc.) para así permitir su integrarse fácilmente con aplicaciones tipo Meshtastic

Funciones principales de la clase Transceptor1262

1. __init__(...)
Propósito: Inicializa los pines de hardware (SPI, CS, IRQ, RST, GPIO) y establece valores por defecto para la configuración de radio.
Parámetros: Pines físicos y bus SPI.
Configuración inicial: Frecuencia por defecto 915 MHz, BW = 125 kHz, SF = 9, CR = 7, CRC activado, etc.

2. iniciar(...)
Propósito: Realiza la secuencia completa de inicialización del chip SX1262.
Acciones clave:
    • Reinicia el chip.
    • Configura el tipo de paquete (LoRa).
    • Establece regulador (LDO o DC-DC).
    • Calibra osciladores y RF.
    • Configura frecuencia, potencia, parámetros de modulación y paquete.
    • Aplica correcciones de hardware (como PA clamping y sensibilidad).
Parámetros ajustables: frecuencia, ancho de banda, SF, CR, potencia, preámbulo, voltaje TCXO, límite de corriente.

3. enviar(datos)
Propósito: Transmite un paquete de datos vía LoRa.
Funcionamiento:
    • Configura el paquete con la longitud del mensaje.
    • Establece interrupciones para TX.
    • Escribe los datos en el buffer del SX1262.
    • Inicia transmisión y espera confirmación vía IRQ.
    • Calcula un timeout basado en el tiempo en aire del paquete.
Retorno: (bytes_enviados, código_error).

4. recibir(longitud=0, timeout_ms=0)
Propósito: Escucha y recibe un paquete LoRa.
Funcionamiento:
    • Configura interrupciones para RX (éxito, timeout, errores CRC/header).
    • Activa modo RX con timeout programable (0 = infinito).
    • Al detectar IRQ, lee el estado, verifica errores y extrae los datos del buffer.
Retorno: (datos_recibidos, código_error).

Funciones internas (de bajo nivel)

Estas funciones no están pensadas para uso directo, pero sustentan toda la operación:

Gestión de hardware y comandos SPI
_reiniciar(): Reinicia el chip mediante el pin RST.
_modo_standby(): Pone el chip en modo Standby RC.
_escribir_comando() / _leer_comando(): Comunicación SPI con el SX1262.
_escribir_registro() / _leer_registro(): Acceso directo a registros internos.
_escribir_buffer() / _leer_buffer(): Manejo del buffer de TX/RX.

Configuración de radio
_establecer_frecuencia_rf(frf): Configura frecuencia en Hz (convertida a FRF interno).
_establecer_parametros_modulacion(): Ajusta SF, BW, CR y optimización LDR.
_establecer_parametros_paquete(): Controla CRC, preámbulo, tipo de header, inversión IQ.
_establecer_pa(potencia): Configura potencia de transmisión y rampa PA.
_configurar_tcxo(voltaje): Activa control TCXO por DIO3 si se usa oscilador de precisión.
_establecer_limite_corriente(mA): Protege el circuito con límite de corriente en PA.
_calibrar_imagen(freq): Calibración automática según banda de frecuencia.

Correcciones de hardware (erratas conocidas del SX1262)
_corregir_sujecion_pa(): Aplica workaround para sobretensión en PA (errata 3.1).
_corregir_sensibilidad(): Ajusta sensibilidad según ancho de banda (especialmente para 500 kHz).

Utilidades
_tiempo_aire(longitud): Calcula el tiempo estimado de transmisión en microsegundos (usado para timeout).
_afirmar(): Lanza excepción si una operación devuelve error.
_ceder(): Pequeña pausa para evitar bloqueo del bucle principal.

Principales características


✅ Modo LoRa puro, sin capas de protocolo adicionales.
✅ Soporte para configuraciones Meshtastic comunes (915 MHz, SF9, etc.).
✅ Manejo de errores con códigos y excepciones.
✅ Corrección de erratas del chip (PA clamping, sensibilidad).
✅ Cálculo automático de timeout basado en tiempo en aire.
✅ Soporte para TCXO, LDO/DC-DC, límites de corriente.
✅ API simple: solo iniciar(), enviar(), recibir() para uso básico.

La API se irá progresivamente ampliando con nuevas funciones en la medida que resulten necesarias

Compatibilidad con Meshtastic

Aun no se implementa el protocolo completo de Meshtastic (como enrutamiento, compresión, o canales cifrados con PSK) que es el objetivo final de este proyecto, la librería usa los mismos parámetros físicos (PHY) que Meshtastic:
– Frecuencia (ej. 915 MHz en América, 868 MHz en Europa).
– SF = 9, BW = 125 kHz, CR = 7, CRC activado.
– Paquetes sin encriptación, pero compatibles a nivel de onda de radio.
Esto permite que un nodo basado en esta librería pueda enviar mensajes legibles por dispositivos Meshtastic (si se respetan el formato del payload y el canal), y viceversa.

Código de la Librería

El código de la librería está organizado en dos archivos: transceptor1262.py, que contiene la lógica principal, y transceptor1262_const.py, que agrupa todas las constantes. Esta separación hace que el código sea más legible, mantenible y menos propenso a errores

Archivo: transceptor1262.py

## 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
📥 Descargar transceptor1262.py

Archivo: transceptor1262_const.py

# Constantes para SX1262

# Comandos SPI
CMD_NOP = 0x00
CMD_SET_STANDBY = 0x80
CMD_SET_RX = 0x82
CMD_SET_TX = 0x83
CMD_SET_DIO_IRQ_PARAMS = 0x08
CMD_SET_SLEEP = 0x84
CMD_SET_RF_FREQUENCY = 0x86
CMD_SET_PACKET_TYPE = 0x8A
CMD_SET_MODULATION_PARAMS = 0x8B
CMD_SET_PACKET_PARAMS = 0x8C
CMD_SET_PA_CONFIG = 0x95
CMD_SET_RX_TX_FALLBACK_MODE = 0x93
CMD_SET_DIO2_AS_RF_SWITCH_CTRL = 0x9D
CMD_SET_DIO3_AS_TCXO_CTRL = 0x97
CMD_SET_TX_PARAMS = 0x8E
CMD_SET_BUFFER_BASE_ADDRESS = 0x8F
CMD_SET_REGULATOR_MODE = 0x96
CMD_CALIBRATE = 0x89
CMD_CALIBRATE_IMAGE = 0x98
CMD_WRITE_BUFFER = 0x0E
CMD_READ_BUFFER = 0x1E
CMD_GET_IRQ_STATUS = 0x12
CMD_CLEAR_IRQ_STATUS = 0x02
CMD_GET_RX_BUFFER_STATUS = 0x13
CMD_GET_PACKET_STATUS = 0x14
CMD_GET_PACKET_TYPE = 0x11
CMD_WRITE_REGISTER = 0x0D
CMD_READ_REGISTER = 0x1D

# Registros
REG_LORA_SYNC_WORD_MSB = 0x0740
REG_LORA_SYNC_WORD_LSB = 0x0741
REG_IQ_CONFIG = 0x0736
REG_OCP_CONFIGURATION = 0x08E7
REG_TX_CLAMP_CONFIG = 0x08D8
REG_SENSITIVITY_CONFIG = 0x0889

# Valores fijos
MAX_PACKET_LENGTH = 255
CRYSTAL_FREQ = 32.0
DIV_EXPONENT = 25

# Tipos y modos
PACKET_TYPE_LORA = 0x01
STANDBY_RC = 0x00
RX_TX_FALLBACK_MODE_STDBY_RC = 0x20
REGULATOR_LDO = 0x00
REGULATOR_DC_DC = 0x01
DIO2_AS_RF_SWITCH = 0x01

# IRQ
IRQ_TX_DONE = 0b0000000001
IRQ_RX_DONE = 0b0000000010
IRQ_TIMEOUT = 0b1000000000
IRQ_CRC_ERR = 0b0001000000
IRQ_HEADER_ERR = 0b0000100000
IRQ_ALL = 0b1111111111
IRQ_NONE = 0b0000000000

# LoRa
LORA_BW_125_0 = 0x04
LORA_BW_250_0 = 0x05
LORA_BW_500_0 = 0x06
LORA_CR_4_5 = 0x01
LORA_CR_4_6 = 0x02
LORA_CR_4_7 = 0x03
LORA_CR_4_8 = 0x04
LORA_CRC_OFF = 0x00
LORA_CRC_ON = 0x01
LORA_HEADER_EXPLICIT = 0x00
LORA_HEADER_IMPLICIT = 0x01
LORA_IQ_STANDARD = 0x00
LORA_IQ_INVERTED = 0x01
LORA_LOW_DATA_RATE_OPTIMIZE_OFF = 0x00
LORA_LOW_DATA_RATE_OPTIMIZE_ON = 0x01
PA_RAMP_200U = 0x04
PA_CONFIG_HP_MAX = 0x07
PA_CONFIG_PA_LUT = 0x01
PA_CONFIG_SX1262 = 0x00

# TCXO
DIO3_OUTPUT_1_6 = 0x00
DIO3_OUTPUT_1_7 = 0x01
DIO3_OUTPUT_1_8 = 0x02
DIO3_OUTPUT_2_2 = 0x03
DIO3_OUTPUT_2_4 = 0x04
DIO3_OUTPUT_2_7 = 0x05
DIO3_OUTPUT_3_0 = 0x06
DIO3_OUTPUT_3_3 = 0x07

# Calibración de imagen
CAL_IMG_430_MHZ_1 = 0x6B
CAL_IMG_430_MHZ_2 = 0x6F
CAL_IMG_470_MHZ_1 = 0x75
CAL_IMG_470_MHZ_2 = 0x81
CAL_IMG_863_MHZ_1 = 0xD7
CAL_IMG_863_MHZ_2 = 0xDB
CAL_IMG_902_MHZ_1 = 0xE1
CAL_IMG_902_MHZ_2 = 0xE9

# Sync words
SYNC_WORD_PUBLIC = 0x34
SYNC_WORD_PRIVATE = 0x12

# Errores
ERR_NONE = 0
ERR_UNKNOWN = -1
ERR_CHIP_NOT_FOUND = -2
ERR_PACKET_TOO_LONG = -4
ERR_TX_TIMEOUT = -5
ERR_RX_TIMEOUT = -6
ERR_CRC_MISMATCH = -7
ERR_INVALID_BANDWIDTH = -8
ERR_INVALID_SPREADING_FACTOR = -9
ERR_INVALID_CODING_RATE = -10
ERR_INVALID_FREQUENCY = -12
ERR_INVALID_OUTPUT_POWER = -13
ERR_INVALID_CURRENT_LIMIT = -17
ERR_WRONG_MODEM = -20
ERR_INVALID_SYNC_WORD = -105
ERR_INVALID_TCXO_VOLTAGE = -703
ERR_SPI_CMD_TIMEOUT = -705
ERR_INVALID_PACKET_LENGTH = -805

ERROR = {
    0: "ERR_NONE",
    -1: "ERR_UNKNOWN",
    -2: "ERR_CHIP_NOT_FOUND",
    -4: "ERR_PACKET_TOO_LONG",
    -5: "ERR_TX_TIMEOUT",
    -6: "ERR_RX_TIMEOUT",
    -7: "ERR_CRC_MISMATCH",
    -8: "ERR_INVALID_BANDWIDTH",
    -9: "ERR_INVALID_SPREADING_FACTOR",
    -10: "ERR_INVALID_CODING_RATE",
    -12: "ERR_INVALID_FREQUENCY",
    -13: "ERR_INVALID_OUTPUT_POWER",
    -17: "ERR_INVALID_CURRENT_LIMIT",
    -20: "ERR_WRONG_MODEM",
    -105: "ERR_INVALID_SYNC_WORD",
    -703: "ERR_INVALID_TCXO_VOLTAGE",
    -705: "ERR_SPI_CMD_TIMEOUT",
    -805: "ERR_INVALID_PACKET_LENGTH",
}
📥 Descargar transceptor1262_const.py

Implementacion y prueba

Sistema de Prueba para la Librería Transceptor1262 (SX1262)

Los archivos transceptor.json, transceptor.py, receptor.py y emisor.py integran un conjunto de programas que forman un entorno modular y fácil de usar para evaluar, depurar y demostrar las capacidades de comunicación LoRa del módulo SX1262, utilizando una implementación en MicroPython basada en la clase Transceptor1262.

Cada uno de estos archivos tiene una función bien definida:

1. transceptor.json – Archivo de configuración
Contiene todos los parámetros necesarios para inicializar el hardware y la comunicación LoRa, divididos en dos secciones:
pines: asignación de pines GPIO y recursos SPI (CS, IRQ, RST, etc.).
lora: parámetros de la capa física de LoRa (frecuencia, ancho de banda, factor de dispersión, potencia, etc.).
Lo que permite cambiar la configuración sin modificar el código fuente, facilitando pruebas en distintos hardware o escenarios de red.

2. transceptor.py – Inicialización centralizada
Este programa:
– Carga la configuración desde transceptor.json.
– Crea una instancia única del objeto Transceptor1262 con los parámetros adecuados.
– Inicializa el módulo SX1262 y lo deja listo para operar.
– Se diseña para ser importado por otros módulos (emisor.py y receptor.py), exponiendo el objeto transceptor ya inicializado.
Con ello se evita duplicar la lógica de inicialización y garantiza que todos los módulos usen la misma configuración.

3. receptor.py – Modo de recepción continua
Este programa:
– Importa el objeto transceptor ya inicializado.
– Entra en un bucle infinito escuchando mensajes LoRa con un timeout de 15 segundos.
– Muestra en consola cualquier mensaje recibido.
– Informa claramente si ocurre un timeout (error -6) o cualquier otro fallo, usando un diccionario de códigos de error (transceptor1262_const.ERROR).
Ofreciendo un log por consola para depuración en tiempo real y verificación de enlace.

4. emisor.py – Modo de transmisión periódica
Este programa:
– Importa el mismo objeto transceptor compartido.
– Envía mensajes del tipo "Ping N" cada 10 segundos.
– Informa si el envío fue exitoso o si ocurrió un error, traduciendo códigos mediante ERROR.
– Usa un contador para distinguir cada mensaje.
Lo que permite probar la confiabilidad del enlace y la correcta configuración del transmisor.

Funcionamiento:
– Configurar transceptor.json según el hardware y la red LoRa deseada, en ambos dispositivos debe ser idéntica la configuración LoRa.
– Instala MicroPython en ambos dispositivos y subir los archivos transceptor1262.py, transceptor1262_const.py, transceptor.json y transceptor.py.
– En la placa que va a ser emisora subir y ejecutar emisor.py.
– En la placa que va a ser receptora subir y ejecutar receptor.py.
– En la consola del emisor veremos el log del envío de los mensajes.
– En la consola del receptor veremos los mensajes enviados por el emisor que son recibidos.

Codigo de la implementación

Archivo: transceptor.py

import json
import time
from transceptor1262 import Transceptor1262

print("Inicializando SX1262 ...")

# Cargar configuración desde archivo JSON
try:
    with open("transceptor.json", "r") as f:
        config = json.load(f)
    print("[INFO] Configuración cargada desde transceptor.json.")
except Exception as e:
    print(f"[ERROR] No se pudo cargar transceptor.json: {e}")
    raise

# Extraer parámetros
pines = config["pines"]
lora = config["lora"]

# Inicializar transceptor
transceptor = Transceptor1262(
    spi_bus=pines["spi_bus"],
    clk=pines["clk"],
    mosi=pines["mosi"],
    miso=pines["miso"],
    cs=pines["cs"],
    irq=pines["irq"],
    rst=pines["rst"],
    gpio=pines["gpio"]
)

# Iniciar módulo LoRa
try:
    transceptor.iniciar(
        freq=lora["freq"],
        bw=lora["bw"],
        sf=lora["sf"],
        cr=lora["cr"],
        potencia=lora["potencia"],
        limite_corriente=lora["limite_corriente"],
        longitud_preambulo=lora["longitud_preambulo"],
        tcxo_voltaje=lora["tcxo_voltaje"],
        usar_ldo=lora["usar_ldo"]
    )
    print("[INFO] Inicialización completada.")
except Exception as e:
    print(f"[ERROR] Fallo en iniciar(): {e}")
    raise

print("[INFO] Transceptor funcionando...")
📥 Descargar transceptor.py

Archivo: transceptor.json

{
  "pines": {
    "spi_bus": 1,
    "clk": 9,
    "mosi": 10,
    "miso": 11,
    "cs": 8,
    "irq": 14,
    "rst": 12,
    "gpio": 13
  },
  "lora": {
    "freq": 923.0,
    "bw": 500.0,
    "sf": 12,
    "cr": 8,
    "potencia": 14,
    "limite_corriente": 60.0,
    "longitud_preambulo": 8,
    "tcxo_voltaje": 1.7,
    "usar_ldo": false
  }
}
📥 Descargar transceptor.json

Archivo: emisor.py

# emisor.py
from transceptor import transceptor
import time
from transceptor1262_const import ERROR

print("[INFO] Empezando a enviar mensajes...")

counter = 0
while True:
    try:
        mensaje = f"Ping {counter}".encode()
        print(f"[TX] Enviando: {mensaje}")
        bytes_enviados, estado = transceptor.enviar(mensaje)
        if estado == 0:
            print("[TX] Mensaje enviado con éxito.")
        else:
            print(f"[TX] Error al enviar: {ERROR.get(estado, 'Desconocido')}")
        counter += 1
    except Exception as e:
        print(f"[ERROR] Excepción al enviar: {e}")
    time.sleep(10)
📥 Descargar emisor.py

Archivo: receptor.py

# receptor.py
from transceptor import transceptor
import time
from transceptor1262_const import ERROR

print("[INFO] Modo recepción activado...")

while True:
    try:
        msg, err = transceptor.recibir(timeout_ms=15000)  # 15 segundos
        if msg:
            print(f"[RX] ✅ Recibido: {msg}")
        else:
            if err == -6:
                print("[RX] ⏳ Timeout: no se recibió nada.")
            else:
                print(f"[RX] ❌ Error: {ERROR.get(err, err)}")
    except Exception as e:
        print(f"[EXCEPCIÓN] {e}")
    time.sleep(0.1)
📥 Descargar receptor.py

Cerrar