كود البحث

ESP32 Project: Build a Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32 on N33

هذه الدرس جزء من: مقدمة عن WiFi LoRa

ESP32 Project: Build a Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32 on N33

Build a Secure, Off-Grid LoRa Messenger with a Custom Keyboard

In a world that relies on constant connectivity, what do you do when the Wi-Fi is down, the power is out, and cellular service is gone? This project tackles that exact problem by creating a completely self-sufficient, off-grid communication device that allows you to send and receive secure, private text messages over distances of up to 13 miles (21km) or even more.

This device is a practical tool for emergency preparedness, outdoor adventures like hiking, or simply for anyone interested in building a robust, private communication network that they control. Using a powerful Heltec WiFi LoRa 32 V3 module packed inside a durable Meshnology N33 case with a long-lasting 3000mAh battery, this project is built for real-world use. The most unique feature is its keyboard—a simple rotary encoder that provides an intuitive and reliable way to type messages anywhere, anytime.


How It Works: The Magic of LoRa and a Rotary Encoder

The project combines three key concepts to create a functional messenger:

  • LoRa Radio: LoRa (Long Range) is a radio technology that allows for communication over vast distances with very little power. Unlike Wi-Fi or cellular, it operates on license-free radio frequencies, meaning there are no SIM cards or monthly fees involved. Our device operates as a full transceiver, meaning it can both send and receive messages. When idle, it listens for incoming packets. After sending or receiving a message, it puts the radio to sleep momentarily to conserve power before returning to listening mode.
  • Rotary Encoder Keyboard: Instead of a bulky or power-hungry keyboard, we use a simple rotary encoder for text input. Rotating the knob cycles through a complete character set (A-Z, a-z, numbers, and symbols). The interface is surprisingly fast and responsive, allowing you to easily compose messages. The encoder's built-in push-button has three functions:
    • Short Press (Click): Adds the selected character to your message.
    • Long Press (Hold): Adds a space, like a spacebar.
    • Double-Click: Acts as a backspace to delete the last character.
  • Transceiver & Display Logic: The device has two main modes: "Compose" and "Read". You type your message in Compose mode. When a new message arrives, a notification appears on the screen (>> NEW MSG <<). You can then press the onboard User Button (on the Heltec board) to switch to Read mode and view the message. Another press switches you back to Compose mode to type your reply. All messages are secured using AES encryption, ensuring your conversations remain private.

Components and Parts List

To build this project, you will need the following components. As mentioned in the video, purchasing through affiliate links is a great way to support creators.

  • Heltec WiFi LoRa 32 V3 Module
  • Meshtastic N33 Case with 3000mAh Battery
  • 915MHz or region-appropriate LoRa Antenna
  • Rotary Encoder Module (5-pin with VCC, GND, CLK, DT, SW)
  • Optional (for audio alerts):
    • 5V Active Buzzer
    • 2N2222 NPN Transistor
    • 1kΩ Resistor
  • Pin Headers and Jumper Wires
  • Soldering Iron and accessories

Setting Up Your Arduino IDE

Before you can upload the code, you need to configure the Arduino IDE with the correct boards and libraries. This process is detailed in the video starting around 05:38.

Step 1: Install the ESP32 Boards

If you haven't already, you need to add support for ESP32 microcontrollers.

  1. Open the Arduino IDE, go to File > Preferences.
  2. In the "Additional boards manager URLs" field, add the following URL: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. Go to Tools > Board > Boards Manager....
  4. Search for esp32 and install the package by Espressif Systems.

Step 2: Add the Heltec ESP32 Board Support

  1. In File > Preferences, add the Heltec JSON URL on a new line in the "Additional boards manager URLs" box (the video shows how to do this at 06:27): https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/releases/download/0.0.7/package_heltec_esp32_index.json
  2. Go back to Tools > Board > Boards Manager....
  3. Search for Heltec ESP32 and install the package.

Step 3: Install Required Libraries from Library Manager

