User Tools

Site Tools


creating-a-lorawan-packet-sniffer

This is an old revision of the document!


Creating a LoRaWAN packet sniffer

Buying a LoRaWAN gateway for an end device can be a little expensive. You'd need an 8-channel LoRa concentrator (e.g. for a Raspberry Pi) or an all-in-one gateway unit.

Then you could build a simple sensor end device and have the gateway route your packets to something like the The Things Network.

The better option, and if you are lucky, there might be a gateway in your area that will pick up your sensor data for you.

That is certainly the case for me being out of town. Thankfully I did discover via the TTN map that there is gateway not very far away, just over the hill.

If a gateway is in range, that gives you the opportunity to experiment without the cost and maintenance of a gateway. Now you could probably just start by making an end device and registering it on the TTN, and hope for the best that a gateway may pick it up, but how about we make a sniffer device that scans the downlink channels for LoRaWAN packets?

Note that this sniffer only listens to gateway downlinks.

Parts

  1. ESP32 dev board with an integrated SX1276 LoRa module (e.g. TTGO LoRa32); or
  2. Regular ESP32 dev board + separate SX1276 LoRa module.
  3. A good antenna tuned for the 915–928 MHz ISM band (AU915), covering roughly 915–928 MHz.

How will it work?

In our case, we'll be working with the AU915 LoRaWAN downlink channels DR8–DR13 used by gateways to talk to end devices. This is the combination of Frequencies and Spreading Factors. We'll listen for a period of time on each channel before switching to the next. This way we increase the amount of packets that can be heard from any nearby gateway. The table of AU915 downlink channels (gateway to end device) are as follows:

Channel index Frequency (MHz) Data Rate SF BW (kHz) CR Preamble (symbols)
0 923.3 DR8 SF12 500 4/5 8
1 923.9 DR9 SF11 500 4/5 8
2 924.5 DR10 SF10 500 4/5 8
3 925.1 DR11 SF9 500 4/5 8
4 925.7 DR12 SF8 500 4/5 8
5 926.3 DR13 SF7 500 4/5 8
6 926.9 DR13 SF7 500 4/5 8
7 927.5 DR13 SF7 500 4/5 8

Note that each channel has the bandwidth of 500 kHz, the Coding Rate of 4/5 and Preamble of 8.

LoRa 915mhz module pinouts

This shows a fairly common ESP32 dev board and connections to an SX1276 module.

Creating and configuring the project

1. Open ESP-IDF 5.3.2 PowerShell Create a blank project with:

idf.py create-project lorawan-sniffer

If you're using an esp32 set it here. Ensure you set the right esp32 hardware, eg: esp32, esp32s2, etc

idf.py set-target esp32

Ensure you know how much memory is on board your esp32. Set the memory on board, and the frequency for the FreeRTOS tick:

idf.py menuconfig
	Serial Flasher config > Flash size > 4MB
	Component > FreeRTOS > Kernel > configTICK_RATE_HZ

Quick test build:

idf.py build

Get the nopnop2002's esp-idf-sx127x library as a component and configure

Add the LoRa library component to the project by following section on their website here: https://github.com/nopnop2002/esp-idf-sx127x#how-to-use-this-component-in-your-project

Then configure again with:

idf.py menuconfig

There will be a new menu called: LoRa Configuration. Head into this menu and set up which points on the ESP32 are connected to SPI MISO, SCK, MOSI, NSS and RESET.

On this ESP32 pictured above it will be:

(19) -> MISO GPIO
(18) -> SCK GPIO
(23) -> MOSI GPIO
(5)  -> NSS GPIO
(22) -> RST GPIO

Save and rebuild with:

idf.py build

Get your COM port from the Device Manager. If it was COM5, you can flash with:

idf.py -p COM5 flash

The sniffer code

I'll dump the entire routine here. Essentially there are two FreeRTOS tasks, one to discover packets over the air on each channel for a set amount of attempts before moving to the next channel. The sniffer will move round-robin over all eight.

If a packet is discovered, the LED will blink off and on, and the resulting packet is sent to the serial terminal via USB.

