Mradi wa ESP32: Tengeneza Kifaa cha Mazungumzo cha LoRa Salama, cha Masafa Marefu Heltec WiFi LoRa 32 kwenye N33
Jenga Kifaa Salama cha LoRa Messenger cha Nje ya Gridi chenye Kibodi Maalum
Katika ulimwengu unaotegemea muunganisho wa mara kwa mara, unafanya nini wakati Wi-Fi imekatika, umeme umekatika, na huduma za simu zimepotea? Mradi huu unashughulikia tatizo hilo hasa kwa kuunda kifaa cha mawasiliano cha kujitosheleza kabisa, nje ya gridi, kinachokuwezesha kutuma na kupokea ujumbe wa maandishi salama na wa faragha kwa umbali wa hadi maili 13 (km 21) au hata zaidi.
Kifaa hiki ni chombo cha vitendo kwa ajili ya maandalizi ya dharura, shughuli za nje kama kupanda milima, au kwa mtu yeyote anayevutiwa na kujenga mtandao thabiti wa mawasiliano wa faragha ambao wao hudhibiti. Kwa kutumia moduli yenye nguvu ya Heltec WiFi LoRa 32 V3 iliyowekwa ndani ya kipochi cha kudumu cha Meshnology N33 chenye betri ya kudumu ya 3000mAh, mradi huu umejengwa kwa matumizi ya ulimwengu halisi. Kipengele cha kipekee zaidi ni kibodi chake—encoder rahisi ya mzunguko inayotoa njia angavu na ya kuaminika ya kuandika ujumbe popote, wakati wowote.
Jinsi Inavyofanya Kazi: Uchawi wa LoRa na Encoder ya Mzunguko
Mradi huu unachanganya dhana tatu kuu kuunda kifaa cha ujumbe kinachofanya kazi:
- Redio ya LoRa: LoRa (Masafa Marefu) ni teknolojia ya redio inayoruhusu mawasiliano kwa umbali mkubwa kwa nishati ndogo sana. Tofauti na Wi-Fi au simu, inafanya kazi kwenye masafa ya redio yasiyohitaji leseni, kumaanisha hakuna SIM kadi au ada za kila mwezi. Kifaa chetu kinafanya kazi kama transceiver kamili, kumaanisha kinaweza kutuma na kupokea ujumbe. Kinapokuwa kimelala, kinasikiliza pakiti zinazoingia. Baada ya kutuma au kupokea ujumbe, kinalala kwa muda mfupi ili kuokoa nishati kabla ya kurudi kwenye hali ya kusikiliza.
- Kibodi ya Encoder ya Mzunguko: Badala ya kibodi kubwa au inayotumia nishati nyingi, tunatumia encoder rahisi ya mzunguko kwa kuingiza maandishi. Kuzungusha knob kunapitia seti kamili ya herufi (A-Z, a-z, nambari, na alama). Kiolesura ni cha haraka na chenye mwitikio wa ajabu, kinachokuruhusu kutunga ujumbe kwa urahisi. Kitufe cha kujengewa ndani cha encoder kina kazi tatu:
- Kushinikiza Kwa Ufupi (Click): Kinaongeza herufi iliyochaguliwa kwenye ujumbe wako.
- Kushinikiza Kwa Muda Mrefu (Hold): Kinaongeza nafasi, kama spacebar.
- Kubofya Mara Mbili: Hufanya kama backspace kufuta herufi ya mwisho.
- Transceiver na Mantiki ya Onyesho: Kifaa kina njia kuu mbili: "Compose" (Kutunga) na "Read" (Kusoma). Unaandika ujumbe wako katika hali ya Kutunga. Ujumbe mpya unapofika, arifa inaonekana kwenye skrini (
>> NEW MSG <<). Kisha unaweza kubonyeza Kitufe cha Mtumiaji cha ndani (kwenye bodi ya Heltec) kubadili hali ya Kusoma na kuona ujumbe. Kubonyeza kingine kunakurudisha kwenye hali ya Kutunga kuandika jibu lako. Ujumbe wote unalindwa kwa kutumia usimbaji wa AES, kuhakikisha mazungumzo yako yanabaki ya faragha.
Vipengele na Orodha ya Sehemu
Ili kujenga mradi huu, utahitaji vipengele vifuatavyo. Kama ilivyotajwa kwenye video, kununua kupitia viungo vya affiliate ni njia nzuri ya kusaidia waundaji.
- Moduli ya Heltec WiFi LoRa 32 V3
- Kipochi cha Meshtastic N33 chenye Betri ya 3000mAh
- Antena ya LoRa ya 915MHz au inayofaa eneo lako
- Moduli ya Encoder ya Mzunguko (5-pini yenye VCC, GND, CLK, DT, SW)
- Hiari (kwa arifa za sauti):
- Buzzer Amilifu ya 5V
- Transistor ya 2N2222 NPN
- Kipingamizi cha 1kΩ
- Vichwa vya Pini na Waya za Jumper
- Chuma cha Solder na vifaa vyake
Kusanidi Arduino IDE Yako
Kabla ya kupakia msimbo, unahitaji kusanidi Arduino IDE na bodi na maktaba sahihi. Mchakato huu umeelezewa kwa kina kwenye video kuanzia dakika ya 05:38.
Hatua ya 1: Sakinisha Bodi za ESP32
Ikiwa hujafanya hivyo, unahitaji kuongeza msaada wa microcontrollers za ESP32.
- Fungua Arduino IDE, nenda kwenye
File > Preferences. - Katika sehemu ya "Additional boards manager URLs", ongeza URL ifuatayo:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - Nenda kwenye
Tools > Board > Boards Manager.... - Tafuta
esp32na usakinishe pakiti ya Espressif Systems.
Hatua ya 2: Ongeza Msaada wa Bodi ya Heltec ESP32
- Katika
File > Preferences, ongeza URL ya JSON ya Heltec kwenye mstari mpya katika kisanduku cha "Additional boards manager URLs" (video inaonyesha jinsi ya kufanya hivi saa 06:27):https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/releases/download/0.0.7/package_heltec_esp32_index.json - Rudi kwenye
Tools > Board > Boards Manager.... - Tafuta
Heltec ESP32na usakinishe pakiti.
Hatua ya 3: Sakinisha Maktaba Zinazohitajika kutoka kwa Meneja wa Maktaba
Kwa kutumia Meneja wa Maktaba wa Arduino (Sketch > Include Library > Manage Libraries...), tafuta na usakinishe maktaba zifuatazo:
- Ai Esp32 Rotary Encoder: Maktaba mahususi inayotumika kwa kibodi chetu chenye mwitikio.
- Heltec ESP32 Dev-Boards: Maktaba rasmi ya vifaa vya Heltec.
- Adafruit GFX Library: Tegemezi la onyesho la OLED.
Hatua ya 4: Sakinisha Maktaba ya Msaidizi ya Robojax kutoka .ZIP
Kama ilivyoelezwa kwenye video saa 18:53, maktaba maalum ya msaidizi inatumika kwa mradi huu. Lazima usakinishe kutoka kwenye faili ya .zip.
- Pakua maktaba ya .zip ya "Robojax HealthTech Lora 32" kutoka kiungo kilichotolewa kwenye ukurasa wa rasilimali.
- Katika Arduino IDE, bofya
Sketch > Include Library > Add .ZIP Library.... - Nenda mahali ulipohifadhi faili ya .zip iliyopakuliwa, ichague, na ubofye "Open".
- Maktaba itasakinishwa na kuwa tayari kutumika.
Tools > Board > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3.
Kuunganisha Vipengele
Utahitaji kuuza pini za kichwa kwenye bodi yako ya Heltec ili kuunganisha kipima-mzunguko (rotary encoder). Kama inavyoonyeshwa kwenye video (11:25), upangaji makini unahitajika ili kuhakikisha pini zinapatikana wakati bodi iko kwenye kifuniko chake.