Using the Arduino Library Manager (Sketch > Include Library > Manage Libraries...), search for and install the following libraries:

  1. Ai Esp32 Rotary Encoder: The specific library used for our responsive keyboard.
  2. Heltec ESP32 Dev-Boards: The official library for the Heltec hardware.
  3. Adafruit GFX Library: A dependency for the OLED display.

Step 4: Install the Robojax Helper Library from .ZIP

As explained in the video at 18:53, a custom helper library is used for this project. You must install it from a .zip file.

  1. Download the "Robojax HealthTech Lora 32" .zip library from the link provided on the resources page.
  2. In the Arduino IDE, click on Sketch > Include Library > Add .ZIP Library....
  3. Navigate to where you saved the downloaded .zip file, select it, and click "Open".
  4. The library will be installed and ready to use.

After installing everything, make sure you select the correct board from the menu: Tools > Board > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3.


Wiring the Components

You will need to solder pin headers to your Heltec board to connect the rotary encoder. As shown in the video (11:25), careful planning is needed to ensure the pins are accessible once the board is in its case.

Helte_Wifi_LoRA_Rotary_Encoder

Diagram 1: Core Messenger (Rotary Encoder Only)

Connect the 5 pins from the rotary encoder module to the Heltec board as follows:

  • GND on encoder → GND on Heltec board
  • VCC on encoder → 3.3V on Heltec board
  • SW (Switch) → GPIO 4
  • DT (Data/Pin B) → GPIO 6
  • CLK (Clock/Pin A) → GPIO 5
Helte_Wifi_LoRA_Rotary_Encoder with buzzer

Diagram 2: Adding an Audible Alert (Buzzer Circuit)

To get an audible beep when a new message arrives, you can add a simple transistor circuit. This is necessary because the ESP32's pins cannot provide enough current to drive a buzzer directly.

  • Connect GPIO 40 from the Heltec board to one end of the 1kΩ resistor.
  • Connect the other end of the resistor to the Base pin of the 2N2222 transistor.
  • Connect the Emitter pin of the transistor to GND.
  • Connect the positive (+) leg of the 5V buzzer to the 3.3V pin on the Heltec board.
  • Connect the negative (-) leg of the buzzer to the Collector pin of the transistor.

Understanding and Customizing the Code

The provided code is ready to upload, but there are a few key settings you should be aware of so you can customize the project. These definitions are at the top of the code.


// Defines for physical pin connections
#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;

// Defines for LoRa and Security settings
#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 = "xxxBg^Tr%43232";//security key for private communication
  • ROTARY_ENCODER_...: These pins define where you connected your rotary encoder. Make sure these numbers match your wiring from the diagrams above.
  • BUZZER_PIN: This defines which pin will trigger the buzzer circuit.
  • RF_FREQUENCY: This is the LoRa operating frequency in Hertz. The value 915432000 (915 MHz) is standard for North America. You must change this value according to your region (e.g., 868MHz for Europe, 433MHz for Asia). All devices that want to communicate must use the same frequency.
  • TX_OUTPUT_POWER: This is the transmission power in dBm. It can range from 2 to 20. A lower number like 2 is perfect for short-range testing and saves a significant amount of battery. For maximum range, you can increase this value, but be aware that higher power drains the battery much faster.
  • userKey: This is your private encryption key. It can be any combination of characters. All devices must have the exact same key to decrypt each other's messages. Change this to your own unique and secret password.

Get the Code

The complete and final Arduino code for this project, as referenced in the video, is available for download on this page.

Video Chapters

  • 00:00 - Introduction
  • 00:56 - Project Outline
  • 01:29 - Components Overview
  • 05:25 - Unboxing and Hardware Assembly
  • 11:19 - Case Modification for External Wires
  • 15:06 - Wiring Guide
  • 15:38 - Arduino IDE and Library Setup
  • 20:21 - Code Settings Explained
  • 21:38 - How to Use the Rotary Encoder to Chat
  • 24:26 - Live Range Test