/*
	Scan LoRaWAN channels for raw packets
*/
 
#include <stdio.h>
#include "esp_log.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lora.h"
 
#define BUILT_IN_LED    		2
#define ON              		1
#define OFF            			0
#define MAX_CHANNEL_ATTEMPTS	2048
#define CODING_RATE_4_5			1
#define PREAMBLE_SYMBOLS		8
#define BW_500_KHZ				9
 
int blinkRequest = 0;
 
// | Channel index | Frequency (MHz) | Data Rate | SF   | BW (kHz) | CR  | Preamble (symbols) |
// | ------------- | --------------- | --------- | ---- | -------- | --- | ------------------ |
// | 0             | 923.3           | DR8       | SF12 | 500      | 4/5 | 8                  |
// | 1             | 923.9           | DR9       | SF11 | 500      | 4/5 | 8                  |
// | 2             | 924.5           | DR10      | SF10 | 500      | 4/5 | 8                  |
// | 3             | 925.1           | DR11      | SF9  | 500      | 4/5 | 8                  |
// | 4             | 925.7           | DR12      | SF8  | 500      | 4/5 | 8                  |
// | 5             | 926.3           | DR13      | SF7  | 500      | 4/5 | 8                  |
// | 6             | 926.9           | DR13      | SF7  | 500      | 4/5 | 8                  |
// | 7             | 927.5           | DR13      | SF7  | 500      | 4/5 | 8                  |
long frequencies[] = { 923300, 923900, 924500, 925100, 925700, 926300, 926900, 927500 };
int factors[] = { 12, 11, 10, 9, 8, 7, 7, 7 };
 
void print_buffer_hex(const uint8_t *buf, size_t len)
{
	for (int i = 0; i < len; ++i) {
		printf("%02X", buf[i]);
	}
	printf("\n");
}
 
void blink(){
	gpio_set_level(BUILT_IN_LED, OFF);
	//^^^ First time lit is a signal found.
	vTaskDelay(50);
	gpio_set_level(BUILT_IN_LED, ON);
	//^^^ Stay lit default as a battery deployment test.
	//Temporary dip shows a new signal
	ESP_LOGI(pcTaskGetName(NULL), "LED has been lit.");
}
 
void receiveTask(void *pvParameters)
{
	int channelAttempts = 0;
	int channel = 0;
 
	ESP_LOGI(pcTaskGetName(NULL), "Start");
	uint8_t buf[255]; // Maximum Payload size of SX1276/77/78/79 is 255
	while(1) {
		lora_receive(); // put into receive mode
 
		if (lora_received()) {
			int rxLen = lora_receive_packet(buf, sizeof(buf));
			if (rxLen > 0){
				ESP_LOGI(pcTaskGetName(NULL), "%d byte packet received:[%.*s]", rxLen, rxLen, buf);
				print_buffer_hex(buf, rxLen);
				blinkRequest = 1;
			} else {
				ESP_LOGI(pcTaskGetName(NULL), "Likely noise.");
			}
		} 
 
		channelAttempts++;
		if (channelAttempts > MAX_CHANNEL_ATTEMPTS){
			channelAttempts = 0;
			channel++;
			if (channel >= 8 ){
				channel = 0;
			}
			ESP_LOGI(pcTaskGetName(NULL), "Switching to Frequency %.3f MHz and SF %d", (double)frequencies[channel]/1000, factors[channel]);
			lora_set_frequency(frequencies[channel]);
			lora_set_spreading_factor(factors[channel]);
		}
		vTaskDelay(1);
	} 
 
}
 
void blinkerTask(void *pvParameters)
{
	while(1) {
		if (blinkRequest == 1){
			blinkRequest = 0;
            blink();
        }
		vTaskDelay(50); 
	} 
}
 
