ኮድ ይፈልጉ

ESP32 ፕሮጀክት፡ ደህንነቱ የተጠበቀ፣ ረጅም ርቀት ያለው LoRa ቻት መሣሪያ ይገንቡ Heltec WiFi LoRa 32 በN33 ላይ

ይህ ትምህርት ከ: የ WiFi LoRa መግቢያ

ESP32 ፕሮጀክት፡ ደህንነቱ የተጠበቀ፣ ረጅም ርቀት ያለው LoRa ቻት መሣሪያ ይገንቡ Heltec WiFi LoRa 32 በN33 ላይ

ብጁ ኪቦርድ ያለው ደህንነቱ የተጠበቀ፣ ከግሪድ ውጪ የሆነ LoRa መልእክተኛ ይገንቡ

በቋሚ ግንኙነት ላይ በሚተማመን ዓለም ውስጥ፣ ዋይ ፋይ ሲቋረጥ፣ ኤሌክትሪክ ሲጠፋ እና የሞባይል አገልግሎት ሲቀር ምን ያደርጋሉ? ይህ ፕሮጀክት ያንን ትክክለኛ ችግር የሚፈታው ሙሉ በሙሉ ራሱን የቻለ፣ ከግሪድ ውጪ የሆነ የመገናኛ መሣሪያ በመፍጠር ሲሆን ይህም እስከ 13 ማይል (21ኪሜ) ወይም ከዚያ በላይ ርቀት ላይ ደህንነቱ የተጠበቀ፣ የግል የጽሑፍ መልእክቶችን እንዲልኩ እና እንዲቀበሉ ያስችልዎታል።

ይህ መሣሪያ ለድንገተኛ አደጋ ዝግጁነት፣ እንደ እግር ጉዞ ላሉ የውጭ እንቅስቃሴዎች፣ ወይም በቀላሉ እነሱ የሚቆጣጠሩትን ጠንካራ፣ የግል የመገናኛ አውታረ መረብ ለመገንባት ለሚፈልግ ለማንኛውም ሰው ተግባራዊ መሣሪያ ነው። በጠንካራ Meshnology N33 መያዣ ውስጥ የታሸገ ኃይለኛ Heltec WiFi LoRa 32 V3 ሞጁል ከረጅም ጊዜ የሚቆይ 3000mAh ባትሪ ጋር በመጠቀም፣ ይህ ፕሮጀክት ለእውነተኛ ዓለም አገልግሎት የተገነባ ነው። በጣም ልዩ ባህሪው የቁልፍ ሰሌዳው ነው—በማንኛውም ቦታ፣ በማንኛውም ጊዜ መልእክቶችን ለመተየብ ሊታወቅ የሚችል እና አስተማማኝ መንገድ የሚሰጥ ቀላል ሮታሪ ኢንኮደር።


እንዴት እንደሚሠራ፡ የLoRa እና የሮታሪ ኢንኮደር አስማት

