Here you will find my recent contribution to LoRa drivers. This post describes the LoRa driver for a Raspberry Pi SBC (Single Board Computer). Additionally, a wrapper written in Python is available making it very easy to use and prototype. Raspberry Pi gets more and more attention. Adding LoRa communication enables it to communicate with IoT devices such as remote thermometers, soil moisture sensors and many more others. You can find HAT boards thatoffer a LoRa module. Here, I describe how to connect and how to use a low-cost LoRa RFM95W module. This particular module comes with different frequency options. However, this post describes the one which uses 868 MHz frequency.
Let’s start with a bit different piece of information. Some time ago I have published a driver for STM32 for a SX1278 LoRa module. This works fine and you can read more about it in a post tiled STM32 HAL driver for LoRa SX1278 wireless module.
Why then write a driver for Raspberry Pi? The reason is quite obvious. Raspberry Pi is a great platform which allows for fast prototyping. Adding a feature to communicate over long distance sounds like a good idea. Moreover, the driver supports Python through C bindings to this script language. Transmitting a data package can be done in only 3 lines: importing LoRa module, initializing device and sending data over the air.
Connect module to RPI
You need to connect power, SPI lines and two additional pins to Raspberry Pi. Below you can find the pinout of LoRa module.
Below a pinout of Raspberry Pi is available.
Using the RFM95W pinout and Raspberry Pi pinout you can connect the module to the mini computer. The connections were gathered in a shape of a table which was attached below.
RFM95W pin | Raspberry Pi pin | Description |
3.3V | 3V3, pin 1 | 3V3 power supply |
GND | GND, pin 6 | power ground |
MISO | GPIO 9 (MISO), pin 21 | SPI Master Input Slave Output |
MOSI | GPIO 10 (MOSI), pin 19 | SPI Master Output Slave Input |
SCK | GPIO 11 (SCLK), pin 23 | SPI clock |
NSS | GPIO 25, pin 22 | SPI chip select |
RESET | GPIO 17, pin 11 | LoRa module reset |
DIO0 | GPIO 4 (GPCLK0), pin 7 | LoRa status line |
Update Raspberry Pi
Before diving into wireless communication an additional step is needed. Operating system has to be updated and equipped with Python 3 package. Updating OS on RPi is pretty straightforward. Then let’s go ahead and type following
sudo apt update
sudo apt dist-upgrade -y
sudo apt install -y python3
The LoRa RPi driver makes use of wiringPi library. This should come as default if not you have to install it.
sudo apt install wiringpi
Additionally, you have to enable SPI interface. For this use raspi-tool. This will open a console application, navigate through the menu and enable SPI interface. After that save changes and exit the application. Restart your Raspberry Pi to apply changes.
Compile the driver
Before compiling you have to verify the Python 3 version. Keep in mind that Python 3 is used instead of Python 2. This is important in terms of used bindings. The version number can be checked with
python3 --version
Example output is
Python 3.7.1
After identifying the exact Python 3 version make appropriate changes to the Makefile. Proper version has to be set as a constant in Makefile. Thus, if the version is 3.7.1 you only have to put 3.7. This is required to make proper bindings to Python libraries. After the changes following line should be present in the Makefile.
PYTHONVER=python3.7
Compile the code. If you only need the library, not the application which acts as sender/receiver, you can invoke following command
make lib
If the compilation succeed a loralib.so file should have been created. This is a library which acts as a Python module, therefore it can be imported with import command in Python script. Using it is straightforward, below work example was given.
Test the driver
To test the driver you can do following
import loralib
Above will import loralib module. If you see an error this is probably due to lacking the loralib library in current working directory. When you start Python interpreter make sure that the library is placed in the same directory from where you are invoking Python. The module is equipped with some function. There are only three so the first one initializes the module, one sends the data and the last but not least receives data.
For more detailed information on what is available got to my GitHub rpeository. the link to the GitHub can be found at the bottom of this post.
Receive data
To receive some data transmitted by an another LoRa module you have to initialize the driver with init() and then invoke the recv().
loralib.init(1, 868000000, 7)
data = loralib.recv()
print(data[0])
init() takes 3 parameters. The first sets the module to receiver mode (0) or transmitter mode (1). The second parameter sets the demanded frequency expressed in Hertz. The third argument is the spread factor. Other parameters like bandwidth and coding ratio were hardcoded and set to 125kHz and 4/5 accordingly.
Please keep in mind that using different frequencies then the radio was designed for can cause very poor reception/transition due to the fact of band pass filters presence in the module. Also, wireless transmission is under government regulation so first check what kind of power, modulation, frequency you can use.
recv() returns a tuplet containing six elements:
- buffer — the actual data
- buffer length
- PRRSI — last packet RSSI (received signal strength indicator) [dBm]
- RRSI — current RSSI [dBm]
- SNR — signal noise ratio
- error — if 0 then no error occurred, if other then 0 then CRC error occurred
If there was a correct reception registered then following should be fulfilled:
data[1] > 0 and data[5] == 0
It translates to two things. The first one checks if there was some data received and the second one checks integrity of received package.
Transmitting data
The data can be transmitted with following
loralib.init(0, 868000000, 7)
loralib.send(b'hello')
This initializes the LoRa module in sender (transmitter) mode and sends data with send() method. This method accepts a byte array. So before sending a string it has to be encoded.
Docker
Docker is a containerization solution. It allows you to run applications in a separate environment, which meets the application’s specific needs. Why is it important to mention this? From time, to time you have definitely struggled with maintaining dependencies for your application. It can be fairly easy, but it requires constant maintenance. The problem starts to show up when there are multiple applications or services which need to coexist in a particular environment. Then the effort put into maintaining the ecosystem might be greater than developing a target solution.
Docker allows you to install all dependencies apart from the host operating system. Thanks to this, it is easier to manage the project. It is not different for the LoRa wrapper. It was developed for Raspberry Pi OS based on Debian 9 (Stretch). Now, we already have Debian 12 (Bookworm). With the ordinary development process, it would involve solving dependencies 3 times already. With docker, when it is set up once, it is enough. However, it is always good to keep in mind that despite the lack of necessity to update the environment, it should be updated. For example, for security reasons.
Below you can find a sample Dockerfile which could be run with the LoRa driver in Python.
FROM python:3.7
# setup timezone
ENV TZ=Europe/Warsaw
# install additional packages and clean redundant files
RUN apt update && apt dist-upgrade -y && \
apt install -y iputils-ping net-tools less wget vim && \
rm -rf /var/lib/apt/lists/*
# upgrade pip to specific version
RUN pip install --upgrade pip==22.3.1
# install wiringpi dependancy
RUN wget https://github.com/WiringPi/WiringPi/releases/download/2.61-1/wiringpi-2.61-1-armhf.deb
RUN dpkg -i wiringpi-2.61-1-armhf.deb
# create app main directory
RUN mkdir /app
# copy content of the directory to /app, this includes lora.c and Makefile
COPY . /app
WORKDIR /app
# compile the LoRa lib
RUN make -f Makefile_docker clean; make -f Makefile_docker all
# execute a Bash script; the script should run indefinitely or instead you can
# start a service which will keep the docer running
CMD /app/start.sh
Several words of explanation. This is just a basic Dockerfile file. The image is based on the official Python image. Notice that a specific version was chosen. In addition, the Dockerfile represents a compilation of a building environment and a production environment. To make the image smaller, one could separate these two environments. By extending this file, you can add your own services.
Please, note that a different Makefile is being used in the docker environment. This is due to the fact that the Python headers are located in a slightly different place. A dedicated docker Makefile solves the issue.
Hear is a grind of docker commands to build the image and run it.
# build Docker container in the root directory of the project
docker build -t lora .
# run docker in demon mode, and name the running container lora
docker run -d --name lora lora
# exec an internal bash in the container
# the bash session can be closed and the container still runs in the background
docker exec -it lora /bin/bash
# kill the container and remove it
# it can be still run because the image is still residing in the OS
docker kill lora; docker rm lora
Source code
The full source code and more detailed documentation is available under LoRa-RaspberryPi repository.
hello Wojciech Domski,
I just use your loralib.so for Python, (Python 3.9) it seems to work but I get this:
DeprecationWarning: PY_SSIZE_T_CLEAN will be required for ‘#’ formats
data=loralib.recv()
do you know, how to fix that?
hi,
to answer myself, I ignore the warning with:
————
import warnings
warnings.filterwarnings(“ignore”, category=DeprecationWarning)
———–
Thank you for the wrapper, because Python is much better for a programming amateur like me!
Hi! I am glad that you have solved your problem. However, the issue is related to the hastag when parsing data from python to C. I have been using this wrapper for python 3.7. Apparently, for Python 3.9 some things have changed. The fix would be to slightly modify the wrapper’s code. Filtering warnings only suppresses it but does not solve this issue.
Hi,
I upgraded my system, Python version 3.11 is installed. The library loralib.so has three functions init(), send(), recv().
init() works fine, but the two other functions throw a SystemError (PY_SIZE_T_CLEAN macro must be defined for ‘#’ formats). with Python (Version 3.9) I could ignore the warning but now I can’t. Is there a way to solve this?
I will look into it since I have already added some changes to the code. Could you perhaps give me some more details, a full backtrack would be perfect.
I found out it is an upgrade issue, now I use ‘bookworm’ as os. Nothing works the way I am used to when compiling anything with ‘./build’. Wiringpi is not working properly either, so it is not your wrapper but my operating system (dependencies and whatever (the system complains about my code using ‘bytes’ instead of ‘Bytes’ when compiling other code)). So it takes me a while how to name it precise for backtracking it (there are so many problems).
Right now I want to express the difficulties I encounter when upgrading the os.
A different approach to a solution would be to setup an enviroment that is independent from operating system ‘bullseye’ or ‘bookworm’ or other upcoming 32-bit systems. Then put your wrapper in this enviroment (the enviroment uses “old” dependencies that always works). Do you have any idea on that?
Perhaps I have a solution. Currently, I am using bullseye based Raspberry Pi OS. I am working with the LoRa wrapper indirectly through docker container. Having told that, maybe you can use the same approach? This solution is decoupled from your host OS and works quite well. The only required thing was to manually install wiringpi library. One other draw back is that the container needs to be run in privileged mode. If I remember correctly the wiringpi lib checks for OS version using /proc. I was not able to map it in a way that the data read from /proc on host OS matched the one mapped to /proc in the container. However, if you are fine with running privileged mode, the this might be suitable solution for you.
I searched your blog for Docker but no result maybe it is worth to blog on that. Is this a solution for my Node.js projects also? Whenever there is a new Node.js version the project might or might not run anymore. If it is suitable it is a major improvement for maintaining the programming projects on a raspberry pi. Knowing now it is possible for your wrapper I will go for it (thanks for showing a way).
here is another thought on your wrapper, since I use LoRa senders apart from LoRaWAN (without TheThingsNetwork) I don’t want to disturb their radio traffic on gateways and I changed my Synchronisation Word (syncWord) from hexadecimal 0x34 to hexadecimal 0x12 which stands for an private network, so the wrapper might be able to encounter this, too. (Right now I change the syncWord in the c-code before I create the wrapper library (loralib.so)).
Ok, so a few things have happened. I have updated the blog post with the information about Docker. I hope it will be useful to you.
As for the remark regarding the sync word. This is a part of what I had in my mind. I have already extended it and it will be possible to set it during the initialization. However, I still need some time to push all the changes to the repository. I use it in part of a larger project and need to complete all of the steps. Keep tuned for the updates 😉