Search Code

ESP32 પ્રોજેક્ટ: Heltec WiFi LoRa 32 પર N33 સાથે સુરક્ષિત, લાંબા-અંતરનું LoRa ચેટ ઉપકરણ બનાવો

This lesson is part of: Introduction to WiFi LoRa

ESP32 પ્રોજેક્ટ: Heltec WiFi LoRa 32 પર N33 સાથે સુરક્ષિત, લાંબા-અંતરનું LoRa ચેટ ઉપકરણ બનાવો

સુરક્ષિત, ઑફ-ગ્રીડ LoRa મેસેન્જર બનાવો કસ્ટમ કીબોર્ડ સાથે

એવી દુનિયામાં જે સતત કનેક્ટિવિટી પર નિર્ભર છે, જ્યારે Wi-Fi બંધ હોય, પાવર ગયો હોય, અને સેલ્યુલર સેવા ગેરહાજર હોય ત્યારે તમે શું કરશો? આ પ્રોજેક્ટ તે જ સમસ્યાનો સામનો કરે છે, એક સંપૂર્ણ સ્વયં-નિર્ભર, ઑફ-ગ્રીડ સંચાર ઉપકરણ બનાવીને જે તમને 13 માઇલ (21 કિમી) અથવા તેથી વધુ અંતર સુધી સુરક્ષિત, ખાનગી ટેક્સ્ટ સંદેશાઓ મોકલવા અને પ્રાપ્ત કરવાની મંજૂરી આપે છે.

આ ઉપકરણ કટોકટીની તૈયારી, હાઇકિંગ જેવા આઉટડોર સાહસો, અથવા ફક્ત એવા કોઈપણ માટે વ્યવહારુ સાધન છે જે મજબૂત, ખાનગી સંચાર નેટવર્ક બનાવવામાં રસ ધરાવે છે જે તેઓ નિયંત્રિત કરે છે. ટકાઉ Meshnology N33 કેસમાં પેક કરેલા શક્તિશાળી Heltec WiFi LoRa 32 V3 મોડ્યુલનો ઉપયોગ કરીને, લાંબા સમય સુધી ચાલતી 3000mAh બેટરી સાથે, આ પ્રોજેક્ટ વાસ્તવિક દુનિયાના ઉપયોગ માટે બનાવવામાં આવ્યો છે. સૌથી અનોખી વિશેષતા તેનું કીબોર્ડ છે—એક સરળ રોટરી એન્કોડર જે ગમે ત્યાં, ગમે ત્યારે સંદેશાઓ ટાઇપ કરવાની સાહજિક અને વિશ્વસનીય રીત પ્રદાન કરે છે.


તે કેવી રીતે કાર્ય કરે છે: LoRa અને રોટરી એન્કોડરનો જાદુ