ፕሮጀክቱ ተግባራዊ መልእክተኛ ለመፍጠር ሦስት ቁልፍ ፅንሰ-ሐሳቦችን ያጣምራል፦

  • LoRa ራዲዮ፡ LoRa (ሎንግ ሬንጅ) በጣም ትንሽ ኃይል በመጠቀም በሰፊ ርቀቶች ላይ መገናኘት የሚያስችል የሬዲዮ ቴክኖሎጂ ነው። እንደ ዋይ ፋይ ወይም ሞባይል ሳይሆን፣ ከፈቃድ ነፃ በሆኑ የሬዲዮ ድግግሞሾች ላይ ይሠራል፣ ይህ ማለት ምንም የሲም ካርድ ወይም ወርሃዊ ክፍያ የለም ማለት ነው። መሣሪያችን እንደ ሙሉ ትራንስሴቨር ይሠራል፣ ማለትም መልእክቶችን መላክ እና መቀበል ይችላል። በስራ ፈት ጊዜ፣ የሚመጡ ፓኬቶችን ያዳምጣል። መልእክት ከላከ ወይም ከተቀበለ በኋላ፣ ወደ ማዳመጥ ሁኔታ ከመመለሱ በፊት ኃይልን ለመቆጠብ ራዲዮውን ለአፍታ ያስተኛል።
  • ሮታሪ ኢንኮደር ኪቦርድ፡ ከመጠን ያለፈ ወይም ኃይል የሚበላ ኪቦርድ ከመጠቀም ይልቅ፣ ለጽሑፍ ግብዓት ቀላል ሮታሪ ኢንኮደር እንጠቀማለን። እንደገባው ማዞር ሙሉ የቁምፊ ስብስብ (A-Z፣ a-z፣ ቁጥሮች እና ምልክቶች) ውስጥ ያሽከረክራል። በይነገጹ በሚገርም ሁኔታ ፈጣን እና ምላሽ ሰጪ ነው፣ መልእክቶችን በቀላሉ እንዲያዘጋጁ ያስችልዎታል። የኢንኮደሩ አብሮ የተሰራ የግፋ አዝራር ሦስት ተግባራት አሉት፦
    • አጭር መጫን (ጠቅታ)፡ የተመረጠውን ቁምፊ ወደ መልእክትዎ ይጨምራል።
    • ረጅም መጫን (መያዝ)፡ እንደ ክፍተት ባር ቦታ ይጨምራል።
    • ድርብ ጠቅታ፡ የመጨረሻውን ቁምፊ ለመሰረዝ እንደ ባክስፔስ ይሠራል።
  • ትራንስሴቨር እና ማሳያ አመክንዮ፡ መሣሪያው ሁለት ዋና ሁነታዎች አሉት፦ "አዘጋጅ" እና "አንብብ"። በአዘጋጅ ሁነታ መልእክትዎን ይተይባሉ። አዲስ መልእክት ሲደርስ፣ በማያ ገጹ ላይ ማሳወቂያ ይታያል (>> አዲስ መልእክት <<)። ከዚያ ወደ አንብብ ሁነታ ለመቀየር እና መልእክቱን ለማየት በቦርዱ ላይ ያለውን የተጠቃሚ አዝራር (በHeltec ቦርድ ላይ) መጫን ይችላሉ። ሌላ መጫን መልስዎን ለመተየብ ወደ አዘጋጅ ሁነታ ይመልስዎታል። ሁሉም መልእክቶች የ AES ምስጠራን በመጠቀም የተጠበቁ ናቸው፣ ውይይቶችዎ የግል መሆናቸውን ያረጋግጣሉ።

ክፍሎች እና የእቃ ዝርዝር

ይህን ፕሮጀክት ለመገንባት የሚከተሉትን ክፍሎች ያስፈልግዎታል። በቪዲዮው ላይ እንደተጠቀሰው፣ በተባባሪ አገናኞች መግዛት ፈጣሪዎችን ለመደገፍ ጥሩ መንገድ ነው።

  • Heltec WiFi LoRa 32 V3 ሞጁል
  • Meshtastic N33 መያዣ ከ3000mAh ባትሪ ጋር
  • 915MHz ወይም ለክልሉ ተስማሚ የሆነ LoRa አንቴና
  • ሮታሪ ኢንኮደር ሞጁል (5-ፒን ከVCC፣ GND፣ CLK፣ DT፣ SW ጋር)
  • አማራጭ (ለድምጽ ማንቂያ)፦
    • 5V ንቁ ባዘር
    • 2N2222 NPN ትራንዚስተር
    • 1kΩ ሬዚስተር
  • ፒን ሄደር እና ጃምፐር ሽቦዎች
  • ብረት መሸጫ እና መለዋወጫዎች

የአርዱኖ IDE እርስዎን ማዋቀር

ኮዱን ከመጫንዎ በፊት፣ የአርዱኖ IDE ን በትክክለኛ ቦርዶች እና ቤተ-መጻሕፍት ማዋቀር ያስፈልግዎታል። ይህ ሂደት በቪዲዮው ውስጥ ከ05፡38 አካባቢ በዝርዝር ተብራርቷል።

ደረጃ 1፡ የESP32 ቦርዶችን ይጫኑ