785-Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
اللغة: C++
/*
 * مشروع ESP32: بناء جهاز دردشة LoRa آمن وبعيد المدى Heltec WiFi LoRa 32
 * 
 * الملف: Heltec_ESP32_LoRa_V3_chat_TX_RX.ino
 * مكتوب في 29 يونيو 2025 بواسطة أحمد شمشيري www.Roboajx.com
 * يرجى مشاهدة الفيديو التعليمي على: https://youtu.be/IsOSvrF60J4
 * صفحة الموارد لهذا الفيديو: https://robojax.com/RJT642
 * 
 * =====================================================================
 * وصف كود ARDUINO: جهاز إرسال واستقبال لوحة مفاتيح LoRa آمنة (نظام دردشة)
 * =====================================================================
 * 
 * مكونات الأجهزة:
 * -------------------
 * - المتحكم الرئيسي: Heltec WiFi LoRa 32 V3
 * - العلبة: هيكل Meshnology N33 مع بطارية 3000mAh
 * - الإدخال: مشفر دوار مع زر ضغط
 * - التغذية الراجعة: شاشة OLED مدمجة
 * 
 * وظائف النظام:
 * -------------------
 * التحكمات الجديدة:
 * - قرص التدوير: اختيار الحرف.
 * - ضغطة سريعة (نقر): إضافة الحرف المختار.
 * - ضغطة طويلة (معلقة): إدراج مسافة (مثل شريط المسافة).
 * - نقرة مزدوجة: حذف.
 * 
 * [2] النقل اللاسلكي الآمن:
 * - يتم تشفير جميع الرسائل قبل إرسالها عبر LoRa
 * - يتم إرسال الأمر الخاص بالموضع المنزلي كحزمة آمنة خاصة
 * - يستخدم نطاق LoRa بتردد 433MHz/915MHZ للتواصل الموثوق
 * 
 * [3] إدارة الطاقة:
 * - مصمم للعمل بالبطارية (3000mAh)
 * - وضعيات ذات استهلاك منخفض للطاقة بين الإرسال
 * 
 * للحصول على تعليمات الإعداد الكاملة:
 * يرجى مشاهدة الفيديو التعليمي على: https://youtu.be/IsOSvrF60J4
 * صفحة الموارد لهذا الفيديو: https://robojax.com/RJT642
 * =====================================================================
 * 
 * إخلاء المسؤولية:
 * يتم تقديم هذا الكود "كما هو" دون أي ضمان من أي نوع. لا يمكن تحميل المؤلف المسؤولية عن أي أضرار تنشأ عن استخدام هذا الكود.
 * 
 * الرخصة:
 * هذه العمل مرخص تحت رخصة جنو العمومية العامة النسخة 3.0
 * قد تتوفر أذونات تتجاوز نطاق هذه الرخصة على Robojax.com
 * 
 * شروط المشاركة:
 * يمكنك مشاركة ونسخ وتعديل هذا الكود لأغراض غير تجارية شريطة أن:
 * 1. يحتفظ بهذا الكتل التعليق بالكامل مع الكود الأصلي
 * 2. تضمين الرابط الأصلي لـ Robojax.com
 * 3. الاحتفاظ برابط الفيديو التعليمي على يوتيوب (إذا كان ذلك مناسبًا)
 * 4. الإشارة بوضوح إلى أي تعديلات أجريت
 * 
 * /
 * // المكتبات الأساسية لـ Heltec وعرض الشاشة
 */
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include "WiFi.h"

 // مكتبات LoRa والأمان
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
#include <cstring>

 // مكتبة ترميز دوار المعتمدة من قبل المستخدم
#include "AiEsp32RotaryEncoder.h"

 // --- تعريفات وتكوين الدبابيس ---
static SSD1306Wire  display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);

#define ROTARY_ENCODER_A_PIN 5 // دبوس A من جهاز التشفير الدوار
#define ROTARY_ENCODER_B_PIN 6 // دبوس B من مشفر دوران
#define ROTARY_ENCODER_BUTTON_PIN 4 // مفتاح دبابيس مشفر دوار

#define RF_FREQUENCY 915432000 // تردد تشغيل لوارا
#define TX_OUTPUT_POWER 20 // يجب أن تكون قدرة الإخراج بين 2 إلى 21 في dBm.
const char *userKey = "wahJ8uy$gt~_3r"; // مفتاح الأمان للتواصل الخاص

