LoRa extension

This is a LoRa extension for uLisp to allow you to send and receive messages using a LoRa radio. It provides a uLisp interface to Sandeep Mistry's Arduino-LoRa library [1] running on a compatible board.

Introduction

LoRa (for Long Range) is a low-power long-range proprietary radio communication protocol based on spread spectrum modulation. It was originally developed by Cycleo, France, and subsequently acquired by the US company Semtech [2]. It is implemented by the Semtech SX1276/77/78/79 modules, also marketed by HopeRF as the RFM95W, RFM96W, and RFM98W.

Characteristics

LoRa typically achieves a range of up to 3 miles (4.8 km) in urban areas and up to 10 miles (16 km) in rural areas. It uses license-free sub-gigahertz radio frequency bands: 433 MHz or 868 MHz in Europe, 915 MHz in North and South America, 868 MHz in India, and 915 MHz in Asia. The LoRa modules are available in two versions: one for 433 MHz, and one that can be tuned to either 868 MHz or 915 MHz.

The uLisp LoRa extension

The LoRa extension exposes the LoRa radio directly, and allows you to send data to any radios in range with the same radio parameters; there is no addressing, and all data is sent unencrypted. If want your data to be encrypted you can encrypt it before passing it to these commands, and decrypt it on the receiving end. For suggestions about implementing addressing and encryption see Addressing and encryption below.

Note that this extension does not implement LoRaWAN, which extends LoRa but requires a LoRaWAN gateway and LoRaWAN network and application server.

The LoRa extension implements most of the functions in the Arduino-LoRa library, and defines a new type of stream, lora-stream, that allows you to use the standard uLisp I/O functions to read from and write to the LoRa radio. So, for example, you can use the uLisp format function to format the messages you send via LoRa, or read to read Lisp expressions received via LoRa and evaluate them with eval.

Installing the LoRa extension

  • Install Sandeep Mistry's Arduino-LoRa Library via the Arduino IDE Library Manager.
  • Download the LoRa extension file and save it with the name LoRaExtension.ino.
  • Put it in the same folder as your uLisp source file.
  • In the main uLisp source file uncomment the lines:
#define extensions
#define streamextensions
  • Compile uLisp and upload it to your board.

The LoRa functions will then be added to the built-in functions in uLisp, together with their documentation.

For information about the Extensions File feature of uLisp see: Adding your own functions.

For information about stream extensions see: Streams.

Functions

This section lists the forms provided in the LoRa extension.

Operation function

lora-begin

Status functions

lora-available, lora-frequency-error, lora-packet-rssi, lora-packet-snr

Configuration functions

lora-set-coding-rate, lora-set-frequency, lora-set-pins, lora-set-preamble-length, lora-set-signal-bandwidth, lora-set-spreading-factor, lora-set-sync-word, lora-set-tx-power

Stream functions

with-lora-input, with-lora-output


lora-available function

Syntax: (lora-available)

Returns the number of bytes available for reading, or zero if no bytes are available.

lora-begin function

Syntax: (lora-begin frequency)

Starts LoRa on the specified frequency, in MHz; one of 433, 868, or 915. Returns nil on failure.

lora-frequency-error function

Syntax: (lora-frequency-error)

Returns the frequency error of the last received packet as a floating-point number in Hz. For example:

> (lora-frequency-error)
-3598.0

lora-packet-rssi function

Syntax: (lora-packet-rssi)

Returns the RSSI of the last received packet. For example:

> (lora-packet-rssi)
-59

lora-packet-snr function

Syntax: (lora-packet-snr)

Returns the signal-to-noise ratio of the last packet as a floating-point number in dB. For example:

> (lora-packet-snr)
9.25

lora-set-coding-rate function

Syntax: (lora-set-coding-rate rate)

Sets the coding rate to 4/rate, where rate can be between 5 and 8, default 4.

lora-set-pins function

