Search Code

โครงการ ESP32: สร้างอุปกรณ์แชท LoRa ระยะไกลที่ปลอดภัย Heltec WiFi LoRa 32 บน N33

บทเรียนนี้เป็นส่วนหนึ่งของ: บทนำสู่ WiFi LoRa

โครงการ ESP32: สร้างอุปกรณ์แชท LoRa ระยะไกลที่ปลอดภัย Heltec WiFi LoRa 32 บน N33

สร้างเครื่องส่งข้อความ LoRa แบบออฟกริดที่ปลอดภัยด้วยคีย์บอร์ดแบบกำหนดเอง

ในโลกที่ต้องพึ่งพาการเชื่อมต่อตลอดเวลา คุณจะทำอย่างไรเมื่อ Wi-Fi ดับ ไฟดับ และสัญญาณโทรศัพท์มือถือหายไป? โปรเจกต์นี้จัดการกับปัญหานั้นโดยการสร้างอุปกรณ์สื่อสารแบบออฟกริดที่พึ่งพาตนเองได้อย่างสมบูรณ์ ซึ่งช่วยให้คุณส่งและรับข้อความส่วนตัวที่ปลอดภัยในระยะทางสูงสุด 13 ไมล์ (21 กิโลเมตร) หรือมากกว่านั้นได้

อุปกรณ์นี้เป็นเครื่องมือที่ใช้งานได้จริงสำหรับการเตรียมพร้อมรับมือเหตุฉุกเฉิน การผจญภัยกลางแจ้งอย่างการเดินป่า หรือสำหรับผู้ที่สนใจสร้างเครือข่ายการสื่อสารส่วนตัวที่แข็งแกร่งซึ่งคุณควบคุมได้เอง โดยใช้โมดูล Heltec WiFi LoRa 32 V3 ที่ทรงพลังซึ่งบรรจุอยู่ในเคส Meshnology N33 ที่ทนทานพร้อมแบตเตอรี่ 3000mAh ที่ใช้งานได้ยาวนาน โปรเจกต์นี้สร้างขึ้นเพื่อการใช้งานจริง จุดเด่นที่สุดคือคีย์บอร์ดของมัน—เอ็นโค้ดเดอร์แบบหมุน (rotary encoder) ธรรมดาที่ให้วิธีการพิมพ์ข้อความที่ใช้งานง่ายและเชื่อถือได้ทุกที่ทุกเวลา


工作原理: ความมหัศจรรย์ของ LoRa และ Rotary Encoder

โปรเจกต์นี้รวมแนวคิดหลักสามประการเพื่อสร้างเครื่องส่งข้อความที่ใช้งานได้:

  • วิทยุ LoRa: LoRa (Long Range) เป็นเทคโนโลยีวิทยุที่ช่วยให้การสื่อสารในระยะทางไกลโดยใช้พลังงานน้อยมาก ต่างจาก Wi-Fi หรือโทรศัพท์มือถือ มันทำงานบนคลื่นความถี่วิทยุที่ไม่ต้องมีใบอนุญาต หมายความว่าไม่ต้องใช้ซิมการ์ดหรือเสียค่าธรรมเนียมรายเดือน อุปกรณ์ของเราทำงานเป็นทรานซีฟเวอร์เต็มรูปแบบ หมายความว่าสามารถทั้งส่งและรับข้อความได้ เมื่อไม่มีการใช้งาน มันจะคอยฟังแพ็กเก็ตที่เข้ามา หลังจากส่งหรือรับข้อความแล้ว มันจะทำให้วิทยุเข้าสู่โหมดสลีปชั่วครู่เพื่อประหยัดพลังงานก่อนจะกลับมาอยู่ในโหมดฟังอีกครั้ง
  • คีย์บอร์ด Rotary Encoder: แทนที่จะใช้คีย์บอร์ดที่เทอะทะหรือกินพลังงานมาก เราใช้เอ็นโค้ดเดอร์แบบหมุนธรรมดาสำหรับการป้อนข้อความ การหมุนลูกบิดจะวนผ่านชุดตัวอักษรทั้งหมด (A-Z, a-z, ตัวเลข และสัญลักษณ์) อินเทอร์เฟซนี้รวดเร็วและตอบสนองได้ดีอย่างน่าประหลาดใจ ช่วยให้คุณเขียนข้อความได้ง่ายๆ ปุ่มกดในตัวของเอ็นโค้ดเดอร์มีสามฟังก์ชัน:
    • กดสั้น (คลิก): เพิ่มตัวอักษรที่เลือกไปยังข้อความของคุณ
    • กดค้าง (กดยาว): เพิ่มช่องว่าง เหมือนปุ่มเว้นวรรค
    • ดับเบิลคลิก: ทำหน้าที่เป็นปุ่มลบเพื่อลบตัวอักษรสุดท้าย
  • ตรรกะของทรานซีฟเวอร์และจอแสดงผล: อุปกรณ์มีสองโหมดหลัก: "เขียนข้อความ" และ "อ่าน" คุณพิมพ์ข้อความในโหมดเขียนข้อความ เมื่อมีข้อความใหม่เข้ามา การแจ้งเตือนจะปรากฏบนหน้าจอ (>> ข้อความใหม่ <<) จากนั้นคุณสามารถกดปุ่มผู้ใช้บนบอร์ด (บนบอร์ด Heltec) เพื่อสลับไปยังโหมดอ่านและดูข้อความได้ การกดอีกครั้งจะสลับคุณกลับไปยังโหมดเขียนข้อความเพื่อพิมพ์ตอบกลับ ข้อความทั้งหมดได้รับการเข้ารหัสด้วย AES เพื่อรับประกันว่าบทสนทนาของคุณยังคงเป็นส่วนตัว

