ESP32 - LoRa - OLED Module

Get the most out of your (heltec/ttgo/aliexpress) ESP32 LoRa OLED development board

Similar projects worth following
Thank you HaD for making us a featured project!
This project aims to reveal the hidden secrets of this ESP32 WiFi Lora module. The number of details available for this board is growing, so new findings are added regularly to the log. Feel free to ask questions and get answers...

I have attached my current sample code to send and receive. It makes use of the Arduino LoRa Library by Sandeep Mistry and displays RSSI and SNR of the received data. 

The very versatile  RadioHead RF95 Library also works now without additional tweaks! You need at least version 1.80. Prior versions failed to compile (missing atomic.h).

Since these modules don't have a tcxo, there is a slight frequency mismatch. RadioHeads 'frequencyError()' tells me, the relative offset between my modules to be roughly approx. 1.5kHz. 

[2018-06-06] cool new Products:

Now this is a complete solution for a standalone GPS-Tracker based on LoRa:

TTGO T-Beam ESP32, LoRa with NEO-6M GPS and 18650 Battery holder

The module now also offers a dedicated ON/OFF Button. An external GPS Antenna is attached via U-FL and the LoRa Module now has an SMA Connector for the Antenna. Just keep in mind that this device does not come with an OLED display!

[03.03.2018] new Case on thingiverse:

A nice new case for the TTGO ESP LoRa OLED V2 module just popped up on thingiverse, published by [decoder]. In case you were not aware, V2 is the new version with the USB-port rotated by 90° apart on the wide side. From the case you have access to the Reset button, power on/of and on the SD Card slot.. The case also offers space for a small LiPo battery.

Check it out and send us your picture in case you have it printed!

[05.02.2018] Product update:

TTGO just recently released an upgrade to their popular LoRA Modules, referred to as V2.0. This board is supposed to be superior in design for both bands (433MHz and 868/915MHz).

New features include:

- microSD-Card Slot

- USB Port moved to the side

- shielded SX127x RF section

- new positioned WiFi antenna (the previous version sufferd from a poor design!)

- battery switch

Check out the V2.0 series at


product update: TTGO finally offers a 3d printed case/shell for the ESP32 Lora Module. Available in black or white. 

good news: 868MHz (EU) / 915MHz (US) Version now avaliable on Aliexpress


A new case design by [klkl] just (November, 26.) popped up on thingiverse:

Not sure if that one has enough space for a LiPo Battery beneath the module.  Since it's tagged 'work in progress', details and features might yet be optimized...


There's a nice case created by Aleksei Golikov / lexgol for the heltec Wifi LoRa 32 board:

Just download the files and have it printed yourself.


updated LoRa client, showing current frequencyError() in the top right corner. RSSI and SNR in the top left corner.

JPEG Image - 1.54 MB - 10/23/2017 at 12:18



Waterfall Display showing sender and receiver with exact matching frequencies. Modulation: 15.6kHz Bandwidth, 4/8 Coding Rate, Spreading Factor 512 on 433MHz ISM Band

JPEG Image - 220.78 kB - 10/23/2017 at 11:12



lora sender and receiver - receiver showing rssi and snr

JPEG Image - 1.79 MB - 10/18/2017 at 14:16



Example LoRa sender sketch. RF modem configured for long range transmission on 434.5MHz

x-arduino - 2.63 kB - 10/18/2017 at 14:04



Example LoRa receiver sketch showing received packets, RSSI and SNR on integrated OLED

x-arduino - 3.08 kB - 10/18/2017 at 14:03