Mchoro 1: Core Messenger (Kipima-Mzunguko Pekee)
Unganisha pini 5 kutoka kwenye moduli ya kipima-mzunguko hadi kwenye bodi ya Heltec kama ifuatavyo:
GNDkwenye kipima-mzunguko →GNDkwenye bodi ya HeltecVCCkwenye kipima-mzunguko →3.3Vkwenye bodi ya HeltecSW(Swichi) →GPIO 4DT(Data/Pini B) →GPIO 6CLK(Saa/Pini A) →GPIO 5

Mchoro 2: Kuongeza Tahadhari ya Sauti (Mzunguko wa Buzzer)
Ili kupata sauti ya beep wakati ujumbe mpya unapofika, unaweza kuongeza mzunguko rahisi wa transistor. Hii ni muhimu kwa sababu pini za ESP32 haziwezi kutoa umeme wa kutosha kuendesha buzzer moja kwa moja.
- Unganisha
GPIO 40kutoka kwenye bodi ya Heltec hadi mwisho mmoja wa kipinzani cha 1kΩ. - Unganisha mwisho mwingine wa kipinzani kwenye pini ya Base ya transistor ya 2N2222.
- Unganisha pini ya Emitter ya transistor kwenye
GND. - Unganisha mguu wa chanya (+) wa buzzer ya 5V kwenye pini ya
3.3Vkwenye bodi ya Heltec. - Unganisha mguu wa hasi (-) wa buzzer kwenye pini ya Collector ya transistor.
Kuelewa na Kubinafsisha Msimbo
Msimbo uliotolewa uko tayari kupakiwa, lakini kuna mipangilio muhimu kadhaa unayopaswa kujua ili uweze kubinafsisha mradi huu. Ufafanuzi huu upo juu ya msimbo.
// Ufafanuzi wa miunganisho ya pini halisi
#define ROTARY_ENCODER_A_PIN 5 //pini A ya kipima-mzunguko
#define ROTARY_ENCODER_B_PIN 6 //pini B ya kipima-mzunguko
#define ROTARY_ENCODER_BUTTON_PIN 4 //pini ya swichi ya kipima-mzunguko
#define BUZZER_PIN 40 //pini ya buzzer au relay inayotakiwa kuwashwa
const bool DEBUG = false;
// Ufafanuzi wa mipangilio ya LoRa na Usalama
#define RF_FREQUENCY 915432000 //masafa ya kufanya kazi ya LoRa
#define TX_OUTPUT_POWER 2 //nguvu ya kutoa inapaswa kuwa kati ya 2 hadi 21 kwa dBm.
const char *userKey = "xxxBg^Tr%43232";//ufunguo wa usalama kwa mawasiliano ya faragha
ROTARY_ENCODER_...: Pini hizi zinafafanua mahali ulipounganisha kipima-mzunguko chako. Hakikisha nambari hizi zinalingana na wiring yako kutoka kwenye michoro hapo juu.BUZZER_PIN: Hii inafafanua ni pini ipi itawasha mzunguko wa buzzer.RF_FREQUENCY: Hii ni masafa ya kufanya kazi ya LoRa kwa Hertz. Thamani 915432000 (915 MHz) ni ya kawaida kwa Amerika Kaskazini. Lazima ubadilishe thamani hii kulingana na eneo lako (kwa mfano, 868MHz kwa Ulaya, 433MHz kwa Asia). Vifaa vyote vinavyotaka kuwasiliana lazima vitumie masafa sawa.TX_OUTPUT_POWER: Hii ni nguvu ya kusambaza kwa dBm. Inaweza kuwa kati ya 2 hadi 20. Nambari ndogo kama2ni bora kwa majaribio ya umbali mfupi na inaokoa kiasi kikubwa cha betri. Kwa umbali wa juu zaidi, unaweza kuongeza thamani hii, lakini fahamu kwamba nguvu kubwa inamaliza betri haraka zaidi.userKey: Hii ni ufunguo wako wa usimbaji fiche wa faragha. Inaweza kuwa mchanganyiko wowote wa herufi. Vifaa vyote lazima viwe na ufunguo sawa kabisa ili kusimbua ujumbe wa kila kimoja. BADILISHA hii kuwa nenosiri lako la kipekee na la siri.
Pata Msimbo
Msimbo kamili na wa mwisho wa Arduino kwa mradi huu, kama unavyorejelewa kwenye video, unapatikana kwa kupakuliwa kwenye ukurasa huu.
Sura za Video
- 00:00 - Utangulizi
- 00:56 - Muhtasari wa Mradi
- 01:29 - Muhtasari wa Vipengele
- 05:25 - Kufungua na Kuunganisha Vifaa
- 11:19 - Marekebisho ya Kifuniko kwa Nyaya za Nje
- 15:06 - Mwongozo wa Wiring
- 15:38 - Usanidi wa Arduino IDE na Maktaba
- 20:21 - Mipangilio ya Msimbo Imefafanuliwa
- 21:38 - Jinsi ya Kutumia Kipima-Mzunguko Kuzungumza
- 24:26 - Jaribio la Moja kwa Moja la Masafa
/*
ESP32 Project: Build a Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
File: Heltec_ESP32_LoRa_V3_chat_TX_RX.ino
written on 29 Jun, 2025 by Ahmad Shamshiri www.Roboajx.com
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* ARDUINO CODE DESCRIPTION: SECURE LoRa Keyboard Transceiver (chat system)
* =====================================================================
*
* HARDWARE COMPONENTS:
* -------------------
* - Main Controller: Heltec WiFi LoRa 32 V3
* - Enclosure: Meshnology N33 case with 3000mAh battery
* - Input: Rotary encoder with push-button
* - Feedback: Built-in OLED display
*
* SYSTEM FUNCTIONALITY:
* -------------------
NEW CONTROLS:
- Rotate Knob: Select character.
- Short Press (Click): Add selected character.
- Long Press (Hold): Insert a space (like a spacebar).
- Double Click: Backspace.
*
* [2] SECURE WIRELESS TRANSMISSION:
* - All message encrypted before LoRa transmission
* - Home position command transmitted as special secure packet
* - Uses 433MHz/915MHZ LoRa band for reliable communication
*
* [3] POWER MANAGEMENT:
* - Optimized for battery operation (3000mAh)
* - Low-power modes between transmissions
*
* FOR COMPLETE SETUP INSTRUCTIONS:
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* DISCLAIMER:
* This code is provided "AS IS" without warranty of any kind. The author
* shall not be held liable for any damages arising from the use of this code.
*
* LICENSE:
* This work is licensed under the GNU General Public License v3.0
* Permissions beyond the scope of this license may be available at Robojax.com
*
* SHARING TERMS:
* You are free to share, copy and modify this code for non-commercial purposes
* PROVIDED you:
* 1. Keep this entire comment block intact with the original code
* 2. Include the original Robojax.com link
* 3. Keep the YouTube tutorial link (if applicable)
* 4. Clearly indicate any modifications made
*
*
* ********************************************************************
*/
// Core Heltec and display libraries
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include "WiFi.h"
// LoRa and security libraries
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
#include <cstring>
// The user-approved Rotary Encoder library
#include "AiEsp32RotaryEncoder.h"
// --- PIN DEFINITIONS & CONFIGURATION ---
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
#define ROTARY_ENCODER_A_PIN 5 //pin A of rotary encoder
#define ROTARY_ENCODER_B_PIN 6 //pin B of rotary encoder
#define ROTARY_ENCODER_BUTTON_PIN 4 //switch pin of rotary encoder
#define RF_FREQUENCY 915432000 //operating frequey of LoRa
#define TX_OUTPUT_POWER 20 //output power should be between 2 to 21 in dBm.
const char *userKey = "wahJ8uy$gt~_3r";//secureity key for private communication
#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4
// --- LORA PARAMETERS ---
#define LORA_BANDWIDTH 0
#define LORA_SPREADING_FACTOR 7
#define LORA_CODINGRATE 1
#define LORA_PREAMBLE_LENGTH 8
#define LORA_SYMBOL_TIMEOUT 0
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#define BUFFER_SIZE 256
#define USER_BUTTON_PIN 0
// --- GLOBAL VARIABLES & STATE ---
enum AppState { COMPOSING, READING };
AppState currentState = COMPOSING;
String messageToCompose = "";
String lastReceivedMessage = "No messages yet.";
bool hasUnreadMessage = false;
volatile bool newPacketAvailable = false;
char rxpacket[BUFFER_SIZE];
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
const char* charset = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?>";
char selectedChar;
static RadioEvents_t RadioEvents;
bool lora_idle = true;
mbedtls_aes_context aes;
unsigned long shortPressAfterMiliseconds = 50;
unsigned long longPressAfterMiliseconds = 1000;
unsigned long doubleClickTimeout = 400;
static unsigned long buttonDownTime = 0;
static unsigned long buttonUpTime = 0;
static bool buttonWasDown = false;
static bool doubleClickWaiting = false;
// --- FUNCTION PROTOTYPES ---
void OnTxDone(void);
void OnTxTimeout(void);
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr);
void encryptAES(uint8_t *data, const char *key);
void decryptAES(uint8_t *data, const char *key);
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize);
void updateDisplay();
void sendData();
void VextON(void);
void processButton();
// --- INTERRUPT SERVICE ROUTINES ---
void IRAM_ATTR readEncoderISR() {
rotaryEncoder.readEncoder_ISR();
}
// --- BUTTON HANDLER ACTIONS ---
void on_button_short_click() {
if (currentState != COMPOSING) return;
if (selectedChar == '>') {
sendData();
} else {
messageToCompose += selectedChar;
}
updateDisplay();
}
void on_button_long_click() {
if (currentState != COMPOSING) return;
messageToCompose += " ";
updateDisplay();
}
void on_button_double_click() {
if (currentState != COMPOSING) return;
if (messageToCompose.length() > 0) {
messageToCompose.remove(messageToCompose.length() - 1);
}
updateDisplay();
}
// --- SETUP ---
void setup() {
Serial.begin(115200);
Serial.println("LoRa Keyboard Transceiver - Definitive Version");
VextON();
delay(100);
Mcu.begin(HELTEC_BOARD,SLOW_CLK_TPYE);
WiFi.mode(WIFI_OFF);
btStop();
display.init();
display.setFont(ArialMT_Plain_10);
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, strlen(charset) - 1, true);
rotaryEncoder.disableAcceleration();
pinMode(USER_BUTTON_PIN, INPUT_PULLUP);
RadioEvents.TxDone = OnTxDone;
RadioEvents.TxTimeout = OnTxTimeout;
RadioEvents.RxDone = OnRxDone;
Radio.Init(&RadioEvents);
Radio.SetChannel(RF_FREQUENCY);
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,
0, true, 0, 0, LORA_IQ_INVERSION_ON, true);
updateDisplay();
}
// --- MAIN LOOP ---
void loop() {
if(lora_idle) {
lora_idle = false;
Serial.println("Entering RX mode...");
Radio.Rx(0);
}
Radio.IrqProcess();
if (newPacketAvailable) {
newPacketAvailable = false;
hasUnreadMessage = true;
lastReceivedMessage = String(rxpacket);
Serial.print("Received Decrypted Message: ");
Serial.println(lastReceivedMessage);
updateDisplay();
}
if (currentState == COMPOSING) {
if (rotaryEncoder.encoderChanged()) {
updateDisplay();
}
processButton();
}
if (digitalRead(USER_BUTTON_PIN) == LOW) {
delay(50);
if (digitalRead(USER_BUTTON_PIN) == LOW) {
if (currentState == COMPOSING) {
if (lastReceivedMessage != "No messages yet.") {
currentState = READING;
hasUnreadMessage = false;
updateDisplay();
}
} else {
currentState = COMPOSING;
updateDisplay();
}
while (digitalRead(USER_BUTTON_PIN) == LOW);
}
}
delay(10);
}
// --- LORA EVENT HANDLERS & HELPERS ---
void OnTxDone(void) {
Serial.println("TX done.");
Radio.Sleep();
lora_idle = true;
}
void OnTxTimeout(void) {
Serial.println("TX Timeout.");
Radio.Sleep();
lora_idle = true;
}
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr) {
Radio.Sleep();
memset(rxpacket, 0, sizeof(rxpacket));
memcpy(rxpacket, payload, size > BUFFER_SIZE ? BUFFER_SIZE : size);
decryptAES((uint8_t*)rxpacket, userKey);
newPacketAvailable = true;
lora_idle = true;
}
void sendData() {
if (messageToCompose.length() > 0) {
lora_idle = false;
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 3000);
uint8_t data[BUFFER_SIZE];
memset(data, 0, BUFFER_SIZE);
strncpy((char*)data, messageToCompose.c_str(), BUFFER_SIZE - 1);
encryptAES(data, userKey);
Serial.print("Sending message: ");
Serial.println(messageToCompose);
Radio.Send(data, 16);
messageToCompose = "";
}
}
// --- DISPLAY & BUTTON LOGIC ---
void updateDisplay() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);
if (currentState == COMPOSING) {
selectedChar = charset[rotaryEncoder.readEncoder()];
display.setFont(ArialMT_Plain_10);
// --- THIS IS THE FIX for the notification ---
String msgLabel;
if (hasUnreadMessage) {
msgLabel = ">> NEW MSG <<"; // Make the notification very obvious
} else {
msgLabel = "Msg:";
}
display.drawStringMaxWidth(0, 0, 128, msgLabel + " " + messageToCompose);
display.setFont(ArialMT_Plain_16);
int16_t labelWidth = display.getStringWidth("Select: ");
display.drawString(0, 35, "Select: ");
display.setFont(ArialMT_Plain_24);
display.drawString(labelWidth, 32, String(selectedChar));
} else { // currentState == READING
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "Received:");
display.setFont(ArialMT_Plain_10);
display.drawStringMaxWidth(0, 18, 128, lastReceivedMessage);
}
display.display();
}
void processButton() {
if (doubleClickWaiting && (millis() - buttonUpTime > doubleClickTimeout)) {
doubleClickWaiting = false;
on_button_short_click();
}
bool isButtonDown = rotaryEncoder.isEncoderButtonDown();
if (isButtonDown && !buttonWasDown) {
buttonDownTime = millis();
buttonWasDown = true;
}
else if (!isButtonDown && buttonWasDown) {
unsigned long pressDuration = millis() - buttonDownTime;
if (pressDuration >= longPressAfterMiliseconds) {
on_button_long_click();
doubleClickWaiting = false;
}
else if (pressDuration >= shortPressAfterMiliseconds) {
if (doubleClickWaiting) {
on_button_double_click();
doubleClickWaiting = false;
} else {
doubleClickWaiting = true;
buttonUpTime = millis();
}
}
buttonWasDown = false;
}
}
// --- UTILITIES ---
void VextON(void) {
pinMode(Vext,OUTPUT);
digitalWrite(Vext, LOW);
}
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize) {
memset(processedKey, 0, keySize);
size_t len = strlen(userKey);
if (len > keySize) len = keySize;
memcpy(processedKey, userKey, len);
}
void encryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, data, data);
mbedtls_aes_free(&aes);
}
void decryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, data, data);
mbedtls_aes_free(&aes);
}
/*
ESP32 Project: Build a Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
File: Heltec_ESP32_LoRa_V3_chat_TX_RX_buzzer.ino
written on 29 Jun, 2025 by Ahmad Shamshiri www.Roboajx.com
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* ARDUINO CODE DESCRIPTION: SECURE LoRa Keyboard Transceiver (chat system)
* =====================================================================
*
* HARDWARE COMPONENTS:
* -------------------
* - Main Controller: Heltec WiFi LoRa 32 V3
* - Enclosure: Meshnology N33 case with 3000mAh battery
* - Input: Rotary encoder with push-button
* - Audiable: Active buzzer (connectedt o pin 40)
* - Feedback: Built-in OLED display
*
* SYSTEM FUNCTIONALITY:
* -------------------
NEW CONTROLS:
- Rotate Knob: Select character.
- Short Press (Click): Add selected character.
- Long Press (Hold): Insert a space (like a spacebar).
- Double Click: Backspace.
*
* [2] SECURE WIRELESS TRANSMISSION:
* - All message encrypted before LoRa transmission
* - Home position command transmitted as special secure packet
* - Uses 433MHz/915MHZ LoRa band for reliable communication
*
* [3] POWER MANAGEMENT:
* - Optimized for battery operation (3000mAh)
* - Low-power modes between transmissions
*
* FOR COMPLETE SETUP INSTRUCTIONS:
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* DISCLAIMER:
* This code is provided "AS IS" without warranty of any kind. The author
* shall not be held liable for any damages arising from the use of this code.
*
* LICENSE:
* This work is licensed under the GNU General Public License v3.0
* Permissions beyond the scope of this license may be available at Robojax.com
*
* SHARING TERMS:
* You are free to share, copy and modify this code for non-commercial purposes
* PROVIDED you:
* 1. Keep this entire comment block intact with the original code
* 2. Include the original Robojax.com link
* 3. Keep the YouTube tutorial link (if applicable)
* 4. Clearly indicate any modifications made
*
*
* ********************************************************************
*/
// Core Heltec and display libraries
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include "WiFi.h"
// LoRa and security libraries
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
#include <cstring>
// The user-approved Rotary Encoder library
#include "AiEsp32RotaryEncoder.h"
// --- PIN DEFINITIONS & CONFIGURATION ---
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
#define ROTARY_ENCODER_A_PIN 5 //pin A of rotary encoder
#define ROTARY_ENCODER_B_PIN 6 //pin B of rotary encoder
#define ROTARY_ENCODER_BUTTON_PIN 4 //switch pin of rotary encoder
#define BUZZER_PIN 40 //the buzzer pin or relay to be triggered
const bool DEBUG = false;
#define RF_FREQUENCY 915432000 //operating frequency of LoRa
#define TX_OUTPUT_POWER 2 //output power should be between 2 to 21 in dBm.
const char *userKey = "wahJ8uy$gt~_3r";//secureity key for private communication
#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4
// --- LORA PARAMETERS ---
#define LORA_BANDWIDTH 0
#define LORA_SPREADING_FACTOR 7
#define LORA_CODINGRATE 1
#define LORA_PREAMBLE_LENGTH 8
#define LORA_SYMBOL_TIMEOUT 0
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#define BUFFER_SIZE 256
#define LOOP_DELAY 10
// Buzzer state variables
unsigned long buzzerStartTime = 0;
bool isBuzzerActive = false;
int beepPhase = 0; // 0=idle, 1=first beep, 2=pause, 3=second beep
#define USER_BUTTON_PIN 0
// --- GLOBAL VARIABLES & STATE ---
enum AppState { COMPOSING, READING };
AppState currentState = COMPOSING;
String messageToCompose = "";
String lastReceivedMessage = "No messages yet.";
bool hasUnreadMessage = false;
volatile bool newPacketAvailable = false;
char rxpacket[BUFFER_SIZE];
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
const char* charset = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?>";
char selectedChar;
static RadioEvents_t RadioEvents;
bool lora_idle = true;
mbedtls_aes_context aes;
unsigned long shortPressAfterMiliseconds = 50;
unsigned long longPressAfterMiliseconds = 1000;
unsigned long doubleClickTimeout = 400;
static unsigned long buttonDownTime = 0;
static unsigned long buttonUpTime = 0;
static bool buttonWasDown = false;
static bool doubleClickWaiting = false;
// --- FUNCTION PROTOTYPES ---
void OnTxDone(void);
void OnTxTimeout(void);
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr);
void encryptAES(uint8_t *data, const char *key);
void decryptAES(uint8_t *data, const char *key);
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize);
void updateDisplay();
void sendData();
void VextON(void);
void processButton();
void buzzer();
void updateBuzzer();
// --- INTERRUPT SERVICE ROUTINES ---
void IRAM_ATTR readEncoderISR() {
rotaryEncoder.readEncoder_ISR();
}
// --- BUTTON HANDLER ACTIONS ---
void on_button_short_click() {
if (currentState != COMPOSING) return;
if (selectedChar == '>') {
sendData();
} else {
messageToCompose += selectedChar;
}
updateDisplay();
}
void on_button_long_click() {
if (currentState != COMPOSING) return;
messageToCompose += " ";
updateDisplay();
}
void on_button_double_click() {
if (currentState != COMPOSING) return;
if (messageToCompose.length() > 0) {
messageToCompose.remove(messageToCompose.length() - 1);
}
updateDisplay();
}
// --- SETUP ---
void setup() {
Serial.begin(115200);
Serial.println("LoRa Keyboard Transceiver - By Robojax");
VextON();
delay(100);
Mcu.begin(HELTEC_BOARD,SLOW_CLK_TPYE);
WiFi.mode(WIFI_OFF);
btStop();
display.init();
display.setFont(ArialMT_Plain_10);
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, strlen(charset) - 1, true);
rotaryEncoder.disableAcceleration();
pinMode(USER_BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
RadioEvents.TxDone = OnTxDone;
RadioEvents.TxTimeout = OnTxTimeout;
RadioEvents.RxDone = OnRxDone;
Radio.Init(&RadioEvents);
Radio.SetChannel(RF_FREQUENCY);
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,
0, true, 0, 0, LORA_IQ_INVERSION_ON, true);
updateDisplay();
}
// --- MAIN LOOP ---
void loop() {
if(lora_idle) {
lora_idle = false;
if(DEBUG)
{
Serial.println("Entering RX mode...");
}
Radio.Rx(0);
}
Radio.IrqProcess();
if (newPacketAvailable) {
newPacketAvailable = false;
hasUnreadMessage = true;
lastReceivedMessage = String(rxpacket);
if(DEBUG)
{
Serial.print("Received Decrypted Message: ");
Serial.println(lastReceivedMessage);
}
updateDisplay();
buzzer();
}
if (currentState == COMPOSING) {
if (rotaryEncoder.encoderChanged()) {
updateDisplay();
}
processButton();
}
if (digitalRead(USER_BUTTON_PIN) == LOW) {
delay(50);
if (digitalRead(USER_BUTTON_PIN) == LOW) {
if (currentState == COMPOSING) {
if (lastReceivedMessage != "No messages yet.") {
currentState = READING;
hasUnreadMessage = false;
updateDisplay();
}
} else {
currentState = COMPOSING;
updateDisplay();
}
while (digitalRead(USER_BUTTON_PIN) == LOW);
}
}
updateBuzzer(); // Update buzzer state (non-blocking)
delay(LOOP_DELAY); // Your 10ms loop delay
}
// --- LORA EVENT HANDLERS & HELPERS ---
void OnTxDone(void) {
if(DEBUG)
{
Serial.println("TX done.");
}
Radio.Sleep();
lora_idle = true;
}
void OnTxTimeout(void) {
if(DEBUG)
{
Serial.println("TX Timeout.");
}
Radio.Sleep();
lora_idle = true;
}
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr) {
Radio.Sleep();
memset(rxpacket, 0, sizeof(rxpacket));
memcpy(rxpacket, payload, size > BUFFER_SIZE ? BUFFER_SIZE : size);
decryptAES((uint8_t*)rxpacket, userKey);
newPacketAvailable = true;
lora_idle = true;
}
void sendData() {
if (messageToCompose.length() > 0) {
lora_idle = false;
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 3000);
uint8_t data[BUFFER_SIZE];
memset(data, 0, BUFFER_SIZE);
strncpy((char*)data, messageToCompose.c_str(), BUFFER_SIZE - 1);
encryptAES(data, userKey);
if(DEBUG)
{
Serial.print("Sending message: ");
Serial.println(messageToCompose);
}
Radio.Send(data, 16);
messageToCompose = "";
}
}
// --- DISPLAY & BUTTON LOGIC ---
void updateDisplay() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);
if (currentState == COMPOSING) {
selectedChar = charset[rotaryEncoder.readEncoder()];
display.setFont(ArialMT_Plain_10);
// --- THIS IS THE FIX for the notification ---
String msgLabel;
if (hasUnreadMessage) {
msgLabel = ">> NEW MSG <<"; // Make the notification very obvious
} else {
msgLabel = "Msg:";
}
display.drawStringMaxWidth(0, 0, 128, msgLabel + " " + messageToCompose);
display.setFont(ArialMT_Plain_16);
int16_t labelWidth = display.getStringWidth("Select: ");
display.drawString(0, 35, "Select: ");
display.setFont(ArialMT_Plain_24);
display.drawString(labelWidth, 32, String(selectedChar));
} else { // currentState == READING
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "Received:");
display.setFont(ArialMT_Plain_10);
display.drawStringMaxWidth(0, 18, 128, lastReceivedMessage);
}
display.display();
}
void processButton() {
if (doubleClickWaiting && (millis() - buttonUpTime > doubleClickTimeout)) {
doubleClickWaiting = false;
on_button_short_click();
}
bool isButtonDown = rotaryEncoder.isEncoderButtonDown();
if (isButtonDown && !buttonWasDown) {
buttonDownTime = millis();
buttonWasDown = true;
}
else if (!isButtonDown && buttonWasDown) {
unsigned long pressDuration = millis() - buttonDownTime;
if (pressDuration >= longPressAfterMiliseconds) {
on_button_long_click();
doubleClickWaiting = false;
}
else if (pressDuration >= shortPressAfterMiliseconds) {
if (doubleClickWaiting) {
on_button_double_click();
doubleClickWaiting = false;
} else {
doubleClickWaiting = true;
buttonUpTime = millis();
}
}
buttonWasDown = false;
}
}
void buzzer()
{
if (!isBuzzerActive) {
isBuzzerActive = true;
buzzerStartTime = millis(); // Start buzzer sequence
beepPhase = 1; // First beep
digitalWrite(BUZZER_PIN, HIGH);
}
}
void updateBuzzer() {
if (!isBuzzerActive) return;
unsigned long currentTime = millis();
unsigned long elapsed = currentTime - buzzerStartTime;
switch (beepPhase) {
case 1: // First beep (300ms ON)
if (elapsed >= 300) {
digitalWrite(BUZZER_PIN, LOW);
buzzerStartTime = currentTime;
beepPhase = 2; // Move to pause
}
break;
case 2: // Pause (200ms OFF)
if (elapsed >= 200) {
digitalWrite(BUZZER_PIN, HIGH);
buzzerStartTime = currentTime;
beepPhase = 3; // Move to second beep
}
break;
case 3: // Second beep (300ms ON)
if (elapsed >= 300) {
digitalWrite(BUZZER_PIN, LOW);
isBuzzerActive = false; // Reset
beepPhase = 0;
}
break;
}
}
// --- UTILITIES ---
void VextON(void) {
pinMode(Vext,OUTPUT);
digitalWrite(Vext, LOW);
}
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize) {
memset(processedKey, 0, keySize);
size_t len = strlen(userKey);
if (len > keySize) len = keySize;
memcpy(processedKey, userKey, len);
}
void encryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, data, data);
mbedtls_aes_free(&aes);
}
void decryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, data, data);
mbedtls_aes_free(&aes);
}
Common Course Links
Common Course Files
Resurser och referenser
-
Extern.Purchase Wi-Fi LoRa 32 from Meshnologymeshnology.com
Filer📁
Andra filer
-
Robojax Heltec WiFi LoRa 32 Library Version 1.1.0 20250703
Robojax_HeltecLoRa32_1.1.0_20250702.zip0.09 MB -