รายการชิ้นส่วนและอุปกรณ์

ในการสร้างโปรเจกต์นี้ คุณจะต้องมีชิ้นส่วนต่อไปนี้ ดังที่กล่าวในวิดีโอ การซื้อผ่านลิงก์พันธมิตรเป็นวิธีที่ดีในการสนับสนุนผู้สร้างสรรค์ผลงาน

  • โมดูล Heltec WiFi LoRa 32 V3
  • เคส Meshtastic N33 พร้อมแบตเตอรี่ 3000mAh
  • เสาอากาศ LoRa ความถี่ 915MHz หรือตามภูมิภาค
  • โมดูล Rotary Encoder (5 พิน พร้อม VCC, GND, CLK, DT, SW)
  • อุปกรณ์เสริม (สำหรับการแจ้งเตือนด้วยเสียง):
    • ออดแอคทีฟ 5V
    • ทรานซิสเตอร์ NPN 2N2222
    • ตัวต้านทาน 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 เพิ่ม Heltec JSON URL ในบรรทัดใหม่ในช่อง "Additional boards manager URLs" (วิดีโอแสดงวิธีทำที่ 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: ติดตั้งไลบรารีที่จำเป็นจาก Library Manager

ใช้ Arduino Library Manager (Sketch > Include Library > Manage Libraries...) ค้นหาและติดตั้งไลบรารีต่อไปนี้:

  1. Ai Esp32 Rotary Encoder: ไลบรารีเฉพาะที่ใช้สำหรับคีย์บอร์ดที่ตอบสนองได้ดีของเรา
  2. Heltec ESP32 Dev-Boards: ไลบรารีอย่างเป็นทางการสำหรับฮาร์ดแวร์ Heltec
  3. Adafruit GFX Library: ไลบรารีที่จำเป็นสำหรับจอแสดงผล OLED

ขั้นตอนที่ 4: ติดตั้ง Robojax Helper Library จากไฟล์ .ZIP

ตามที่อธิบายในวิดีโอที่ 18:53 มีการใช้ไลบรารีตัวช่วยแบบกำหนดเองสำหรับโปรเจกต์นี้ คุณต้องติดตั้งจากไฟล์ .zip

  1. ดาวน์โหลดไลบรารี .zip "Robojax HealthTech Lora 32" จากลิงก์ที่ให้ไว้ในหน้าทรัพยากร
  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: Core Messenger (เฉพาะโรตารีเอนโค้ดเดอร์)

เชื่อมต่อขาทั้ง 5 ขาจากโมดูลโรตารีเอนโค้ดเดอร์เข้ากับบอร์ด Heltec ดังนี้:

  • GND บนเอนโค้ดเดอร์ → GND บนบอร์ด Heltec
  • VCC บนเอนโค้ดเดอร์ → 3.3V บนบอร์ด Heltec
  • SW (สวิตช์) → GPIO 4
  • DT (ข้อมูล/ขา B) → GPIO 6
  • CLK (สัญญาณนาฬิกา/ขา A) → GPIO 5
Helte_Wifi_LoRA_Rotary_Encoder with buzzer

แผนภาพที่ 2: การเพิ่มสัญญาณเตือนแบบเสียง (วงจรออด)

เพื่อให้มีเสียงบี๊บเมื่อมีข้อความใหม่เข้ามา คุณสามารถเพิ่มวงจรทรานซิสเตอร์อย่างง่ายได้ ซึ่งจำเป็นเพราะขาของ ESP32 ไม่สามารถจ่ายกระแสไฟได้เพียงพอที่จะขับออดโดยตรง

  • เชื่อมต่อ GPIO 40 จากบอร์ด Heltec เข้ากับปลายด้านหนึ่งของ ตัวต้านทาน 1kΩ
  • เชื่อมต่อปลายอีกด้านของตัวต้านทานเข้ากับขา Base ของ ทรานซิสเตอร์ 2N2222
  • เชื่อมต่อขา Emitter ของทรานซิสเตอร์เข้ากับ GND
  • เชื่อมต่อขา บวก (+) ของออด 5V เข้ากับขา 3.3V บนบอร์ด Heltec
  • เชื่อมต่อขา ลบ (-) ของออดเข้ากับขา Collector ของทรานซิสเตอร์

การทำความเข้าใจและปรับแต่งโค้ด

โค้ดที่ให้มาพร้อมใช้งานสำหรับการอัปโหลด แต่มีค่าตั้งค่าที่สำคัญบางอย่างที่คุณควรทราบเพื่อให้สามารถปรับแต่งโปรเจกต์ได้ ค่านิยามเหล่านี้อยู่ที่ส่วนบนของโค้ด


// นิยามสำหรับการเชื่อมต่อขาทางกายภาพ
#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
ภาษา: 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);
}

แหล่งข้อมูลและเอกสารอ้างอิง

ไฟล์📁

ไฟล์อื่น ๆ