// SPDX-License-Identifier: 0BSD // Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io //go:build tinygo package interfaces import ( "fmt" "machine" "sync" "time" "git.quad4.io/Networks/Reticulum-Go/pkg/common" "git.quad4.io/Networks/Reticulum-Go/pkg/debug" ) const ( REG_FIFO = 0x00 REG_OP_MODE = 0x01 REG_FRF_MSB = 0x06 REG_FRF_MID = 0x07 REG_FRF_LSB = 0x08 REG_PA_CONFIG = 0x09 REG_FIFO_ADDR_PTR = 0x0D REG_FIFO_TX_BASE_ADDR = 0x0E REG_FIFO_RX_BASE_ADDR = 0x0F REG_FIFO_RX_CURRENT_ADDR = 0x10 REG_IRQ_FLAGS = 0x12 REG_RX_NB_BYTES = 0x13 REG_MODEM_CONFIG_1 = 0x1D REG_MODEM_CONFIG_2 = 0x1E REG_PREAMBLE_MSB = 0x20 REG_PREAMBLE_LSB = 0x21 REG_PAYLOAD_LENGTH = 0x22 REG_MODEM_CONFIG_3 = 0x26 REG_RSSI_WIDEBAND = 0x2C REG_DETECTION_OPTIMIZE = 0x31 REG_DETECTION_THRESHOLD = 0x37 REG_SYNC_WORD = 0x39 REG_DIO_MAPPING_1 = 0x40 REG_VERSION = 0x42 MODE_LONG_RANGE_MODE = 0x80 MODE_SLEEP = 0x00 MODE_STDBY = 0x01 MODE_TX = 0x03 MODE_RX_CONTINUOUS = 0x05 IRQ_RX_DONE_MASK = 0x40 IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20 IRQ_TX_DONE_MASK = 0x08 MAX_PKT_LENGTH = 255 ) // LoRaInterface provides a TinyGo SPI-based LoRa interface for SX127x. type LoRaInterface struct { BaseInterface spi machine.SPI cs machine.Pin reset machine.Pin dio0 machine.Pin freq uint32 bw uint32 sf uint8 cr uint8 txPower uint8 done chan struct{} stopOnce sync.Once } // NewLoRaInterface initializes a new LoRaInterface. func NewLoRaInterface(name string, spi machine.SPI, cs, reset, dio0 machine.Pin, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) { li := &LoRaInterface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled), spi: spi, cs: cs, reset: reset, dio0: dio0, freq: freq, bw: bw, sf: sf, cr: cr, txPower: 17, done: make(chan struct{}), } li.MTU = MAX_PKT_LENGTH li.Bitrate = int64(bw * uint32(sf) / (1 << (sf - 1))) if enabled { err := li.Start() if err != nil { return nil, err } } return li, nil } // Start configures and brings the LoRaInterface online. func (li *LoRaInterface) Start() error { li.Mutex.Lock() defer li.Mutex.Unlock() if li.Online { return nil } li.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) li.cs.High() li.reset.Configure(machine.PinConfig{Mode: machine.PinOutput}) li.dio0.Configure(machine.PinConfig{Mode: machine.PinInput}) li.reset.Low() time.Sleep(10 * time.Millisecond) li.reset.High() time.Sleep(10 * time.Millisecond) version := li.readReg(REG_VERSION) if version != 0x12 { return fmt.Errorf("LoRa chip not found, version: 0x%02x", version) } li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP) time.Sleep(10 * time.Millisecond) frf := uint64(li.freq) << 19 / 32000000 li.writeReg(REG_FRF_MSB, uint8(frf>>16)) li.writeReg(REG_FRF_MID, uint8(frf>>8)) li.writeReg(REG_FRF_LSB, uint8(frf)) li.writeReg(REG_FIFO_TX_BASE_ADDR, 0) li.writeReg(REG_FIFO_RX_BASE_ADDR, 0) li.writeReg(0x0C, 0x23) li.writeReg(REG_MODEM_CONFIG_3, 0x04) li.writeReg(REG_PA_CONFIG, 0x80|(li.txPower-2)) var bwVal uint8 switch li.bw { case 125000: bwVal = 7 case 250000: bwVal = 8 case 500000: bwVal = 9 default: bwVal = 7 } li.writeReg(REG_MODEM_CONFIG_1, (bwVal<<4)|(li.cr-4)<<1|0x00) li.writeReg(REG_MODEM_CONFIG_2, (li.sf<<4)|0x04) li.writeReg(REG_SYNC_WORD, 0x12) li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY) li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS) li.Online = true li.Enabled = true go li.readLoop() return nil } // readReg reads a byte from the given register. func (li *LoRaInterface) readReg(reg uint8) uint8 { li.cs.Low() li.spi.Transfer(reg & 0x7F) val, _ := li.spi.Transfer(0) li.cs.High() return val } // writeReg writes a byte to the given register. func (li *LoRaInterface) writeReg(reg uint8, val uint8) { li.cs.Low() li.spi.Transfer(reg | 0x80) li.spi.Transfer(val) li.cs.High() } // readLoop polls the radio for received packets and dispatches them. func (li *LoRaInterface) readLoop() { for { li.Mutex.RLock() online := li.Online done := li.done li.Mutex.RUnlock() if !online { return } select { case <-done: return default: } irq := li.readReg(REG_IRQ_FLAGS) if irq&IRQ_RX_DONE_MASK != 0 { li.writeReg(REG_IRQ_FLAGS, IRQ_RX_DONE_MASK) if irq&IRQ_PAYLOAD_CRC_ERROR_MASK == 0 { currentAddr := li.readReg(REG_FIFO_RX_CURRENT_ADDR) li.writeReg(REG_FIFO_ADDR_PTR, currentAddr) count := li.readReg(REG_RX_NB_BYTES) packet := make([]byte, count) li.cs.Low() li.spi.Transfer(REG_FIFO) for i := uint8(0); i < count; i++ { packet[i], _ = li.spi.Transfer(0) } li.cs.High() li.ProcessIncoming(packet) } } time.Sleep(10 * time.Millisecond) } } // Send transmits a packet over LoRa. func (li *LoRaInterface) Send(data []byte, address string) error { return li.ProcessOutgoing(data) } // ProcessOutgoing encodes and sends a packet. func (li *LoRaInterface) ProcessOutgoing(data []byte) error { li.Mutex.Lock() defer li.Mutex.Unlock() if !li.Online { return fmt.Errorf("interface offline") } if len(data) > MAX_PKT_LENGTH { return fmt.Errorf("packet too long for LoRa: %d", len(data)) } li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY) li.writeReg(REG_FIFO_ADDR_PTR, 0) li.cs.Low() li.spi.Transfer(REG_FIFO | 0x80) for _, b := range data { li.spi.Transfer(b) } li.cs.High() li.writeReg(REG_PAYLOAD_LENGTH, uint8(len(data))) li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_TX) start := time.Now() for { if li.readReg(REG_IRQ_FLAGS)&IRQ_TX_DONE_MASK != 0 { li.writeReg(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK) break } if time.Since(start) > 2*time.Second { debug.Log(debug.DEBUG_ERROR, "LoRa TX timeout") break } time.Sleep(1 * time.Millisecond) } li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS) li.TxBytes += uint64(len(data)) li.lastTx = time.Now() return nil } // Stop disables the LoRaInterface. func (li *LoRaInterface) Stop() error { li.Mutex.Lock() li.Online = false li.Enabled = false li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP) li.Mutex.Unlock() li.stopOnce.Do(func() { if li.done != nil { close(li.done) } }) return nil }