આ પ્રોજેક્ટ કાર્યાત્મક મેસેન્જર બનાવવા માટે ત્રણ મુખ્ય ખ્યાલોને જોડે છે:

  • LoRa રેડિયો: LoRa (લોંગ રેન્જ) એક રેડિયો ટેકનોલોજી છે જે ખૂબ ઓછી શક્તિ સાથે વિશાળ અંતર પર સંચાર કરવાની મંજૂરી આપે છે. Wi-Fi અથવા સેલ્યુલરથી વિપરીત, તે લાઇસન્સ-મુક્ત રેડિયો ફ્રીક્વન્સીઝ પર કાર્ય કરે છે, એટલે કે તેમાં કોઈ SIM કાર્ડ અથવા માસિક ફી સામેલ નથી. અમારું ઉપકરણ સંપૂર્ણ ટ્રાન્સસીવર તરીકે કાર્ય કરે છે, એટલે કે તે સંદેશાઓ મોકલી અને પ્રાપ્ત બંને કરી શકે છે. જ્યારે નિષ્ક્રિય હોય, ત્યારે તે આવતા પેકેટો માટે સાંભળે છે. સંદેશો મોકલ્યા અથવા પ્રાપ્ત કર્યા પછી, તે સાંભળવાની સ્થિતિમાં પાછા આવતા પહેલા શક્તિ બચાવવા માટે રેડિયોને થોડા સમય માટે સૂવા દે છે.
  • રોટરી એન્કોડર કીબોર્ડ: ભારે અથવા વધુ શક્તિ વાપરતા કીબોર્ડને બદલે, અમે ટેક્સ્ટ ઇનપુટ માટે સરળ રોટરી એન્કોડરનો ઉપયોગ કરીએ છીએ. નોબ ફેરવવાથી સંપૂર્ણ અક્ષર સમૂહ (A-Z, a-z, સંખ્યાઓ અને પ્રતીકો) માંથી પસાર થાય છે. ઇન્ટરફેસ આશ્ચર્યજનક રીતે ઝડપી અને પ્રતિભાવશીલ છે, જે તમને સરળતાથી સંદેશાઓ લખવાની મંજૂરી આપે છે. એન્કોડરનું બિલ્ટ-ઇન પુશ-બટન ત્રણ કાર્યો ધરાવે છે:
    • ટૂંકું દબાવો (ક્લિક): પસંદ કરેલ અક્ષર તમારા સંદેશામાં ઉમેરે છે.
    • લાંબું દબાવો (હોલ્ડ): સ્પેસબારની જેમ સ્પેસ ઉમેરે છે.
    • ડબલ-ક્લિક: છેલ્લો અક્ષર કાઢી નાખવા માટે બેકસ્પેસ તરીકે કાર્ય કરે છે.
  • ટ્રાન્સસીવર અને ડિસ્પ્લે લોજિક: ઉપકરણમાં બે મુખ્ય સ્થિતિઓ છે: "કમ્પોઝ" અને "રીડ". તમે કમ્પોઝ સ્થિતિમાં તમારો સંદેશો ટાઇપ કરો છો. જ્યારે નવો સંદેશો આવે છે, ત્યારે સ્ક્રીન પર સૂચના દેખાય છે (>> NEW MSG <<). પછી તમે રીડ સ્થિતિમાં જઈને સંદેશો જોવા માટે ઓનબોર્ડ યુઝર બટન (Heltec બોર્ડ પર) દબાવી શકો છો. બીજું દબાવવાથી તમે તમારો જવાબ ટાઇપ કરવા માટે કમ્પોઝ સ્થિતિમાં પાછા જાઓ છો. બધા સંદેશાઓ AES એન્ક્રિપ્શનનો ઉપયોગ કરીને સુરક્ષિત છે, જે ખાતરી કરે છે કે તમારી વાતચીત ખાનગી રહે.

ઘટકો અને ભાગોની સૂચિ

આ પ્રોજેક્ટ બનાવવા માટે, તમારે નીચેના ઘટકોની જરૂર પડશે. વિડિઓમાં ઉલ્લેખ કર્યા મુજબ, એફિલિએટ લિંક્સ દ્વારા ખરીદી કરવી એ સર્જકોને ટેકો આપવાનો શ્રેષ્ઠ માર્ગ છે.

  • Heltec WiFi LoRa 32 V3 મોડ્યુલ
  • Meshtastic N33 કેસ 3000mAh બેટરી સાથે
  • 915MHz અથવા પ્રદેશ-યોગ્ય LoRa એન્ટેના
  • રોટરી એન્કોડર મોડ્યુલ (5-પિન VCC, GND, CLK, DT, SW સાથે)
  • વૈકલ્પિક (ઓડિયો ચેતવણીઓ માટે):
    • 5V એક્ટિવ બઝર
    • 2N2222 NPN ટ્રાન્ઝિસ્ટર
    • 1kΩ રેઝિસ્ટર
  • પિન હેડર અને જમ્પર વાયર
  • સોલ્ડરિંગ આયર્ન અને એસેસરીઝ