Syntax: (lora-set-pins ss rst dio0)

Optionally configures the pins used by the library. Must be called before lora-begin. The arguments are as follows:

  • ss is slave select, default 10

  • rst is reset, default 9

  • dio0 is interrupt, default 2.

lora-set-preamble-length function

Syntax: (lora-set-preamble-length length)

Sets the preamble length of the radio in the range 6 to 65535, default 8.

lora-set-signal-bandwidth function

Syntax: (lora-set-signal-bandwidth bandwidth)

Sets the signal bandwidth in Hz. Supported values are 7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000, and 500000, default 125000.

lora-set-spreading-factor function

Syntax: (lora-set-spreading-factor factor)

Sets the spreading factor, which determines how far apart the radio's transmissions are within the available bandwidth. It can be between 6 and 12, default 7. A lower spreading factor corresponds to a higher data rate but lower sensitivity.

Radios with different spreading factors will not receive each other's transmissions, making this a simple addressing scheme.

lora-set-sync-word function

Syntax: (lora-set-sync-word word)

Sets the byte value to be used as the sync word, from #x01 to #xff, default #x12. The value #x34 is reserved for LoRaWAN.

Radios with different sync words will not receive each other's transmissions, making this a simple addressing scheme.

lora-set-tx-power function

Syntax: (lora-set-tx-power power)

Sets the transmit power of the radio to between 2 and 20 dB, default 17. A lower transmit power reduces the power consumption but gives a lower range.

with-lora-input special form

Syntax: (with-lora-input (stream [size]) form*)

If a packet has been received evaluates the forms with stream bound to a lora-input-stream and returns the size of the packet; otherwise returns nil.

If size is specified and is greater than zero implicit header mode is enabled with an expected packet of size bytes.

You can use the standard uLisp I/O functions to read from the stream.

The with-lora-input form incorporates the Arduino-LoRa function parsePacket().

with-lora-output special form

Syntax: (with-lora-output (stream [implicit async]) form*)

Evaluates the forms with stream bound to a lora-output-stream to send a packet of up to 255 bytes.

If implicit is non-nil no header is provided.

If async is non-nil the call returns immediately without waiting for the packet to be completed.

You can use the standard uLisp I/O functions to write to the stream.

The with-lora-output form incorporates the Arduino-LoRa functions beginPacket() and endPacket().

Boards

The following boards are ideal for use with the LoRa Extension because they include an integrated LoRa radio. Obviously you'll need two boards to experiment with LoRa, but they don't need to be the same type of board because the LoRa modules are compatible.

LILYGO LoRa32

LilygoLoRa2.jpg

For the LILYGO LoRa32 the pin configuration should be:

(lora-set-pins 18 23 26)

Adafruit Feather M0 LoRa Radio

The Adafruit Feather M0 with RFM95 LoRa Radio [3] is based on the ATSAMD21 processor with the addition of an RFM95C LoRa module:

FeatherLoRa.jpg

Use the Adafruit Feather M0 (SAMD21) board option in the Arduino IDE for this board.

For the Feather M0 LoRa Radio board the pin configuration should be:

(lora-set-pins 8 4 3)

Adafruit Feather RP2040 with RFM95 LoRa Radio

There is also an RP2040 version of the above board [4].

Adafruit RFM95W LoRa Radio Transceiver Breakout

The Adafruit RFM95W LoRa Radio Transceiver Breakout [5] provides an RFM95 LoRa radio module on a breakout board that you can interface via SPI to the processor board of your choice:

RFM95LoRa.jpg

It includes a 3.3V regulator and logic level translation to make it suitable for use with 3.3V or 5V processor boards.

Examples

Because this is Lisp, you can interactively send and receive messages by calling the functions in the LoRa extension by typing them in the Serial Monitor.

Here are some example programs that implement equivalents of the examples in the Arduino-LoRa library. If you can't get these to work check that you can run the equivalent C version in the library first.