ካላደረጉት በስተቀር፣ ለESP32 ማይክሮ መቆጣጠሪያዎች ድጋፍ መጨመር ያስፈልግዎታል።

  1. የአርዱኖ IDE ን ይክፈቱ፣ ወደ ፋይል > ምርጫዎች ይሂዱ።
  2. በ"ተጨማሪ ቦርዶች አስተዳዳሪ URLs" መስክ ውስጥ፣ የሚከተለውን URL ያክሉ፦ https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. ወደ መሣሪያዎች > ቦርድ > የቦርዶች አስተዳዳሪ... ይሂዱ።
  4. esp32 ን ይፈልጉ እና በEspressif Systems የተሰራውን ጥቅል ይጫኑ።

ደረጃ 2፡ የHeltec ESP32 ቦርድ ድጋፍን ያክሉ

  1. ፋይል > ምርጫዎች ውስጥ፣ የHeltec JSON URL ን በ"ተጨማሪ ቦርዶች አስተዳዳሪ URLs" ሳጥን ውስጥ በአዲስ መስመር ላይ ያክሉ (ቪዲዮው ይህን በ06፡27 እንዴት እንደሚያደርጉ ያሳያል)፦ https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/releases/download/0.0.7/package_heltec_esp32_index.json
  2. ወደ መሣሪያዎች > ቦርድ > የቦርዶች አስተዳዳሪ... ይመለሱ።
  3. Heltec ESP32 ን ይፈልጉ እና ጥቅሉን ይጫኑ።

ደረጃ 3፡ ከቤተ-መጻሕፍት አስተዳዳሪ የሚያስፈልጉትን ቤተ-መጻሕፍት ይጫኑ

የአርዱኖ ቤተ-መጻሕፍት አስተዳዳሪን በመጠቀም (ስኬች > ቤተ-መጻሕፍትን ያካትቱ > ቤተ-መጻሕፍትን ያስተዳድሩ...)፣ የሚከተሉትን ቤተ-መጻሕፍት ይፈልጉ እና ይጫኑ፦

  1. Ai Esp32 Rotary Encoder፡ ለምላሽ ሰጪ ኪቦርዳችን የተጠቀምንበት ልዩ ቤተ-መጻሕፍት።
  2. Heltec ESP32 Dev-Boards፡ ለHeltec ሃርድዌር ይፋዊ ቤተ-መጻሕፍት።
  3. Adafruit GFX Library፡ ለOLED ማሳያ ጥገኛ።

ደረጃ 4፡ የRobojax Helper ቤተ-መጻሕፍትን ከ.ZIP ይጫኑ

በቪዲዮው ላይ በ18፡53 እንደተብራራው፣ ለዚህ ፕሮጀክት ብጁ ረዳት ቤተ-መጻሕፍት ጥቅም ላይ ይውላል። ከ.zip ፋይል መጫን አለብዎት።

  1. ከመርጃ ገጹ ላይ ከተሰጠው አገናኝ የ"Robojax HealthTech Lora 32" .zip ቤተ-መጻሕፍትን ያውርዱ።
  2. በአርዱኖ IDE ውስጥ፣ ስኬች > ቤተ-መጻሕፍትን ያካትቱ > የ.ZIP ቤተ-መጻሕፍትን ያክሉ... ላይ ጠቅ ያድርጉ።
  3. ያወረዱትን .zip ፋይል ወደሚገኝበት ቦታ ይሂዱ፣ ይምረጡት እና "ክፈት" ን ጠቅ ያድርጉ።
  4. ቤተ-መጻሕፍቱ ይጫናል እና ለመጠቀም ዝግጁ ይሆናል።

ሁሉንም ነገር ከጫኑ በኋላ፣ ከምናሌው ውስጥ ትክክለኛውን ቦርድ መምረጥዎን ያረጋግጡ፡ Tools > Board > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3


ክፍሎቹን ማገናኘት

የ rotary encoder ን ለማገናኘት በ Heltec ቦርድዎ ላይ የፒን ራሶችን መሸጥ ያስፈልግዎታል። በቪዲዮው ላይ እንደሚታየው (11፡25)፣ ቦርዱ በመያዣው ውስጥ ከገባ በኋላ ፒኖቹ ተደራሽ መሆናቸውን ለማረጋገጥ ጥንቃቄ የተሞላበት እቅድ ማውጣት ያስፈልጋል።

Helte_Wifi_LoRA_Rotary_Encoder

ሥዕላዊ መግለጫ 1፡ ዋና መልእክተኛ (Rotary Encoder ብቻ)