#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4

 // --- معلمات لورا ---
#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
 // --- المتغيرات العالمية والحالة ---
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;

 // --- بروتوكولات الوظائف ---
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 IRAM_ATTR readEncoderISR() {
  rotaryEncoder.readEncoder_ISR();
}

 // --- إجراءات التعامل مع الأزرار ---
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();
}

 // --- الإعداد ---
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();
}

 // --- الحلقة الرئيسية ---
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);
}

 // --- معالجات أحداث لورا والمساعدات ---
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 = "";
  }
}

 // --- منطق العرض والأزرار ---
void updateDisplay() {
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);

  if (currentState == COMPOSING) {
    selectedChar = charset[rotaryEncoder.readEncoder()];
    display.setFont(ArialMT_Plain_10);

 // --- هذا هو الحل للإشعار ---
    String msgLabel;
    if (hasUnreadMessage) {
        msgLabel = ">> NEW MSG <<"; // اجعل الإشعار واضحًا جدًا
    } 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 == القراءة
    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 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: بناء جهاز دردشة LoRa آمن وبعيد المدى Heltec WiFi LoRa 32
 * 
 * الملف: Heltec_ESP32_LoRa_V3_chat_TX_RX_buzzer.ino
 * نُشر في ٢٩ يونيو ٢٠٢٥ بواسطة أحمد شمشيري www.Roboajx.com
 * يرجى مشاهدة الفيديو التعليمي على الرابط: https://youtu.be/IsOSvrF60J4
 * صفحة الموارد لهذا الفيديو: https://robojax.com/RJT642
 * 
 * =================================================================================
 * وصف كود ARDUINO: جهاز إرسال واستقبال لوحة مفاتيح LoRa آمن (دردشة) النظام)
 * ========================================================================
 * 
 * مكونات الجهاز:
 * ------------------
 * - وحدة التحكم الرئيسية: Heltec WiFi LoRa 32 V3
 * - العلبة: Meshnology N33 مع بطارية 3000 مللي أمبير/ساعة
 * - المدخل: مشفر دوار مع زر ضغط
 * - مسموع: جرس تنبيه نشط (متصل بالدبوس 40)
 * - التغذية الراجعة: شاشة OLED مدمجة
 * 
 * وظائف النظام:
 * ------------------
 * عناصر تحكم جديدة:
 * - مقبض التدوير: تحديد الحرف.
 * - الضغط القصير (النقر): إضافة الحرف المحدد.
 * - الضغط المطول (الضغط المستمر): إدخال مسافة (مثل مفتاح المسافة).
 * - النقر المزدوج: زر مسافة للخلف.
 * 
 * [2] إرسال لاسلكي آمن:
 * - جميع الرسائل مشفرة قبل إرسال LoRa.
 * - يتم إرسال أمر تحديد الموقع الرئيسي كحزمة آمنة خاصة.
 * - يستخدم نطاق LoRa بتردد 433 ميجاهرتز/915 ميجاهرتز لاتصال موثوق.
 * 
 * [3] إدارة الطاقة:
 * - مُحسّن لتشغيل البطارية (3000 مللي أمبير/ساعة).
 * - أوضاع طاقة منخفضة بين عمليات الإرسال.
 * 
 * للاطلاع على تعليمات الإعداد الكاملة:
 * يرجى مشاهدة الفيديو التعليمي على الرابط: https://youtu.be/IsOSvrF60J4
 * صفحة الموارد لهذا الفيديو: https://robojax.com/RJT642
 * ============================================================================
 * 
 * إخلاء المسؤولية:
 * هذا الرمز مُقدم "كما هو" دون أي ضمان من أي نوع. لا يتحمل المؤلف مسؤولية أي أضرار ناجمة عن استخدام هذا الكود.
 * 
 * الترخيص:
 * هذا العمل مرخص بموجب رخصة جنو العمومية الإصدار 3.0. قد تتوفر أذونات خارج نطاق هذه الرخصة على موقع Robojax.com.
 * 
 * شروط المشاركة:
 * لك مطلق الحرية في مشاركة هذا الكود ونسخه وتعديله لأغراض غير تجارية.
 * بشرط:
 * 1. الاحتفاظ بكامل خانة التعليقات هذه مع الكود الأصلي.
 * 2. تضمين رابط Robojax.com الأصلي.
 * 3. الاحتفاظ برابط فيديو يوتيوب التعليمي (إن وجد).
 * 4. الإشارة بوضوح إلى أي تعديلات تم إجراؤها.
 * 
 * /
 * 
 * // مكتبات Heltec الأساسية والعرض
 */
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include "WiFi.h"

 // LoRa ومكتبات الأمان
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
#include <cstring>

 // مكتبة Rotary Encoder المعتمدة من قبل المستخدم