View all 8 components

  • Module Testing & Some Useful Links

    reg.swensen11/18/2017 at 20:31 0 comments

    Range Testing 

    Transmitter at my desk

    Testing the TTGO Modules

    I spent a couple of hours this morning gathering some data from my two TTGO ESP32-LoRa radios to try to determine the best settings to use to get reliable communications in weak signal conditions. For the purposes of these tests I needed a relatively poor signal path so I wouldn't have to walk too far to run out of signal. To this end I placed the module programmed as the transmitter on my desk right next to my computer. My desk is located in a cubicle on the second floor of a brick building so it is surrounded on 3 sides by the (mostly) steel cubicle walls in addition to the usual clutter of steel and brick. The remaining side (south facing) is next to a window that looks down the longest sidewalk on campus. 

    Signal Path 1
    1500ft path with no solid obstructions
    Signal Path 2
    Shorter path with significant obstructions

    Signal Paths

    I chose two signal paths for evaluation. The first is straight down the sidewalk outside my window 1500ft (457m) to the entrance of the performing arts center. Along this path there are no buildings that stand directly in the way however there are quite a few trees. The path has buildings on either side that are likely to provide for some multipath reflections. The second signal path is a more difficult one in terms of RF penetration. The far point of that path is about 750ft (228m) to the northwest of my office. Signals traversing this path must pass through the wall of my cubicle as well as that of two adjacent cubicles. In addition the signal travels through the brick wall of my building and passes through the corner of a nearby building on the way. This path has less multipath opportunities but greater signal attenuation than the first path. To make the observations I walked both paths while watching the received packets on the OLED screen of my second TTGO module.

    Test Settings

    For all of the tests the transmitter was set to a center frequency of 433.000mHz with the transmitter power level of 20dBm. Neither figure was verified as I don't have the test gear to do so. The bandwidth setting of the LoRa modulator was set to 125kHz and forward error correction was set at 8/4 for all tests. The antennas used were the 'coil spring' antennas supplied with the modules. Packets consisted of my Amateur Radio callsign followed by a space, a # character, and a sequential number. They varied in length from 8 to 11 characters. I varied the LoRa spreading factor from 7 to 11 and took readings of RSSI and reported SNR from the receiver at the far end of each path. The receive antenna was held at about 1 meter off the ground and oriented vertically to match the transmitter antenna. The data are shown below:

    Spreading factorRSSI (path 1)SNR (path 1)RSSI (path 2)SNR (Path 2)
    7 (128 chips/bit)-114 dBm13.25 dB-111 dBm14.5 dB
    8 (256 chips/bit)-109 dBm14.0 dB-113 dBm14.25 dB
    9 (512 chips/bit)-105 dBm15.5 dB-107 dBm16.25 dB
    10 (1024 chips/bit)-106 dBm16.5 dB-105 dBm16.75 dB
    11 (2048 chips/bit)-103 dBm17.75 dB-102 dBm17.25 dB
    RTL-SDR display of transmitted signal
    Signal as displayed by RTL-SDR (sf = 11, BW = 125Khz)


    While the reported signal levels and SNR figures all appeared to improve as I increased the spreading factor, I didn't see the amount of improvement that I might have been led to expect from reading Semtech's marketing literature. I also noticed that contrary to what one might intuitively expect, as the spreading factor was increased the reliability of packet receipt while moving (walking) went down considerably. By the time I got to sf = 11 there were practically no packets being received while moving but reception would resume immediately when I stopped and held the receiver still. I believe that this may have been the result of multipath distortion since it affected path 1 more than path 2 and was worse adjacent to buildings beside the path. The most reliable packet reception while moving was at sf = 7. In fact this was the only...

    Read more »

  • RadioHead library v1.81 update

    data11/15/2017 at 19:43 0 comments

    New RadioHead library v1.81 available.

    LoRa relevant updates: 

    - slow data rate for predefined ModemConfig with Sf 4096 enabled

    - AGC for all modes enabled

    Download from

    1.81 2017-11-15 RH_CC110, moved setPaTable() from protected to public.
    RH_RF95 modem config Bw125Cr48Sf4096 altered to enable slow daat rate in register 26 as suggested by Dieter Kneffel. Added support for nRF52 compatible Arm chips such as as Adafruit BLE Feather board, with a patch from Mike Bell.
    Fixed a problem where rev 1.80 broke Adafruit M0 LoRa support by declaring bitOrder variable always as a unsigned char. Reported by Guilherme Jardim.
    In RH_RF95, all modes now have AGC enabled, as suggested by Dieter Kneffel.

  • Bluetooth

    data10/25/2017 at 21:03 0 comments

    Included within the esp32 repository as well as the recently added one from heltec ( ) is an example for using Bluetooth. I just gave it a try but besides advertising its name via BLE there is nothing else supported yet :(

    One can not even pair this device yet, just update the advertised name. At least, this can be used to broadcast some sort of  data - e.g. temperature and humidity from a connected DHT22 or DS18b20... 

    Once espressif has their repository updated and full bluetooth/BLE support added, a number of interesting applications come to mind: e.g. a Bluetooth to LoRa bridge for a long range bluetooth chat.

  • RadioHead RF95 Driver / Low Data Rate Optimization

    data10/24/2017 at 03:28 2 comments

    Digging somewhat deeper into the RadioHead driver, I noticed an issue with the predefined ModemConfiguration parameters. The third configuration register (0x26) is always 0x00. This is, how they are defined in RH_RF95.cpp:

    PROGMEM static const RH_RF95::ModemConfig MODEM_CONFIG_TABLE[] =
        //  1d,     1e,      26
        { 0x72,   0x74,    0x00}, // Bw125Cr45Sf128 (the chip default)
        { 0x92,   0x74,    0x00}, // Bw500Cr45Sf128
        { 0x48,   0x94,    0x00}, // Bw31_25Cr48Sf512
        { 0x78,   0xc4,    0x00}, // Bw125Cr48Sf4096

    Now, I found in the SX127x application note ( ) that the third register 0x26 has two functions: AGC and Low Data Rate Optimization. The later being mandatory for spreading factors >= 11

    So for a Sf of 11 (2048) or 12 (4096), it should be set. The predefined configuration for Bw 125kHz, Cr 4/8 with Sf 4096 should actually read    { 0x78, 0xc4, 0x08 }

    Also keep this in mind when creating your own configuration parameters.

  • RadioHead RF95 Driver - advantages:

    data10/23/2017 at 10:59 0 comments

    After some tweaking, I now drastically reduced the  offset (aka frequency mismatch) between my modules.

    Without any matching, I had a deviation of approx. 1.500Hz. Now, with dynamically adjusting the frequency based on the reported difference, I am able to keep the offset around +/- 10Hz. That's an improvement by a factor of  more than 100!

    Due to this improvement, I hope to make use of even lower bandwidths in the future.  Instead of the predefined ModemConfigs available in the RF95 driver,  Using setModemRegisters(), I've set my modules to a bandwidth of 15.6kHz, Coding Rate of 4/8 and a Spreading Factor of 512.  A quick look at gqrx confirmed the desired bandwidth and exact identical/matching frequency.

    I am pretty curious how much this will affect the range. Report follows once I have some time...

  • Battery Voltage Measurement

    data10/23/2017 at 10:58 3 comments

    ADC / Battery Voltage measurement:

    So far, I had no luck in getting proper voltage readings from one of the ADC inputs. Since there is no documentation, I simply queried all ADC pins in a hope to get one with some voltage readings. Here's part of the code: 


     int pinCount = 11;  int ADCpins[] = {2,12,13,32,33,34,35,36,37,38,39}; // these are the ADC pins

    float VBAT;  // battery voltage from ESP32 ADC read float ADC_divider = 250/30;  // voltage divider proportions - hypothetical so far :-)

    void setup(){

     for (int thisPin = 0; thisPin < pinCount; thisPin++) {

        Serial.print(thisPin, DEC);     Serial.print(" = ");     Serial.print(ADCpins[thisPin], DEC);     Serial.print(" => ");

        pinMode(ADCpins[thisPin], INPUT);

        VBAT = ADC_divider * (float)(analogRead(ADCpins[thisPin])) / 1024.0; // LiPo battery voltage in volts     Serial.print("Vbat = "); Serial.print(VBAT); Serial.println(" Volts"); }   


    And this is, what I get: 

    0 = 2 => Vbat = 0.00 Volts

    1 = 12 => Vbat = 0.00 Volts

    2 = 13 => Vbat = 0.00 Volts

    3 = 32 => Vbat = 3.52 Volts

    4 = 33 => Vbat = 1.16 Volts

    5 = 34 => Vbat = 0.00 Volts

    6 = 35 => Vbat = 0.00 Volts

    7 = 36 => Vbat = 0.00 Volts

    8 = 37 => Vbat = 0.00 Volts

    9 = 38 => Vbat = 0.00 Volts

    10 = 39 => Vbat = 0.00 Volts

    So I do get some reading on ADC pin 32 and 33 but it does not really make sense so far.

    The value remains more or less the same when I remove the battery...

View all 6 project logs

  • 1
    RadioHead library: custom modem configurations

    A brief application note if you are using the RadioHead library.
    To the moment, the RadioHead library offers only a set of four predefined modem configurations. These can be found in RH_RF95.cpp:

        { 0x72,   0x74,    0x00}, // Bw125Cr45Sf128 (the chip default)
        { 0x92,   0x74,    0x00}, // Bw500Cr45Sf128
        { 0x48,   0x94,    0x00}, // Bw31_25Cr48Sf512
        { 0x78,   0xc4,    0x00}, // Bw125Cr48Sf4096

    The first of these three bytes is written to configuration register 0x1d, the second to 0x1e and the third into 0x26. Relevant settings include:

    RegModemConfig 1 / 0x1D

    • signal bandwidth (7.8kHz to 500kHz)
    • codingRate 4/4 to 4/8
    • implicit Header on/off

    RegModemConfig 2 / 0x1E

    • spreading factor (64chips/symbol to 4096 chips/symbol)
    • CRC on/off

    RegModemConfig 3 / 0x26

    • 'Low Data Rate Optimization' aka MobileNode
    • automatic AGC on/off

    Utilizing one of the predefined settings, you would want to  initialize your modem like this:

    #define ModemConfig RH_RF95::Bw31_25Cr48Sf512

    And  instead of using one of the predefined settings, you can configure your LoRa mode according your needs:

    RH_RF95::ModemConfig myconfig=  { 0x28,   0x94,    0x04}; // Bw15_6Cr48Sf512 
    // will give you
    // 15.6kHz bandwidth, 4/8 coding rate, spreading factor  9 (512), low data rate optimization off, AGC on

    Now, if this seems too cryptic to you, this can be rewritten into a more human readable form. Fortunatelly, the available options are predefined accordingly in RH_RF95.h:

    // RH_RF95_REG_1D_MODEM_CONFIG1                       0x1d
    #define RH_RF95_BW                                    0xf0
    #define RH_RF95_BW_7_8KHZ                             0x00
    #define RH_RF95_BW_10_4KHZ                            0x10
    #define RH_RF95_BW_15_6KHZ                            0x20
    #define RH_RF95_BW_20_8KHZ                            0x30
    #define RH_RF95_BW_31_25KHZ                           0x40
    #define RH_RF95_BW_41_7KHZ                            0x50
    #define RH_RF95_BW_62_5KHZ                            0x60
    #define RH_RF95_BW_125KHZ                             0x70
    #define RH_RF95_BW_250KHZ                             0x80
    #define RH_RF95_BW_500KHZ                             0x90
    #define RH_RF95_CODING_RATE                           0x0e
    #define RH_RF95_CODING_RATE_4_5                       0x02
    #define RH_RF95_CODING_RATE_4_6                       0x04
    #define RH_RF95_CODING_RATE_4_7                       0x06
    #define RH_RF95_CODING_RATE_4_8                       0x08
    #define RH_RF95_IMPLICIT_HEADER_MODE_ON               0x01
    // RH_RF95_REG_1E_MODEM_CONFIG2                       0x1e
    #define RH_RF95_SPREADING_FACTOR                      0xf0
    #define RH_RF95_SPREADING_FACTOR_64CPS                0x60
    #define RH_RF95_SPREADING_FACTOR_128CPS               0x70
    #define RH_RF95_SPREADING_FACTOR_256CPS               0x80
    #define RH_RF95_SPREADING_FACTOR_512CPS               0x90
    #define RH_RF95_SPREADING_FACTOR_1024CPS              0xa0
    #define RH_RF95_SPREADING_FACTOR_2048CPS              0xb0
    #define RH_RF95_SPREADING_FACTOR_4096CPS              0xc0
    #define RH_RF95_TX_CONTINUOUS_MOE                     0x08
    #define RH_RF95_PAYLOAD_CRC_ON                        0x04
    #define RH_RF95_SYM_TIMEOUT_MSB                       0x03

    So your custom modem configuration can be rewritten into this:

    // so instead of 
    RH_RF95::ModemConfig myconfig=  { 0x28,   0x94,    0x04};
    // you can use
    RH_RF95::ModemConfig myconfig =  {  RH_RF95_BW_15_6KHZ | RH_RF95_CODING_RATE_4_8, RH_RF95_SPREADING_FACTOR_512CPS, 0x04};
    // where the last parameter (0x04) turns the AGC on

    All relevant registers are described also within the SX127x datasheet, available e.g. at

    Happy coding!


View all instructions

Enjoy this project?



prosto wrote 06/20/2018 at 17:51 point

is possible to create similar device but open source?

  Are you sure? yes | no

data wrote 06/21/2018 at 10:32 point

Absolutely, yes.
Do you know how to code? Do you want to join??

  Are you sure? yes | no

bosmith wrote 04/25/2018 at 15:19 point

Fun modules (wifi,oled,bt,lora!) but how to extend range between two nodes?

I recently received the Heltec Esp32 w/Lora.wifi, oled, got two.

Been testing the examples in the Arduino IDE with the espressif libs, all is working after a few setup hiccups. (lib at: )

As tested out of the box with the cheapo antennas and no case or shielding, I can send and receive about 1/2 mile ( .8 kilometer ). I tried different Spreading Factors but that did not make a difference.

I’m hoping to extend this range, any DIY ideas?

Some interesting links on LoRa range:

Andreas Spiess (Swiss accent guy):

Or build your own antenna?

Some of my Esp32 Lora action pix at:

  Are you sure? yes | no

data wrote 05/24/2018 at 07:16 point

- What bitrate are using? Have you tried reducing it as well?

- Please check if the provided antenna does even match your used frequency range.
  A good antenna is the best rf-amplifier...

  Are you sure? yes | no

Roman.Sokolov wrote 03/31/2018 at 15:33 point

could you please help with example of using RadioHead lib with Heltec ESP32 OLED LORA for transmitting and receiving. 

  Are you sure? yes | no

data wrote 05/24/2018 at 07:26 point

You can try the examples for rf95 provided by the RadioHead Library. You just have to specify the proper SPI pins. Here are the proper settings for the LoRa module as well as the included OLED Display:

#include <Wire.h>  // Only needed for Arduino 1.6.5 and earlier
#include "SSD1306.h" // alias for `#include "SSD1306Wire.h"`

#include <SPI.h>
#include <RH_RF95.h>

SSD1306 display(0x3c, 4, 15);

// WIFI_LoRa_32 ports

// GPIO5 — SX1278’s SCK
// GPIO19 — SX1278’s MISO
// GPIO27 — SX1278’s MOSI
// GPIO18 — SX1278’s CS
// GPIO14 — SX1278’s RESET
// GPIO26 — SX1278’s IRQ(Interrupt Request)

#define SS 18
#define RST 14
#define DI0 26

// Singleton instance of the radio driver
RH_RF95 rf95(SS, DI0); // [heltec|ttgo] ESP32 Lora OLED with sx1278


Let me know if you need more details.

  Are you sure? yes | no

Msciegienny wrote 02/19/2018 at 12:53 point

is it possible to use OLED and DHT31 /i2c temp sensor/ together? I can use OLED but I must comment out DHT31 code, and vice versa - when I turn on OLED DHT31 won't work ( gives me NaN value)... DHT31 is in 21 and 22 pin

  Are you sure? yes | no

data wrote 02/19/2018 at 13:30 point

Have you tried connecting the sensor to the same i2c bus (SDA: 4, SDL: 15) in use by the oled? Please post your code!

  Are you sure? yes | no

morgan wrote 01/22/2018 at 00:12 point

Did you ever hit any issue with the OLED not initializing? I'm getting...

I (718) SSD1306: Enabling i2c on
SDA: 4
SCL: 15

E (728) SSD1306: OLED configuration failed. code: 0xFFFFFFFF

I have a vague recollection of a similar error on a different ESP32 device but cannot recall a solution.

  Are you sure? yes | no

data wrote 01/22/2018 at 07:29 point

I have not encountered this problem yet. Which display library are you using?

  Are you sure? yes | no

morgan wrote 01/22/2018 at 18:52 point

I'm using a fork of driver by yanbe, This worked fine on another device with the same screen but just on different pins. I've updated to emulate the Arduino drivers init sequence to no avail.

  Are you sure? yes | no

data wrote 01/22/2018 at 20:25 point

There are a couple of libraries for the ssd1306 you can choose from:

Maybe it'll work with one of these without problems?

  Are you sure? yes | no

morgan wrote 01/22/2018 at 21:48 point

Welp, I figured it out, and all of those failed for the same reason. The previous board I was using apparently takes care of resetting the display in hardware. The example code I'd glanced at before digging into the driver, also took care of this. Look forward to some LoRa hacking with you.

  Are you sure? yes | no

daniel.nguyen2 wrote 01/23/2018 at 11:22 point

Hello Morgan

Have a look here :

This is the github dedicated to Heltec board

In this example you can see,  it's necessary to reset OLED before init it



void setup() {
  digitalWrite(16, LOW);    // set GPIO16 low to reset OLED
  delay(50);   digitalWrite(16, HIGH); // while OLED is running, must set GPIO16 in high、

  Are you sure? yes | no

Sergey wrote 12/24/2017 at 15:35 point


I do not see appropriate port for ESP32 LoRa Module in Arduino IDE (Tools > Port) when connect the Module to my Mac USB Port (macOS 10.13.2, Arduino 1.8.5).

Thanks in advance!


  Are you sure? yes | no

ccorrea wrote 12/19/2017 at 18:21 point


Nice project!. I want to make something similar but I can't start because  I can't download the installation library? Have you an alternative download link? I can't download it from Baidu.



  Are you sure? yes | no

data wrote 12/19/2017 at 18:24 point

Dear Christian.

what library are you referring to?  I don't see a link to baidu??

  Are you sure? yes | no

borie.f wrote 11/01/2017 at 08:16 point

Hello data thank you, in fact i want to know if this tuning is specific for one shield or you must do a mesure for each shields (even if the shield is of the same brand/model). what is this tool who generate this information ? 

  Are you sure? yes | no

data wrote 11/01/2017 at 14:59 point

I can only guess, that you are referring to the frequency error?!
You can can obtain this value by calling frequencyError() from the RadioHead library after having received a packet.

Due to the use of cheap components, every module is supposed to have an offset. On the other hand, the LoRa protocol is robust enough to cope with even a large offset. According to manufacturer specs, the

Maximum tolerated frequency offset between transmitter and receiver, no sensitivity degradation, SF6 thru 12 is +- 25% of BW

Hence, the recommended min. bandwidth is 62.5kHz for modules without a TCXO.  This also gives you more than enough margin for the doppler effect, if your devices are on the move.


Returns the last measured frequency error. The LoRa receiver estimates the frequency offset between the receiver centre frequency and that of the received LoRa signal. This function returns the estimates offset (in Hz) of the last received message. Caution: this measurement is not absolute, but is measured relative to the local receiver's oscillator. Apparent errors may be due to the transmitter, the receiver or both.
ReturnsThe estimated centre frequency offset in Hz of the last received message. If the modem bandwidth selector in register RH_RF95_REG_1D_MODEM_CONFIG1 is invalid, returns 0.


  Are you sure? yes | no

borie.f wrote 10/31/2017 at 21:30 point

Hello, thank you for sharing your project. can you detail to me your parameters :

1/     #define spreadingFactor 9
// #define SignalBandwidth 62.5E3
2/    #define SignalBandwidth 31.25E3
3/    #define preambleLength 8
4/    #define codingRateDenominator 8

  Are you sure? yes | no

data wrote 10/31/2017 at 22:54 point

What kind of details do you want to know?

In brief, less bandwidth and higher spreading factor means slower datarate but longer range.

Have a look at the API documentation at

  Are you sure? yes | no

ia wrote 10/23/2017 at 14:38 point

Meybe add a sound modem?

for example using baofeng to increase distance.

i nead keyboard 4-5 buttons

and mesh or normal ax25 protocol on lora

  Are you sure? yes | no

data wrote 10/18/2017 at 14:07 point

Does anybody know to which ADC pin the LiPo circuit is connected? I'd like to display charging status as well...

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates