#!/usr/bin/env python3
import argparse
import os
import time
 
import spidev
import RPi.GPIO as GPIO
 
 
CMD_SET_STANDBY = 0x80
CMD_SET_TX = 0x83
CMD_SET_RX = 0x82
CMD_SET_PACKET_TYPE = 0x8A
CMD_SET_RF_FREQUENCY = 0x86
CMD_SET_PA_CONFIG = 0x95
CMD_SET_TX_PARAMS = 0x8E
CMD_SET_BUFFER_BASE = 0x8F
CMD_WRITE_BUFFER = 0x0E
CMD_READ_BUFFER = 0x1E
CMD_GET_RX_BUFFER_STATUS = 0x13
CMD_SET_MODULATION = 0x8B
CMD_SET_PACKET_PARAMS = 0x8C
CMD_SET_DIO_IRQ_PARAMS = 0x08
CMD_GET_IRQ_STATUS = 0x12
CMD_CLEAR_IRQ_STATUS = 0x02
CMD_SET_DIO2_RF_SWITCH = 0x9D
CMD_WRITE_REGISTER = 0x0D
CMD_CALIBRATE_IMAGE = 0x98
CMD_GET_PACKET_STATUS = 0x14
 
PACKET_TYPE_LORA = 0x01
STANDBY_RC = 0x00
RAMP_200U = 0x04
HEADER_EXPLICIT = 0x00
CRC_ON = 0x01
IQ_STANDARD = 0x00
 
IRQ_TX_DONE = 0x0001
IRQ_RX_DONE = 0x0002
IRQ_HEADER_ERR = 0x0020
IRQ_CRC_ERR = 0x0040
IRQ_TIMEOUT = 0x0200
IRQ_ALL = 0x03FF
 
REG_LORA_SYNC_WORD_MSB = 0x0740
 
 
def bw_code(bw_hz):
    table = [
        (9100, 0x00),    # 7.8 kHz
        (13000, 0x08),   # 10.4 kHz
        (18200, 0x01),   # 15.6 kHz
        (26000, 0x09),   # 20.8 kHz
        (36500, 0x02),   # 31.25 kHz
        (52100, 0x0A),   # 41.7 kHz
        (93800, 0x03),   # 62.5 kHz
        (187500, 0x04),  # 125 kHz
        (375000, 0x05),  # 250 kHz
    ]
    for limit, code in table:
        if bw_hz < limit:
            return code
    return 0x06          # 500 kHz
 
 
