// 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 ( SERIAL_DEFAULT_BAUD = 115200 SERIAL_MTU = 1500 ) // SerialInterface implements a serial interface using TinyGo UART. type SerialInterface struct { BaseInterface uart *machine.UART baud uint32 done chan struct{} stopOnce sync.Once } // NewSerialInterface creates and initializes a new SerialInterface. func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) { if baud == 0 { baud = SERIAL_DEFAULT_BAUD } uart, err := getUART(portName) if err != nil { return nil, err } si := &SerialInterface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled), uart: uart, baud: baud, done: make(chan struct{}), } si.MTU = SERIAL_MTU si.Bitrate = int64(baud) if enabled { err := si.Start() if err != nil { return nil, err } } return si, nil } // getUART returns a TinyGo UART handle by name or index. func getUART(name string) (*machine.UART, error) { switch name { case "UART0", "0": return machine.UART0, nil case "UART1", "1": return machine.UART1, nil case "UART2", "2": return machine.UART2, nil default: if name == "" { return machine.UART0, nil } return nil, fmt.Errorf("unknown UART: %s", name) } } // Start enables the serial interface and starts the read loop. func (si *SerialInterface) Start() error { si.Mutex.Lock() defer si.Mutex.Unlock() if si.Online { return nil } err := si.uart.Configure(machine.UARTConfig{ BaudRate: si.baud, }) if err != nil { return fmt.Errorf("failed to configure UART: %w", err) } si.Online = true si.Enabled = true go si.readLoop() return nil } // Stop disables the serial interface. func (si *SerialInterface) Stop() error { si.Mutex.Lock() si.Online = false si.Enabled = false si.Mutex.Unlock() si.stopOnce.Do(func() { if si.done != nil { close(si.done) } }) return nil } // readLoop reads and processes frames from the UART, handling KISS framing. func (si *SerialInterface) readLoop() { buffer := make([]byte, si.MTU) dataBuffer := make([]byte, 0, si.MTU) inFrame := false escape := false for { si.Mutex.RLock() online := si.Online done := si.done si.Mutex.RUnlock() if !online { return } select { case <-done: return default: } if si.uart.Buffered() > 0 { n, err := si.uart.Read(buffer) if err != nil { debug.Log(debug.DEBUG_ERROR, "Serial read error", "name", si.Name, "error", err) time.Sleep(100 * time.Millisecond) continue } if n > 0 { for i := 0; i < n; i++ { b := buffer[i] if b == KISS_FEND { if inFrame && len(dataBuffer) > 0 { packet := make([]byte, len(dataBuffer)) copy(packet, dataBuffer) si.ProcessIncoming(packet) dataBuffer = dataBuffer[:0] } inFrame = true escape = false continue } if inFrame { if b == KISS_FESC { escape = true } else { if escape { if b == KISS_TFEND { b = KISS_FEND } else if b == KISS_TFESC { b = KISS_FESC } escape = false } dataBuffer = append(dataBuffer, b) } } } } } else { time.Sleep(10 * time.Millisecond) } } } // Send transmits data using KISS protocol with the default command 0x00. func (si *SerialInterface) Send(data []byte, address string) error { return si.SendKISS(0x00, data) } // SendKISS sends a KISS-encoded packet over the serial UART. func (si *SerialInterface) SendKISS(command byte, data []byte) error { si.Mutex.RLock() online := si.Online si.Mutex.RUnlock() if !online { return fmt.Errorf("interface offline") } frame := make([]byte, 0, len(data)*2+3) frame = append(frame, KISS_FEND) frame = append(frame, command) frame = append(frame, escapeKISS(data)...) frame = append(frame, KISS_FEND) _, err := si.uart.Write(frame) if err != nil { return err } si.Mutex.Lock() si.TxBytes += uint64(len(frame)) si.lastTx = time.Now() si.Mutex.Unlock() return nil }