#include "AiEsp32RotaryEncoder.h"

 // --- تعريفات الدبوس وتكوينه ---
static SSD1306Wire  display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);

#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;

#define RF_FREQUENCY 915432000 // تردد تشغيل LoRa
#define TX_OUTPUT_POWER 2 // يجب أن تكون طاقة الخرج ما بين 2 إلى 21 ديسيبل ميلي واط.
const char *userKey = "wahJ8uy$gt~_3r"; // مفتاح الأمان للاتصالات الخاصة

#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4

 // --- معلمات لورا ---
#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
 // متغيرات حالة الجرس
unsigned long buzzerStartTime = 0;
bool isBuzzerActive = false;
int beepPhase = 0; // 0=خامل، 1=إشارة صوتية أولى، 2=توقف مؤقت، 3=إشارة صوتية ثانية

#define USER_BUTTON_PIN 0
 // --- المتغيرات العالمية والحالة ---
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;

 // --- نماذج أولية للوظائف ---
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();


 // --- إجراءات مقاطعة الخدمة ---
void IRAM_ATTR readEncoderISR() {
  rotaryEncoder.readEncoder_ISR();
}

 // --- إجراءات معالج الزر ---
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();
}

 // --- يثبت ---
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();
}

 // --- الحلقة الرئيسية ---
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(); // تحديث حالة الجرس (غير حظر)
  delay(LOOP_DELAY); // تأخير الحلقة 10 مللي ثانية
}

 // --- LORA EVENT SHANDLERS & 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 = "";
  }
}

 // --- منطق العرض والأزرار ---
void updateDisplay() {
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);

  if (currentState == COMPOSING) {
    selectedChar = charset[rotaryEncoder.readEncoder()];
    display.setFont(ArialMT_Plain_10);

 // --- هذا هو الإصلاح للإشعار ---
    String msgLabel;
    if (hasUnreadMessage) {
        msgLabel = ">> NEW MSG <<"; // جعل الإشعار واضحًا جدًا
    } 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 { // الحالة الحالية == القراءة
    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(); // بدء تسلسل الجرس
    beepPhase = 1; // أول صوت صفير
    digitalWrite(BUZZER_PIN, HIGH);
  }
}
void updateBuzzer() {
  if (!isBuzzerActive) return;

  unsigned long currentTime = millis();
  unsigned long elapsed = currentTime - buzzerStartTime;

  switch (beepPhase) {
    case 1: // أول صوت تنبيه (300 مللي ثانية تشغيل)
      if (elapsed >= 300) {
        digitalWrite(BUZZER_PIN, LOW);
        buzzerStartTime = currentTime;
        beepPhase = 2; // التحرك للإيقاف المؤقت
      }
      break;

    case 2: // إيقاف مؤقت (إيقاف التشغيل لمدة 200 مللي ثانية)
      if (elapsed >= 200) {
        digitalWrite(BUZZER_PIN, HIGH);
        buzzerStartTime = currentTime;
        beepPhase = 3; // انتقل إلى الصفير الثاني
      }
      break;

    case 3: // الصفارة الثانية (300 مللي ثانية تشغيل)
      if (elapsed >= 300) {
        digitalWrite(BUZZER_PIN, LOW);
        isBuzzerActive = false; // إعادة ضبط
        beepPhase = 0;
      }
      break;
  }
}
 // --- المرافق ---
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 Files

الموارد والمراجع

ملفات📁

ملفات أخرى