તમારું Arduino IDE સેટ કરવું

કોડ અપલોડ કરતા પહેલા, તમારે યોગ્ય બોર્ડ અને લાઇબ્રેરીઓ સાથે Arduino IDE ગોઠવવાની જરૂર છે. આ પ્રક્રિયા વિડિઓમાં લગભગ 05:38 થી વિગતવાર છે.

પગલું 1: ESP32 બોર્ડ ઇન્સ્ટોલ કરો

જો તમે પહેલેથી નથી કર્યું, તો તમારે ESP32 માઇક્રોકંટ્રોલર્સ માટે સપોર્ટ ઉમેરવાની જરૂર છે.

  1. Arduino IDE ખોલો, File > Preferences પર જાઓ.
  2. "Additional boards manager URLs" ફીલ્ડમાં, નીચેનું URL ઉમેરો: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. Tools > Board > Boards Manager... પર જાઓ.
  4. esp32 શોધો અને Espressif Systems દ્વારા પેકેજ ઇન્સ્ટોલ કરો.

પગલું 2: Heltec ESP32 બોર્ડ સપોર્ટ ઉમેરો

  1. File > Preferences માં, "Additional boards manager URLs" બોક્સમાં નવી લાઇન પર Heltec JSON URL ઉમેરો (વિડિઓ 06:27 પર આ કેવી રીતે કરવું તે બતાવે છે): https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/releases/download/0.0.7/package_heltec_esp32_index.json
  2. Tools > Board > Boards Manager... પર પાછા જાઓ.
  3. Heltec ESP32 શોધો અને પેકેજ ઇન્સ્ટોલ કરો.

પગલું 3: લાઇબ્રેરી મેનેજરમાંથી જરૂરી લાઇબ્રેરીઓ ઇન્સ્ટોલ કરો

Arduino લાઇબ્રેરી મેનેજરનો ઉપયોગ કરીને (Sketch > Include Library > Manage Libraries...), નીચેની લાઇબ્રેરીઓ શોધો અને ઇન્સ્ટોલ કરો:

  1. Ai Esp32 Rotary Encoder: અમારા પ્રતિભાવશીલ કીબોર્ડ માટે ઉપયોગમાં લેવાતી ચોક્કસ લાઇબ્રેરી.
  2. Heltec ESP32 Dev-Boards: Heltec હાર્ડવેર માટે સત્તાવાર લાઇબ્રેરી.
  3. Adafruit GFX Library: OLED ડિસ્પ્લે માટે નિર્ભરતા.

પગલું 4: .ZIP માંથી Robojax હેલ્પર લાઇબ્રેરી ઇન્સ્ટોલ કરો

વિડિઓમાં 18:53 પર સમજાવ્યા મુજબ, આ પ્રોજેક્ટ માટે કસ્ટમ હેલ્પર લાઇબ્રેરીનો ઉપયોગ થાય છે. તમારે તેને .zip ફાઇલમાંથી ઇન્સ્ટોલ કરવી આવશ્યક છે.

  1. સંસાધન પૃષ્ઠ પર આપેલી લિંકમાંથી "Robojax HealthTech Lora 32" .zip લાઇબ્રેરી ડાઉનલોડ કરો.
  2. Arduino IDE માં, Sketch > Include Library > Add .ZIP Library... પર ક્લિક કરો.
  3. તમે ડાઉનલોડ કરેલી .zip ફાઇલ જ્યાં સાચવી છે ત્યાં નેવિગેટ કરો, તેને પસંદ કરો અને "Open" પર ક્લિક કરો.
  4. લાઇબ્રેરી ઇન્સ્ટોલ થઈ જશે અને ઉપયોગ માટે તૈયાર થઈ જશે.

બધું ઇન્સ્ટોલ કર્યા પછી, ખાતરી કરો કે તમે મેનૂમાંથી યોગ્ય બોર્ડ પસંદ કરો છો: Tools > Board > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3.


