Here is a simple working code for 2 LILYGO TTGO LoRa32 V2.1_1.6 (915 MHz) to exchange data.
Hoping that this could be useful to someone these LoRa device over a very long distance.
There is 2 devices 1) Cabane and 2) Station.
Both devices can send packet to the other device, received packet form the other device and display data.
I create a simple data structure that contains
2 temperatures values, for temperature outside and inside a water tank
1 single integer to represent the level of water in the tank
1 boolean flag to indicate if the water pump in active or not (will be use to start /stop pum
3 integer to store the time of the message Hour Minutes and Seconds.
My plan is to use these units in a Sugar Shack with multiple pumping stations.
Notes, none of the sensors are yet implemented. This will come later with a GPS unit to gather the time, since there is no WiFi.
Here is the code for the Cabane (Base Station)
# Gaston Paradis
# Program to demonstrate 2 devices LILYGO TTGO LoRa32 V2.1_1.6 (915 MHz) communicating
# There is a software button on each device that will send its data to the other device.
# The data is source from the sending device and store in globals variables to be display
# The display shows the data of both devices o the screen
# THIS IS THE CABANE base station
# 02/09.25 Finalize data structure, correction to encoding and decoding code
# 02/08/25 Adding Data Structure and coding, decoding displaying
# 02/06/25 V2.0 Added configuration fro LoRa component
# 02/06/25 V1.0 Display ONLY Hello LoRa Cabane
substitutions:
devicename: ttgo_lora_cabane
friendly: TTGO LoRa Cabane # This is the name sent to the frontend. It is used by Home Assistant
# as the integration name, device name,
# and is automatically prefixed to entities where necessary.#
comment: LoRa Cabane # Display in ESPHome
esphome:
name: ttgo-lora
friendly_name: ${friendly}
platform: esp32
board: ttgo-lora32-v21
logger: # Enable logging
api: # Enable Home Assistant API
ota: # Enable OverThe Air ota update
- platform: esphome
password: !secret ota_password
wifi: # Enable WiFi
ssid: !secret wifi_ssid
password: !secret wifi_password
ap: # Enable an access point mode on the node in case wifi connection fails
ssid: ${devicename}
password: !secret ap_wifi_password
captive_portal: # component is a fallback mechanism for when connecting to the configured WiFi fails
web_server:
port: 80
i2c: # I2C for OLED
sda: 21
scl: 22
scan: true
spi:
- id: spi_lora # LoRa SPI
clk_pin: GPIO5
miso_pin: GPIO19
mosi_pin: GPIO27
external_components:
- source: github://swoboda1337/sx127x-esphome
# Example configuration entry
sx127x:
dio0_pin: GPIO26
cs_pin: GPIO18
rst_pin: GPIO23
pa_pin: BOOST
pa_power: 17
bitsync: true
bitrate: 4800
frequency: 915000000 # 915 MHz in Hz
modulation: FSK
rx_start: true
payload_length: 10
sync_value: [0x33, 0x33]
preamble_size: 2
preamble_errors: 8
preamble_polarity: 0x55
on_packet:
then:
- logger.log: "Receiving a package"
- lambda: |-
ESP_LOGD("LoRa", "Raw Packet Data: %s", format_hex(x).c_str());
if (x.size() == 10) { // Ensure the packet size is correct
id(originator_1) = static_cast<uint8_t>(x[0]); // Extract Originator and save in Globals Variable
// Temp 1 (reconstruct int16_t and divide)
int16_t received_temp1 = (x[2] << 8) | x[1]; // Combine high and low bytes
id(temp1_1) = static_cast<float>(received_temp1) / 10.0; // Divide by 10.0 (float)
// Temp 2 (reconstruct int16_t and divide)
int16_t received_temp2 = (x[4] << 8) | x[3]; // Combine high and low bytes
id(temp2_1) = static_cast<float>(received_temp2) / 10.0; // Divide by 10.0 (float)
id(water_level_1) = static_cast<uint8_t>(x[5]); // Extract Water Temperature and save in Globals Variable
id(relay_state_1) = static_cast<uint8_t>(x[6]); // Extract Relay boolean and save in Globals Variable
id(hours_1) = static_cast<uint8_t>(x[7]); // Extract Hours and save in Globals Variable
id(minutes_1) = static_cast<uint8_t>(x[8]); // Extract Minutes and save in Globals Variable
id(seconds_1) = static_cast<uint8_t>(x[9]); // Extract Seconds and save in Globals Variable
ESP_LOGD("LoRa", "Received from %d | Temp1: %.1f°C | Temp2: %.1f°C | Water: %d | Relay: %s | Time: %02d:%02d:%02d",
id(originator_1), id(temp1_1), id(temp2_1), id(water_level_1), id(relay_state_1) ? "ON" : "OFF", id(hours_1), id(minutes_1), id(seconds_1));
ESP_LOGD("LoRa", "Raw Packet Data: %s", format_hex(x).c_str());
} else {
ESP_LOGW("LoRa", "Received packet with incorrect size (%d bytes)", x.size());
}
button:
- platform: template # Prepare and send packet
name: "Transmit Packet"
on_press:
then:
- sx127x.send_packet:
data: !lambda |-
std::vector<uint8_t> data;
data.push_back(id(originator_0)); //x[0] Originator
// Temp 1 (scaled and cast to int16_t - more range) // Outside Temperature
int16_t scaled_temp1 = static_cast<int16_t>(id(temp1_0) * 10); // Scale by 10 for one decimal place
data.push_back(scaled_temp1 & 0xFF); //x[1] Low byte
data.push_back((scaled_temp1 >> 8) & 0xFF); //x[2] High byte
// Temp 2 (similarly for int16_t) // Water Temperature
int16_t scaled_temp2 = static_cast<int16_t>(id(temp2_0) * 10); // Scale by 10 for one decimal place
data.push_back(scaled_temp2 & 0xFF); //x[3] Low byte
data.push_back((scaled_temp2 >> 8) & 0xFF); //x[4] High byte
data.push_back(id(water_level_0)); //x[5] Water Level
data.push_back(id(relay_state_0) ? 1 : 0); //x[6] Relay State (ON)
data.push_back(id(hours_0)); //x[7] Hours
data.push_back(id(minutes_0)); //x[8] Minutes
data.push_back(id(seconds_0)); //x[9] Seconds
return data;
ESP_LOGD("LoRa", "Raw Packet Data: %s", format_hex(data).c_str()); // Debuggimg same data received at the station
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
update_interval: 15s
lambda: |-
it.print (0, 0, id(arimo12), "00");
it.print (35, 0, id(arimo12), "Cabane");
it.print (85, 0, id(arimo12), "Station");
it.print (0, 12, id(arimo12), "Ext");
it.print (0, 22, id(arimo12), "Water");
it.print (0, 32, id(arimo12), "Level");
it.print (0, 42, id(arimo12), "Pump");
it.print (0, 52, id(arimo12), "Time");
it.printf(36, 12, id(arimo12), id(RED), "%.1f°C", id(temp1_0));
it.printf(36, 22, id(arimo12), id(GREEN), "%.1f°C", id(temp2_0));
it.printf(36, 32, id(arimo12), id(BLUE), "%d", id(water_level_0));
it.printf(36, 42, id(arimo12), id(YELLOW), "%s", id(relay_state_0) ? "ON" : "OFF");
it.printf(36, 52, id(arimo12), id(CYAN), "%02d:%02d", id(hours_0), id(minutes_0) );
it.printf(85, 12, id(arimo12), id(RED), "%.1f°C", id(temp1_1));
it.printf(85, 22, id(arimo12), id(GREEN), "%.1f°C", id(temp2_1));
it.printf(85, 32, id(arimo12), id(BLUE), "%d", id(water_level_1));
it.printf(85, 42, id(arimo12), id(YELLOW), "%s", id(relay_state_1) ? "ON" : "OFF");
it.printf(85, 52, id(arimo12), id(CYAN), "%02d:%02d", id(hours_1), id(minutes_1) );
font: # Create a font to use, add and remove glyphs as needed.
- file: 'fonts/Arimo-Regular.ttf'
id: arimo12
size: 12
glyphs: "<>!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzé"
- file: 'fonts/Arimo-Regular.ttf'
id: arimo24
color:
- id: RED
red: 100%
#green: 0%
#blue: 0%
- id: GREEN
#red: 0%
green: 100%
#blue: 0%
- id: BLUE
#red: 0%
#green: 0%
blue: 100%
- id: WHITE
red: 100%
green: 100%
blue: 100%
- id: BLACK
red: 0%
green: 0%
blue: 0%
- id: CYAN
#red: 0%
green: 100%
blue: 100%
- id: YELLOW
red: 100%
green: 100%
#blue: 0%
- id: PINK
red_int: 225
green_int: 105
blue_int: 180
- id: color_text
red: 100%
#green: 0%
#blue: 0%
globals:
# Message Data Structure
# Field Type Bytes Range Notes Description
# Originator uint8_t 1 0-255 0 = Base, 1 = Station 1, 2 = Station 2 Originating Location
# Temp 1 int8_t 1 -20 to 100 Signed integer (scaled to 0.1°C) Outside Temperature
# Temp 2 int8_t 1 -20 to 100 Same as above Water Temperature
# Water Level uint8_t 1 1-4 Single byte This Level of water in the tank
# Relay State bool 1 0 or 1 Boolean flag Indicate that the pump is on/off
# Timestamp uint16_t 2 0-86399 Seconds since midnight (HH*3600 + MM*60 + SS)
- id: originator_0 # MSG Data Structure. Originator of the message 0: Cabane; 1: Station Pompage 1; 2: Station Pompage 2
type: int
restore_value: no
initial_value: "0"
- id: temp1_0 # MSG Data Structure.Outside temperature
type: float
restore_value: no
initial_value: "-5.2"
- id: temp2_0 # MSG Data Structure. Water Temperature in the tank
type: float
restore_value: no
initial_value: "10.6"
- id: water_level_0 # MSG Data Structure. Water level in the tank
type: int
restore_value: no
initial_value: "2"
- id: relay_state_0 # MSG Data Structure. Will be use to trigger the water pump
type: bool
restore_value: no
initial_value: "true"
- id: hours_0 # MSG Data Structure. Hour the message was send
type: int
restore_value: no
initial_value: "15"
- id: minutes_0 # MSG Data Structure. Minute the message was send
type: int
restore_value: no
initial_value: "20"
- id: seconds_0 # MSG Data Structure. Second the message was send
type: int
restore_value: no
initial_value: "10"
- id: originator_1 # MSG Data Structure. Originator of the message 0: Cabane; 1: Station Pompage 1; 2: Station Pompage 2
type: int
restore_value: no
initial_value: "0"
- id: temp1_1 # MSG Data Structure.Outside temperature
type: float
restore_value: no
initial_value: "0.0"
- id: temp2_1 # MSG Data Structure. Water Temperature in the tank
type: float
restore_value: no
initial_value: "0.0"
- id: water_level_1 # MSG Data Structure. Water level in the tank
type: int
restore_value: no
initial_value: "0"
- id: relay_state_1 # MSG Data Structure. Will be use to trigger the water pump
type: bool
restore_value: no
initial_value: "false"
- id: hours_1 # MSG Data Structure. Hour the message was send
type: int
restore_value: no
initial_value: "0"
- id: minutes_1 # MSG Data Structure. Minute the message was send
type: int
restore_value: no
initial_value: "0"
- id: seconds_1 # MSG Data Structure. Second the message was send
type: int
restore_value: no
initial_value: "0"
And the code for the pumping station
# Gaston Paradis
# Program to demonstrate 2 devices LILYGO TTGO LoRa32 V2.1_1.6 (915 MHz) communicating
# There is a software button on each device that will send its data to the other device.
# The data is source from the sending device and store in globals variables to be display
# The display shows the data of both devices o the screen
# THIS IS THE CABANE base station
# 02/09.25 Finalize data structure, correction to encoding and decoding code
# 02/08/25 Adding Data Structure and coding, decoding displaying
# 02/06/25 V2.0 Added configuration fro LoRa component
# 02/06/25 V1.0 Display ONLY Hello LoRa Cabane
substitutions:
devicename: ttgo-lora-station
friendly: TTGO LoRa Station # This is the name sent to the frontend. It is used by Home Assistant
# as the integration name, device name,
# and is automatically prefixed to entities where necessary.#
comment: LoRa Cabane # Display in ESPHome
esphome:
name: ${devicename}
friendly_name: ${friendly}
platform: esp32
board: ttgo-lora32-v21
logger: # Enable logging
api: # Enable Home Assistant API
ota: # Enable OverThe Air ota update
- platform: esphome
password: !secret ota_password
wifi: # Enable WiFi
ssid: !secret wifi_ssid
password: !secret wifi_password
ap: # Enable an access point mode on the node in case wifi connection fails
ssid: ${devicename}
password: !secret ap_wifi_password
captive_portal: # component is a fallback mechanism for when connecting to the configured WiFi fails
web_server:
port: 80
i2c: # I2C for OLED
sda: 21
scl: 22
scan: true
spi:
- id: spi_lora # LoRa SPI
clk_pin: GPIO5
miso_pin: GPIO19
mosi_pin: GPIO27
external_components:
- source: github://swoboda1337/sx127x-esphome # Thanks to Jonathan Swoboda
sx127x: # Documentation https://deploy-preview-4278--esphome.netlify.app/components/sx127x
# In packet mode the sx127x is used as both a transmitter and receiver in this program
dio0_pin: GPIO26
cs_pin: GPIO18
rst_pin: GPIO23
pa_pin: BOOST
pa_power: 17
bitsync: true
bitrate: 4800
frequency: 915000000 # 915 MHz in Hz
modulation: FSK
rx_start: true
payload_length: 10 # This is the size of the packet
sync_value: [0x33, 0x33]
preamble_size: 2
preamble_errors: 8
preamble_polarity: 0x55
on_packet:
then:
- logger.log: "Receiving a package"
- lambda: |-
ESP_LOGD("LoRa", "Raw Packet Data: %s", format_hex(x).c_str());
if (x.size() == 10) { // Ensure the packet size is correct
id(originator_0) = static_cast<uint8_t>(x[0]); // Extract Originator and save in Globals Variable
// Temp 1 (reconstruct int16_t and divide)
int16_t received_temp1 = (x[2] << 8) | x[1]; // Combine high and low bytes
id(temp1_0) = static_cast<float>(received_temp1) / 10.0; // Divide by 10.0 (float)
// Temp 2 (reconstruct int16_t and divide)
int16_t received_temp2 = (x[4] << 8) | x[3]; // Combine high and low bytes
id(temp2_0) = static_cast<float>(received_temp2) / 10.0; // Divide by 10.0 (float)
id(water_level_0) = static_cast<uint8_t>(x[5]); // Extract Water Temperature and save in Globals Variable
id(relay_state_0) = static_cast<uint8_t>(x[6]); // Extract Relay boolean and save in Globals Variable
id(hours_0) = static_cast<uint8_t>(x[7]); // Extract Hours and save in Globals Variable
id(minutes_0) = static_cast<uint8_t>(x[8]); // Extract Minutes and save in Globals Variable
id(seconds_0) = static_cast<uint8_t>(x[9]); // Extract Seconds and save in Globals Variable
ESP_LOGD("LoRa", "Received from %d | Temp1: %.1f°C | Temp2: %.1f°C | Water: %d | Relay: %s | Time: %02d:%02d:%02d",
id(originator_0), id(temp1_0), id(temp2_0), id(water_level_0), id(relay_state_0) ? "ON" : "OFF", id(hours_0), id(minutes_0), id(seconds_0));
ESP_LOGD("LoRa", "Raw Packet Data: %s", format_hex(x).c_str());
} else {
ESP_LOGW("LoRa", "Received packet with incorrect size (%d bytes)", x.size());
}
button:
- platform: template # Prepare and send packet
name: "Transmit Packet"
on_press:
then:
- sx127x.send_packet:
data: !lambda |-
std::vector<uint8_t> data;
data.push_back(id(originator_1)); //x[0] Originator
// Temp 1 (scaled and cast to int16_t - more range) // Outside Temperature
int16_t scaled_temp1 = static_cast<int16_t>(id(temp1_1) * 10); // Scale by 10 for one decimal place
data.push_back(scaled_temp1 & 0xFF); //x[1] Low byte
data.push_back((scaled_temp1 >> 8) & 0xFF); //x[2] High byte
// Temp 2 (similarly for int16_t) // Water Temperature
int16_t scaled_temp2 = static_cast<int16_t>(id(temp2_1) * 10); // Scale by 10 for one decimal place
data.push_back(scaled_temp2 & 0xFF); //x[3] Low byte
data.push_back((scaled_temp2 >> 8) & 0xFF); //x[4] High byte
data.push_back(id(water_level_1)); //x[5] Water Level
data.push_back(id(relay_state_1) ? 1 : 0); //x[6] Relay State (ON)
data.push_back(id(hours_1)); //x[7] Hours
data.push_back(id(minutes_1)); //x[8] Minutes
data.push_back(id(seconds_1)); //x[9] Seconds
return data;
ESP_LOGD("LoRa", "Raw Packet Data: %s", format_hex(data).c_str());
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
update_interval: 15s
lambda: |-
it.print (0, 0, id(arimo12), "01");
it.print (35, 0, id(arimo12), "Cabane");
it.print (85, 0, id(arimo12), "Station");
it.print (0, 12, id(arimo12), "Ext");
it.print (0, 22, id(arimo12), "Water");
it.print (0, 32, id(arimo12), "Level");
it.print (0, 42, id(arimo12), "Pump");
it.print (0, 52, id(arimo12), "Time");
it.printf(36, 12, id(arimo12), id(RED), "%.1f°C", id(temp1_0));
it.printf(36, 22, id(arimo12), id(GREEN), "%.1f°C", id(temp2_0));
it.printf(36, 32, id(arimo12), id(BLUE), "%d", id(water_level_0));
it.printf(36, 42, id(arimo12), id(YELLOW), "%s", id(relay_state_0) ? "ON" : "OFF");
it.printf(36, 52, id(arimo12), id(CYAN), "%02d:%02d", id(hours_0), id(minutes_0) );
it.printf(85, 12, id(arimo12), id(RED), "%.1f°C", id(temp1_1));
it.printf(85, 22, id(arimo12), id(GREEN), "%.1f°C", id(temp2_1));
it.printf(85, 32, id(arimo12), id(BLUE), "%d", id(water_level_1));
it.printf(85, 42, id(arimo12), id(YELLOW), "%s", id(relay_state_1) ? "ON" : "OFF");
it.printf(85, 52, id(arimo12), id(CYAN), "%02d:%02d", id(hours_1), id(minutes_1) );
# Create a font to use, add and remove glyphs as needed.
font:
- file: 'fonts/Arimo-Regular.ttf'
id: arimo12
size: 12
glyphs: "<>!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzé"
- file: 'fonts/Arimo-Regular.ttf'
id: arimo24
color:
- id: RED
red: 100%
#green: 0%
#blue: 0%
- id: GREEN
#red: 0%
green: 100%
#blue: 0%
- id: BLUE
#red: 0%
#green: 0%
blue: 100%
- id: WHITE
red: 100%
green: 100%
blue: 100%
- id: BLACK
red: 0%
green: 0%
blue: 0%
- id: CYAN
#red: 0%
green: 100%
blue: 100%
- id: YELLOW
red: 100%
green: 100%
#blue: 0%
- id: PINK
red_int: 225
green_int: 105
blue_int: 180
- id: color_text
red: 100%
#green: 0%
#blue: 0%
globals: # Definition of the global variables
# Message Data Structure
# Field Type Bytes Range Notes Description
# Originator uint8_t 0 0-255 0 = Base, 1 = Station 1, 2 = Station 2 Originating Location
# Temp 1 int16_t 1-2 -20 to 100 Signed integer (scaled to 0.1°C) Outside Temperature
# Temp 2 int8_t 3-4 -20 to 100 Signed integer (scaled to 0.1°C) Water Temperature
# Water Level uint8_t 5 1-4 Single byte Level of water in the tank
# Relay State bool 6 0 or 1 Boolean flag Indicate that the pump is on/off
# Hours uint8_t 7 0-23 Single byte Hours
# Minutes uint8_t 8 0-59 Single byte Minutes
# Seconds uint8_t 9 0-59 Single byte Seconds
- id: originator_0 # MSG Data Structure. Originator of the message 0: Cabane; 1: Station Pompage 1; 2: Station Pompage 2
type: int
restore_value: no
initial_value: "0"
- id: temp1_0 # MSG Data Structure.Outside temperature
type: float
restore_value: no
initial_value: "0"
- id: temp2_0 # MSG Data Structure. Water Temperature in the tank
type: float
restore_value: no
initial_value: "0"
- id: water_level_0 # MSG Data Structure. Water level in the tank
type: int
restore_value: no
initial_value: "2"
- id: relay_state_0 # MSG Data Structure. Will be use to trigger the water pump
type: bool
restore_value: no
initial_value: "true"
- id: hours_0 # MSG Data Structure. Hour the message was send
type: int
restore_value: no
initial_value: "15"
- id: minutes_0 # MSG Data Structure. Minute the message was send
type: int
restore_value: no
initial_value: "20"
- id: seconds_0 # MSG Data Structure. Second the message was send
type: int
restore_value: no
initial_value: "10"
- id: originator_1 # MSG Data Structure. Originator of the message 0: Cabane; 1: Station Pompage 1; 2: Station Pompage 2
type: int
restore_value: no
initial_value: "1"
- id: temp1_1 # MSG Data Structure.Outside temperature
type: float
restore_value: no
initial_value: "-15.9"
- id: temp2_1 # MSG Data Structure. Water Temperature in the tank
type: float
restore_value: no
initial_value: "32.0"
- id: water_level_1 # MSG Data Structure. Water level in the tank
type: int
restore_value: no
initial_value: "3"
- id: relay_state_1 # MSG Data Structure. Will be use to trigger the water pump
type: bool
restore_value: no
initial_value: "false"
- id: hours_1 # MSG Data Structure. Hour the message was send
type: int
restore_value: no
initial_value: "55"
- id: minutes_1 # MSG Data Structure. Minute the message was send
type: int
restore_value: no
initial_value: "66"
- id: seconds_1 # MSG Data Structure. Second the message was send
type: int
restore_value: no
initial_value: "77"