class SX126x:
    def __init__(self, args):
        self.args = args
        self.spi = spidev.SpiDev()
 
    def open(self):
        dev = f"/dev/spidev{self.args.bus}.{self.args.cs}"
        if not os.path.exists(dev):
            raise FileNotFoundError(f"{dev} not found. Enable SPI first.")
 
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        GPIO.setup(self.args.reset, GPIO.OUT, initial=GPIO.HIGH)
        GPIO.setup(self.args.busy, GPIO.IN)
        if self.args.nss >= 0:
            GPIO.setup(self.args.nss, GPIO.OUT, initial=GPIO.HIGH)
        if self.args.txen >= 0:
            GPIO.setup(self.args.txen, GPIO.OUT, initial=GPIO.HIGH)
        if self.args.rxen >= 0:
            GPIO.setup(self.args.rxen, GPIO.OUT, initial=GPIO.LOW)
 
        self.spi.open(self.args.bus, self.args.cs)
        self.spi.max_speed_hz = self.args.speed
        self.spi.mode = 0
        self.spi.lsbfirst = False
 
        self.reset()
        self.configure_common(255)
 
    def close(self):
        try:
            self.spi.close()
        finally:
            GPIO.cleanup()
 
    def wait_busy(self):
        t0 = time.monotonic()
        while GPIO.input(self.args.busy):
            if time.monotonic() - t0 > self.args.busy_timeout:
                raise TimeoutError("SX126x BUSY pin timeout. Check BUSY GPIO and wiring.")
            time.sleep(0.001)
 
    def reset(self):
        GPIO.output(self.args.reset, GPIO.LOW)
        time.sleep(0.01)
        GPIO.output(self.args.reset, GPIO.HIGH)
        time.sleep(0.02)
        self.wait_busy()
 
    def xfer(self, data):
        if self.args.nss >= 0:
            GPIO.output(self.args.nss, GPIO.LOW)
        try:
            return self.spi.xfer2(list(data))
        finally:
            if self.args.nss >= 0:
                GPIO.output(self.args.nss, GPIO.HIGH)
 
    def cmd(self, op, data=()):
        self.wait_busy()
        self.xfer([op, *data])
        self.wait_busy()
 
    def read_cmd(self, op, prefix=(), length=1):
        self.wait_busy()
        rx = self.xfer([op, *prefix, *([0x00] * length)])
        self.wait_busy()
        return rx[-length:]
 
    def write_register(self, address, data):
        self.cmd(CMD_WRITE_REGISTER, [(address >> 8) & 0xFF, address & 0xFF, *data])
    def clear_irq(self, mask=IRQ_ALL):
        self.cmd(CMD_CLEAR_IRQ_STATUS, [(mask >> 8) & 0xFF, mask & 0xFF])
    def irq_status(self):
        b = self.read_cmd(CMD_GET_IRQ_STATUS, [0x00], 2)
        return (b[0] << 8) | b[1]
 
    def packet_status(self):
        b = self.read_cmd(CMD_GET_PACKET_STATUS, [0x00], 3)
        rssi_pkt = -b[0] / 2.0
        snr = b[1] / 4.0
        if b[1] & 0x80:
            snr = ((b[1] - 256) / 4.0)
        signal_rssi = -b[2] / 2.0
        return rssi_pkt, snr, signal_rssi
 
    def set_dio_irq(self, irq_mask, dio1_mask=0, dio2_mask=0, dio3_mask=0):
        self.cmd(CMD_SET_DIO_IRQ_PARAMS, [
            (irq_mask >> 8) & 0xFF, irq_mask & 0xFF,
            (dio1_mask >> 8) & 0xFF, dio1_mask & 0xFF,
            (dio2_mask >> 8) & 0xFF, dio2_mask & 0xFF,
            (dio3_mask >> 8) & 0xFF, dio3_mask & 0xFF,
        ])
 
    def calibrate_image(self):
        freq = self.args.frequency
        if freq < 446_000_000:
            cal = [0x6B, 0x6F]
        elif freq < 734_000_000:
            cal = [0x75, 0x81]
        elif freq < 828_000_000:
            cal = [0xC1, 0xC5]
        elif freq < 877_000_000:
            cal = [0xD7, 0xDB]
        else:
            cal = [0xE1, 0xE9]
        self.cmd(CMD_CALIBRATE_IMAGE, cal)
 
    def set_frequency(self):
        rf = int(self.args.frequency * 33554432 / 32000000)
        self.calibrate_image()
        self.cmd(CMD_SET_RF_FREQUENCY, [
            (rf >> 24) & 0xFF,
            (rf >> 16) & 0xFF,
            (rf >> 8) & 0xFF,
            rf & 0xFF,
        ])
 
    def set_tx_power(self):
        # SX1262 high-power PA setting, same practical default as the LoRaRF example.
        self.cmd(CMD_SET_PA_CONFIG, [0x04, 0x07, 0x00, 0x01])
        power = max(-9, min(22, self.args.power))
        self.cmd(CMD_SET_TX_PARAMS, [power & 0xFF, RAMP_200U])
 
    def configure_common(self, payload_len):
        self.cmd(CMD_SET_STANDBY, [STANDBY_RC])
        self.cmd(CMD_SET_PACKET_TYPE, [PACKET_TYPE_LORA])
        self.cmd(CMD_SET_DIO2_RF_SWITCH, [0x01])
        self.set_frequency()
        self.set_tx_power()
 
        cr = max(5, min(8, self.args.cr)) - 4
        self.cmd(CMD_SET_MODULATION, [
            max(5, min(12, self.args.sf)),
            bw_code(self.args.bw),
            cr,
            0x00,
        ])
        self.cmd(CMD_SET_PACKET_PARAMS, [
            (self.args.preamble >> 8) & 0xFF,
            self.args.preamble & 0xFF,
            HEADER_EXPLICIT,
            payload_len & 0xFF,
            CRC_ON,
            IQ_STANDARD,
        ])
        self.write_register(REG_LORA_SYNC_WORD_MSB, [
            (self.args.sync_word >> 8) & 0xFF,
            self.args.sync_word & 0xFF,
        ])
        self.cmd(CMD_SET_BUFFER_BASE, [0x00, 0x00])
 
    def set_rf_switch(self, tx):
        if self.args.txen >= 0:
            if self.args.txen_active_high:
                GPIO.output(self.args.txen, GPIO.HIGH if tx else GPIO.LOW)
            else:
                GPIO.output(self.args.txen, GPIO.LOW if tx else GPIO.HIGH)
        if self.args.rxen >= 0:
            if self.args.rxen_active_high:
                GPIO.output(self.args.rxen, GPIO.LOW if tx else GPIO.HIGH)
            else:
                GPIO.output(self.args.rxen, GPIO.HIGH if tx else GPIO.LOW)
 
    def transmit(self, message):
        payload = message.encode("utf-8")[:255]
        self.configure_common(len(payload))
        self.set_dio_irq(IRQ_TX_DONE | IRQ_TIMEOUT, IRQ_TX_DONE | IRQ_TIMEOUT)
        self.clear_irq()
 
        self.wait_busy()
        self.xfer([CMD_WRITE_BUFFER, 0x00, *payload])
        self.wait_busy()
 
        self.set_rf_switch(tx=True)
        timeout = min(0xFFFFFF, max(1, int(self.args.tx_timeout_ms * 64)))
        self.cmd(CMD_SET_TX, [(timeout >> 16) & 0xFF, (timeout >> 8) & 0xFF, timeout & 0xFF])
        last_debug = 0
        while True:
            irq = self.irq_status()
            now = time.monotonic()
            if self.args.debug and now - last_debug >= 1.0:
                print(f"DEBUG TX irq=0x{irq:04X}")
                last_debug = now
            if irq & IRQ_TX_DONE:
                print(f"TX done: {message}")
                break
            if irq & IRQ_TIMEOUT:
                print("TX timeout")
                break
            time.sleep(0.01)
        self.set_rf_switch(tx=False)
        self.clear_irq()
 
    def receive_forever(self):
        self.configure_common(255)
        mask = IRQ_RX_DONE | IRQ_TIMEOUT | IRQ_HEADER_ERR | IRQ_CRC_ERR
        self.set_dio_irq(mask, mask)
        self.clear_irq()
        self.set_rf_switch(tx=False)
        self.cmd(CMD_SET_RX, [0xFF, 0xFF, 0xFF])
        print("RX continuous...")
        last_debug = 0
 
        while True:
            irq = self.irq_status()
            now = time.monotonic()
            if self.args.debug and now - last_debug >= 1.0:
                print(f"DEBUG RX irq=0x{irq:04X}")
                last_debug = now
            if irq & IRQ_RX_DONE:
                stat = self.read_cmd(CMD_GET_RX_BUFFER_STATUS, [0x00], 2)
                length, offset = stat[0], stat[1]
                data = self.read_cmd(CMD_READ_BUFFER, [offset, 0x00], length)
                text = bytes(data).decode("utf-8", errors="replace")
                rssi, snr, signal = self.packet_status()
                print(f"RX {length} bytes: {text}")
                print(f"RSSI={rssi:.1f} dBm SNR={snr:.1f} dB SignalRSSI={signal:.1f} dBm")
                self.clear_irq()
            elif irq & IRQ_CRC_ERR:
                print("RX CRC error")
                self.clear_irq()
            elif irq & IRQ_HEADER_ERR:
                print("RX header error")
                self.clear_irq()
            time.sleep(0.02)
 
 