ઘટકોને વાયરિંગ કરવું

રોટરી એન્કોડરને કનેક્ટ કરવા માટે તમારે તમારા Heltec બોર્ડ પર પિન હેડરોને સોલ્ડર કરવાની જરૂર પડશે. વિડિઓમાં બતાવ્યા પ્રમાણે (11:25), બોર્ડ તેના કેસમાં હોય ત્યારે પિન સુલભ છે તેની ખાતરી કરવા માટે કાળજીપૂર્વક આયોજન જરૂરી છે.

Helte_Wifi_LoRA_Rotary_Encoder

આકૃતિ 1: કોર મેસેન્જર (માત્ર રોટરી એન્કોડર)

રોટરી એન્કોડર મોડ્યુલના 5 પિનને Heltec બોર્ડ સાથે નીચે મુજબ કનેક્ટ કરો:

  • એન્કોડર પર GND → Heltec બોર્ડ પર GND
  • એન્કોડર પર VCC → Heltec બોર્ડ પર 3.3V
  • SW (સ્વિચ) → GPIO 4
  • DT (ડેટા/પિન B) → GPIO 6
  • CLK (ક્લોક/પિન A) → GPIO 5
Helte_Wifi_LoRA_Rotary_Encoder with buzzer

આકૃતિ 2: શ્રાવ્ય ચેતવણી ઉમેરવી (બઝર સર્કિટ)

જ્યારે નવો સંદેશ આવે ત્યારે શ્રાવ્ય બીપ મેળવવા માટે, તમે એક સરળ ટ્રાન્ઝિસ્ટર સર્કિટ ઉમેરી શકો છો. આ જરૂરી છે કારણ કે ESP32 ના પિન બઝરને સીધું ચલાવવા માટે પૂરતો કરંટ પ્રદાન કરી શકતા નથી.

  • Heltec બોર્ડમાંથી GPIO 40 ને 1kΩ રેઝિસ્ટર ના એક છેડા સાથે કનેક્ટ કરો.
  • રેઝિસ્ટરના બીજા છેડાને 2N2222 ટ્રાન્ઝિસ્ટર ના બેઝ પિન સાથે કનેક્ટ કરો.
  • ટ્રાન્ઝિસ્ટરના એમિટર પિનને GND સાથે કનેક્ટ કરો.
  • 5V બઝરના પોઝિટિવ (+) પગને Heltec બોર્ડ પરના 3.3V પિન સાથે કનેક્ટ કરો.
  • બઝરના નેગેટિવ (-) પગને ટ્રાન્ઝિસ્ટરના કલેક્ટર પિન સાથે કનેક્ટ કરો.

કોડને સમજવો અને કસ્ટમાઇઝ કરવો

પ્રદાન કરેલ કોડ અપલોડ કરવા માટે તૈયાર છે, પરંતુ કેટલીક મુખ્ય સેટિંગ્સ છે જેના વિશે તમારે જાગૃત હોવું જોઈએ જેથી તમે પ્રોજેક્ટને કસ્ટમાઇઝ કરી શકો. આ વ્યાખ્યાઓ કોડની ટોચ પર છે.


// ભૌતિક પિન કનેક્શન માટે વ્યાખ્યાઓ
#define ROTARY_ENCODER_A_PIN 5 //રોટરી એન્કોડરનો પિન A
#define ROTARY_ENCODER_B_PIN 6 //રોટરી એન્કોડરનો પિન B
#define ROTARY_ENCODER_BUTTON_PIN 4 //રોટરી એન્કોડરનો સ્વિચ પિન
#define BUZZER_PIN 40 //બઝર પિન અથવા ટ્રિગર કરવા માટે રિલે
const bool DEBUG = false;