ከ rotary encoder ሞጁል የሚመጡትን 5 ፒኖች ከ Heltec ቦርድ ጋር እንደሚከተለው ያገናኙ፡

  • በ encoder ላይ ያለው GND → በ Heltec ቦርድ ላይ ያለው GND
  • በ encoder ላይ ያለው VCC → በ Heltec ቦርድ ላይ ያለው 3.3V
  • SW (መቀየሪያ) → GPIO 4
  • DT (ዳታ/ፒን B) → GPIO 6
  • CLK (ሰዓት/ፒን A) → GPIO 5
Helte_Wifi_LoRA_Rotary_Encoder with buzzer

ሥዕላዊ መግለጫ 2፡ የድምጽ ማንቂያ መጨመር (Buzzer ወረዳ)

አዲስ መልእክት ሲደርስ የድምጽ ምልክት ለማግኘት፣ ቀላል የ transistor ወረዳ መጨመር ይችላሉ። ይህ አስፈላጊ ነው ምክንያቱም የ ESP32 ፒኖች buzzer ን በቀጥታ ለማስኬድ የሚያስችል በቂ የኤሌክትሪክ ጅረት ማቅረብ ስለማይችሉ ነው።

  • ከ Heltec ቦርድ ላይ ያለውን GPIO 401kΩ resistor አንድ ጫፍ ጋር ያገናኙ።
  • የ resistor ን ሌላኛውን ጫፍ ከ 2N2222 transistorBase ፒን ጋር ያገናኙ።
  • የ transistor ን Emitter ፒን ከ GND ጋር ያገናኙ።
  • የ 5V buzzer ን አዎንታዊ (+) እግር በ Heltec ቦርድ ላይ ካለው 3.3V ፒን ጋር ያገናኙ።
  • የ buzzer ን አሉታዊ (-) እግር ከ transistor የ Collector ፒን ጋር ያገናኙ።

ኮዱን መረዳት እና ማበጀት

የቀረበው ኮድ ለመጫን ዝግጁ ነው፣ ነገር ግን ፕሮጀክቱን ማበጀት እንዲችሉ ሊያውቋቸው የሚገቡ ጥቂት ቁልፍ ቅንብሮች አሉ። እነዚህ ትርጓሜዎች በኮዱ መጀመሪያ ላይ ይገኛሉ።


// ለአካላዊ ፒን ግንኙነቶች ትርጓሜዎች
#define ROTARY_ENCODER_A_PIN 5 //የ rotary encoder ፒን A
#define ROTARY_ENCODER_B_PIN 6 //የ rotary encoder ፒን B
#define ROTARY_ENCODER_BUTTON_PIN 4 //የ rotary encoder መቀየሪያ ፒን
#define BUZZER_PIN 40 //የ buzzer ፒን ወይም የሚቀሰቀስ relay
const bool DEBUG = false;

// ለ LoRa እና ደህንነት ቅንብሮች ትርጓሜዎች
#define RF_FREQUENCY 915432000 //የ LoRa የስራ ድግግሞሽ
#define TX_OUTPUT_POWER 2 //የውጤት ኃይል በ dBm ከ 2 እስከ 21 መሆን አለበት።
const char *userKey = "xxxBg^Tr%43232";//ለግል ግንኙነት የደህንነት ቁልፍ
  • ROTARY_ENCODER_...፡ እነዚህ ፒኖች rotary encoder ዎን የት እንዳገናኙ ይገልጻሉ። እነዚህ ቁጥሮች ከላይ ካሉት ሥዕላዊ መግለጫዎች ጋር የሚስማሙ መሆናቸውን ያረጋግጡ።
  • BUZZER_PIN፡ ይህ የ buzzer ወረዳን የሚያነቃቃውን ፒን ይገልጻል።
  • RF_FREQUENCY፡ ይህ የ LoRa የስራ ድግግሞሽ በ Hertz ነው። እሴቱ 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 - ለመወያየት Rotary Encoder ን እንዴት መጠቀም እንደሚቻል
  • 24፡26 - የቀጥታ ስርጭት የርቀት ሙከራ
785-Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
ቋንቋ: 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
ቋንቋ: 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);
}

ምንጮች እና ምንጮች

ፋይሎች📁

ሌላ ፋይሎች