all posts

Using LoRa with Kotlin

2021 Nov 02
Using LoRa with Kotlin

The Kotlin mascot holding a LoRa radio

Overview

LoRa is a useful technology for efficiently sending small packets of data long distances without relying on infrastructure like the internet. Many of the currently available LoRa development boards allow for programming using the Arduino IDE, a useful tool for iterating on simple, low-level software. However, in some cases, LoRa applications may need more computing power, or the need to interface with more complicated business logic, like a Java or Kotlin application.

The solution I'm proposing for using LoRa with Kotlin is to open up the LoRa radio's serial connection, and forward all packets to and from the LoRa device through this serial port. This guide covers how I achieved this and how you can use this approach to add LoRa messaging to your Java or Kotlin project.

Getting Started

This guide uses a Heltec ESP32 LoRa V2 board, and a Linux computer. If you have a different Arduino-compatible LoRa board, please feel free to reach out and I can help you get the Arduino code working on your hardware. I am also working on getting an Android phone so I can port the Kotlin code to Android as well.

Install Arduino IDE and needed dependencies

Understanding the Arduino Code

Arduino code is split into two system functions which are called by the board. There is a setup() function which is executed once when the board powers on, then a loop() function which is called continuously as the board is running.

The GPL licensed code used in this guide is available on the project's git repo

Setup

void setup()
{
  // Heltec setup function
  Heltec.begin(false /*DisplayEnable Enable*/, true /*Heltec.Heltec.Heltec.LoRa Disable*/, true /*Serial Enable*/, true /*PABOOST Enable*/, BAND /*long BAND*/);

  // This opens the Serial port on the LoRa board, 
  // then sets the Serial buffer size 
  // (not sure if the buffer size function works, 
  // haven't noticed any performance change) 
  Serial.begin(115200);
  Serial.setRxBufferSize(1024);

  // Sets the LoRa signal bandwidth. 
  // This bandwith along with the default spreading factor of 7,
  // allows for us to send 255 byte packages fairly far distances. 
  // In the future, other signalBandwidth and spreading factors configs,
  // could allow for sending smaller packets of around 50bytes, much longer distances 
  LoRa.setSignalBandwidth(250E3);
}

Loop and Receive Function

// Reads LoRa buffer, then prints buffer to serial output
void cbk(int packetSize) {
  packet ="";
  packSize = String(packetSize,DEC);
  for (int i = 0; i < packetSize; i++) { packet += (char) LoRa.read(); }
  Serial.println(packet);
}

void loop()
{ 
  // If the serial buffer is not empty, goto next line
  if(Serial.available() > 0) {
    String data = "";
    // Parse incoming serial data until a newline char
    data = Serial.readStringUntil('\n');
    
    // Send packet
    LoRa.beginPacket(); 
    LoRa.print(data);
    LoRa.endPacket();
    // Set radio back to receive mode
    LoRa.receive();
    
  } 
  // If there is a packet in LoRa buffer, parsePacket() returns > 0
  int packetSize = LoRa.parsePacket();
  if (packetSize) { cbk(packetSize);  }
}

Reading and Writing LoRa packets in Kotlin

Now that we have uploaded the serial driver to the LoRa device, sending and receiving LoRa packets in Kotlin is as simple as sending and receiving serial data over a serial port. To interface with a computer's serial port, we will be using the Java JSerialComm library. To add this library to your Kotlin project, you can add the following line to the dependencies{} section of your build.gradle.kts:

implementation("com.fazecast:jSerialComm:[2.0.0,3.0.0)")

Writing serial data is as easy as opening the correct serial port, setting the correct baud rate, then using the comPort.write() method.

There are numerous ways to read from the serial port, including non-blocking and blocking methods shown in the JSerialComm documentation. For use in Kotlin, I chose the event-based reading method because I think it pairs nicely with Kotlin Coroutines and Flows. To implement an event-based reading strategy using JSerialComm, we define an instance of a SerialPortMessageListener:

object SerialListener: SerialPortMessageListener {
    override fun getListeningEvents(): Int {
        return SerialPort.LISTENING_EVENT_DATA_RECEIVED;
    }

    override fun getMessageDelimiter(): ByteArray {
        return byteArrayOf(0x0A.toByte())
    }

    override fun delimiterIndicatesEndOfMessage(): Boolean {
       return true
    }

    override fun serialEvent(event: SerialPortEvent?) {
        val delimitedMessage = event!!.receivedData
        var rawString = String(delimitedMessage).replace("\n", "")
        rawString = strDelim.replace("\r", "")
        GlobalScope.launch(Dispatchers.IO) {
            launch {
                IncomingMessageBus.postMessage(rawString)
            }
        }
    }

}

Essentially this is a class that defines a delimiter, for which we use the newline char 0x0A, and a function serialEvent() which gets called when a serial message that has a delimiter byte is parsed. Once the message is recieved and the newline and carriage return chars are removed, the message is posted to a Kotlin flow. The implementation of IncomingMessageBus is an object wrapping a basic MutableSharedFlow.

object IncomingMessageBus {
    private val _incoming = MutableSharedFlow<String>()
    val incoming = _incoming.asSharedFlow()
    suspend fun postMessage(string: String) {
        _incoming.emit(string)
    }
}

The actual Kotlin definition of the SerialInterface is straightforward, we open the port the LoRa device is on, set baud rate and connect our SerialListener. There is also a close() method for good measure.

object SerialInterface {
    private val comPort: SerialPort = SerialPort.getCommPort("/dev/ttyUSB0");
    init {
        comPort.openPort()
        comPort.setComPortTimeouts(SerialPort.TIMEOUT_WRITE_BLOCKING, 0, 0)
        comPort.baudRate = 115200
        comPort.addDataListener(SerialListener)
    }
    fun write(str: String) {
        val str = str + '\n'
        val bstr = str.toByteArray()
        comPort.writeBytes(bstr, bstr.size.toLong())
    }
    fun close() {
        if(comPort.isOpen) {
            comPort.closePort()
        }
    }
}

Processing LoRa/Serial Messages Using Kotlin Flows

Now that we have set up our SerialListener to emit data to our IncomingMesssageBus, writing functions that react to message receive events are simple. We simply collect() the flow, and new messages will be passed to any function calls we add.

// Run in a new IO thread
GlobalScope.launch(Dispatchers.IO) {
    // str is the string value of the incoming message
    IncomingMessageBus.incoming.collect { str ->
         doSomethingToMessage(str)
    }
}

And that covers it! You should now be able to nicely send and receive data over LoRa. If you have experience with Kotlin and know of any improvements over my proposed solution, please feel free to get in touch! I am still very new to Koltin and its more complicated areas like Coroutines and Flows, and I'm sure there are many improvements to be made. My contact info is linked on the home page of this website. I can also be reached via matrix @paul-lorenc:matrix.org which I prefer.

This blog also has an RSS feed you can subscribe to. In the future I hope to write more about a LoRa mesh chat app I am building with Compose Multiplatform.