// LoRa અને સુરક્ષા સેટિંગ્સ માટે વ્યાખ્યાઓ
#define RF_FREQUENCY 915432000 //LoRa ની ઓપરેટિંગ ફ્રીક્વન્સી
#define TX_OUTPUT_POWER 2 //આઉટપુટ પાવર 2 થી 21 dBm ની વચ્ચે હોવો જોઈએ.
const char *userKey = "xxxBg^Tr%43232";//ખાનગી સંચાર માટે સુરક્ષા કી
  • ROTARY_ENCODER_...: આ પિન વ્યાખ્યાયિત કરે છે કે તમે તમારો રોટરી એન્કોડર ક્યાં કનેક્ટ કર્યો છે. ખાતરી કરો કે આ સંખ્યાઓ ઉપરના આકૃતિઓમાં તમારા વાયરિંગ સાથે મેળ ખાય છે.
  • BUZZER_PIN: આ વ્યાખ્યાયિત કરે છે કે કયો પિન બઝર સર્કિટને ટ્રિગર કરશે.
  • RF_FREQUENCY: આ હર્ટ્ઝમાં LoRa ઓપરેટિંગ ફ્રીક્વન્સી છે. મૂલ્ય 915432000 (915 MHz) ઉત્તર અમેરિકા માટે પ્રમાણભૂત છે. તમારે તમારા પ્રદેશ અનુસાર આ મૂલ્ય બદલવું આવશ્યક છે (દા.ત., યુરોપ માટે 868MHz, એશિયા માટે 433MHz). સંચાર કરવા માંગતા તમામ ઉપકરણોએ સમાન ફ્રીક્વન્સીનો ઉપયોગ કરવો આવશ્યક છે.
  • TX_OUTPUT_POWER: આ dBm માં ટ્રાન્સમિશન પાવર છે. તે 2 થી 20 સુધી હોઈ શકે છે. 2 જેવી ઓછી સંખ્યા ટૂંકા અંતરના પરીક્ષણ માટે યોગ્ય છે અને નોંધપાત્ર પ્રમાણમાં બેટરી બચાવે છે. મહત્તમ રેન્જ માટે, તમે આ મૂલ્ય વધારી શકો છો, પરંતુ ધ્યાન રાખો કે વધુ પાવર બેટરીને વધુ ઝડપથી ખતમ કરે છે.
  • userKey: આ તમારી ખાનગી એન્ક્રિપ્શન કી છે. તે અક્ષરોનું કોઈપણ સંયોજન હોઈ શકે છે. એકબીજાના સંદેશાઓને ડિક્રિપ્ટ કરવા માટે તમામ ઉપકરણો પાસે બરાબર સમાન કી હોવી આવશ્યક છે. આને તમારા પોતાના અનન્ય અને ગુપ્ત પાસવર્ડમાં બદલો.

કોડ મેળવો

આ પ્રોજેક્ટ માટેનો સંપૂર્ણ અને અંતિમ Arduino કોડ, જેમ કે વિડિઓમાં સંદર્ભિત છે, આ પૃષ્ઠ પર ડાઉનલોડ માટે ઉપલબ્ધ છે.

વિડિઓ પ્રકરણો

  • 00:00 - પરિચય
  • 00:56 - પ્રોજેક્ટ રૂપરેખા
  • 01:29 - ઘટકોની ઝાંખી
  • 05:25 - અનબોક્સિંગ અને હાર્ડવેર એસેમ્બલી
  • 11:19 - બાહ્ય વાયર માટે કેસ ફેરફાર
  • 15:06 - વાયરિંગ માર્ગદર્શિકા
  • 15:38 - Arduino IDE અને લાઇબ્રેરી સેટઅપ
  • 20:21 - કોડ સેટિંગ્સ સમજાવ્યા
  • 21:38 - ચેટ કરવા માટે રોટરી એન્કોડરનો ઉપયોગ કેવી રીતે કરવો
  • 24:26 - લાઇવ રેન્જ ટેસ્ટ
785-Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
Language: C++
/*
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);
}
786-Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32 with buzzer
Language: C++
/*
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);
}

Resources & references

Files📁

Other files