LoRa sender

Sends a test message every five seconds:

(defun lora-sender ()
  (lora-set-pins 8 4 3)
  (if (lora-begin 868)
      (let ((count 0))
        (loop
         (with-lora-output (str)
           (format str "hello ~a~%" (incf count)))
         (delay 5000))
        (print "Starting LoRa failed!"))))

The lora-set-pins function and frequency 868 should be changed to appropriate values for your board.

LoRa receiver

Checks whether a packet is available and if so, reads it and displays it with the RSSI:

(defun lora-receiver ()
  (lora-set-pins 8 4 3)
  (if (lora-begin 868)
      (loop
       (with-lora-input (str)
         (format t "~s RSSI: ~a~%" (read-line str) (lora-packet-rssi))))
    (print "Starting LoRa failed!")))

Again, the lora-set-pins function and frequency 868 should be changed to appropriate values for your board.

Here's some typical output:

> (lora-receiver)
"hello 24" RSSI: -62
"hello 25" RSSI: -61
"hello 26" RSSI: -61

Addressing and encryption

Addressing

For simple addressing the functions lora-set-spreading-factor or lora-set-sync-word can be used. Either of these will ensure that you only receive messages with the same spreading factor or sync word.

The following example sends two separate sets of messages using different sync words:

(defun lora-sender-two-syncs ()
  (lora-set-pins 8 4 3) ; ss rst irq = M0
  (if (lora-begin 868)
      (let ((count 0))
        (loop
         (lora-set-sync-word #x12)
         (with-lora-output (str)
           (format str "Hello Laura! ~a~%" (incf count)))
         (lora-set-sync-word #x21)
         (with-lora-output (str)
           (format str "Hello Eliza! ~a~%" (incf count)))
         (delay 5000))
        (print "Starting LoRa failed!"))))

The receiver allows you to specify which set of messages you wish to receive:

(defun lora-receiver (sync)
  (lora-set-pins 8 4 3) ; ss rst irq = M0
  (if (lora-begin 868)
      (loop
       (lora-set-sync-word sync)
       (with-lora-input (str)
         (format t "~s RSSI: ~a~%" (read-line str) (lora-packet-rssi))))
    (print "Starting LoRa failed!")))

For example:

> (lora-receiver #x12)
"Hello Laura! 13" RSSI: -56
"Hello Laura! 15" RSSI: -59
"Hello Laura! 17" RSSI: -54

Encryption

There are several libraries that provide symmetric encryption, to hide the contents of messages where the sender and receiver are both using the same secret key.

Providing an antenna

These boards should not be used without an antenna, because transmitting without an antenna could cause the output stage to overheat. A suitable antenna can simply be a short length of single-core cable:

Frequency Antenna length
433 MHz 165 mm
868 MHz 82 mm
915 MHz 78 mm

Calculation

The wavelength λ of electromagnetic radiation is given by:

λ = 
vf

where v is the speed of light, 3.00x10m/s. So for a frequency f of 868 MHz, λ = 346 mm.

A straight wire antenna should be a quarter-wave monopole; a quarter of this length is 86 mm. This should be reduced by about 5% to 82 mm to account for the end effect.

In a rubber ducky antenna, like the one used on the LILYGO LoRa32, the quarter-wave monopole is coiled into a helix, with a rubber cover for protection. This makes it less efficient than a straight quarter-wave monopole, but more practical.

Acknowledgements

Thanks to Sandeep Mistry for providing the excellent Arduino-LoRa library on which this extension is based.


  1. ^ Arduino-LoRa on GitHub.
  2. ^ LoRa on Wikipedia.
  3. ^ Feather M0 with RFM95 LoRa Radio on Adafruit.
  4. ^ Feather RP2040 with RFM95 LoRa Radio on Adafruit.
  5. ^ RFM95W LoRa Radio Transceiver Breakout on Adafruit.