void app_main(void)
{
    gpio_set_direction(BUILT_IN_LED, GPIO_MODE_OUTPUT);
 
	// Initialize LoRa
	if (lora_init() == 0) {
		ESP_LOGE(pcTaskGetName(NULL), "Does not recognize the module");
		while(1) {
			vTaskDelay(1);
		}
	}
 
	lora_set_frequency(frequencies[0]);
    ESP_LOGI(pcTaskGetName(NULL), "Frequency is %.3f MHz", (double)frequencies[0]/1000);
 
	lora_enable_crc();
 
    lora_set_coding_rate(CODING_RATE_4_5);
	ESP_LOGI(pcTaskGetName(NULL), "coding_rate=%d", CODING_RATE_4_5);
 
	lora_set_bandwidth(BW_500_KHZ);
	ESP_LOGI(pcTaskGetName(NULL), "bandwidth=%d", BW_500_KHZ);
 
	lora_set_spreading_factor(factors[0]);
	ESP_LOGI(pcTaskGetName(NULL), "spreading_factor=%d", factors[0]);
 
	lora_set_preamble_length(PREAMBLE_SYMBOLS);
	ESP_LOGI(pcTaskGetName(NULL), "preamble: %d", PREAMBLE_SYMBOLS);
 
	lora_set_sync_word(0x34);
	lora_explicit_header_mode();
 
 
    xTaskCreate(&receiveTask, "RX", 1024*3, NULL, 5, NULL);
    xTaskCreate(&blinkerTask, "BLINKER", 1024*2, NULL, 6, NULL);
 
}

Note that Sync Word of 0x34 is important for LoRaWAN packets.

Build and flash to your device and watch for the LED to light. Eventually you should strike gold with something like this:

I (4569637) RX: Switching to Frequency 927.500 MHz and SF 7
I (4590127) RX: Switching to Frequency 923.300 MHz and SF 12
I (4610617) RX: Switching to Frequency 923.900 MHz and SF 11
I (4631107) RX: Switching to Frequency 924.500 MHz and SF 10
I (4651597) RX: Switching to Frequency 925.100 MHz and SF 9
I (4672087) RX: Switching to Frequency 925.700 MHz and SF 8
I (4692577) RX: Switching to Frequency 926.300 MHz and SF 7
I (4708937) RX: 197 byte packet received:[w▒▒X▒gԆ▒&▒j▒m▒hs▒[▒2N▒R▒+_▒SC▒▒HE[▒▒▒=V▒▒▒▒-
6▒˓й="g+CB▒J▒jR ▒▒▒L]
77939258F567D486D126D26A916DAF6873FD5BA2324EE7529F2B5FE55343E1F648455BEDE7DE3D56B982A9CF012D0C36B3CB93D0B93D22672B43421DD94AA16A140252091BAFD6CEE4FB1A07084C0002A1ABEBD8CE3A8DB37FF013C963F23186AFEDFD8D962782DF75BD16D4ED776FF50FC21AD28030685379C4DA15811083515C2A3B2AE58C2B0CC9CD2114538688F126ADFC652C976B24E055FE43C0B2B5916221D0674CD368DB7A9289DE2B67805EF46A2A655BE060DF6F093C32A388F098AF47B948DD
I (4709847) BLINKER: LED has been lit.
I (4713097) RX: Switching to Frequency 926.900 MHz and SF 7
I (4733587) RX: Switching to Frequency 927.500 MHz and SF 7
I (4754077) RX: Switching to Frequency 923.300 MHz and SF 12
I (4774567) RX: Switching to Frequency 923.900 MHz and SF 11
I (4795057) RX: Switching to Frequency 924.500 MHz and SF 10
I (4815547) RX: Switching to Frequency 925.100 MHz and SF 9

You can copy the hex string out of the terminal output and pop it into a LoRaWAN packet decoder like Alien Fusion's LoRa Packet Decoder and check that it's legit.

In the example hex above we can decode that it is:

Unconfirmed Data Down (Type 3) LoRaWAN R4 177 bytes of payload.

That's paydirt, and that shows there is a gateway somewhere around that you can hear. Ensuring it can hear you will come down to choosing a good antenna and decent position for your end device. But that's for next time.

Have fun. Everyone loves a Mister Sniffles!

creating-a-lorawan-packet-sniffer.1788584874.txt.gz · Last modified: 2026/09/05 05:07 by sausage