def parse_args():
    parser = argparse.ArgumentParser(description="Standalone SX126x LoRa TX/RX, no LoRaRF package required")
    parser.add_argument("mode", choices=["tx", "rx"], nargs="?", default="tx", help="default: tx")
    parser.add_argument("--bus", type=int, default=0)
    parser.add_argument("--cs", type=int, default=0)
    parser.add_argument("--speed", type=int, default=7800000)
    parser.add_argument("--reset", type=int, default=18, help="BCM GPIO")
    parser.add_argument("--busy", type=int, default=20, help="BCM GPIO")
    parser.add_argument("--nss", type=int, default=21, help="manual NSS GPIO, -1 to use SPI CE only")
    parser.add_argument("--txen", type=int, default=6, help="BCM GPIO, -1 if unused")
    parser.add_argument("--rxen", type=int, default=-1, help="BCM GPIO, -1 if unused")
    parser.add_argument("--txen-active-high", action="store_true")
    parser.add_argument("--rxen-active-high", action="store_true", default=True)
    parser.add_argument("--frequency", type=int, default=433000000)
    parser.add_argument("--power", type=int, default=22)
    parser.add_argument("--sf", type=int, default=7)
    parser.add_argument("--bw", type=int, default=125000)
    parser.add_argument("--cr", type=int, default=5)
    parser.add_argument("--preamble", type=int, default=12)
    parser.add_argument("--sync-word", type=lambda x: int(x, 0), default=0x3444)
    parser.add_argument("--message", default="hello")
    parser.add_argument("--interval", type=float, default=5.0)
    parser.add_argument("--count", type=int, default=0, help="TX packet count, 0 means forever")
    parser.add_argument("--busy-timeout", type=float, default=2.0)
    parser.add_argument("--tx-timeout-ms", type=int, default=5000)
    parser.add_argument("--debug", action="store_true")
    return parser.parse_args()
 
 
def main():
    args = parse_args()
    radio = SX126x(args)
    radio.open()
    try:
        if args.mode == "tx":
            counter = 0
            while True:
                radio.transmit(f"{args.message} {counter}")
                counter = (counter + 1) & 0xFF
                if args.count > 0 and counter >= args.count:
                    break
                time.sleep(args.interval)
        else:
            radio.receive_forever()
    finally:
        radio.close()
 
 
if __name__ == "__main__":
    main()