โครงการ 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
- เปิด Arduino IDE ไปที่
File > Preferences - ในช่อง "Additional boards manager URLs" เพิ่ม URL ต่อไปนี้:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - ไปที่
Tools > Board > Boards Manager... - ค้นหา
esp32และติดตั้งแพ็กเกจโดย Espressif Systems
ขั้นตอนที่ 2: เพิ่มการรองรับบอร์ด Heltec ESP32
- ใน
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 - กลับไปที่
Tools > Board > Boards Manager... - ค้นหา
Heltec ESP32และติดตั้งแพ็กเกจ
ขั้นตอนที่ 3: ติดตั้งไลบรารีที่จำเป็นจาก Library Manager
ใช้ Arduino Library Manager (Sketch > Include Library > Manage Libraries...) ค้นหาและติดตั้งไลบรารีต่อไปนี้:
- Ai Esp32 Rotary Encoder: ไลบรารีเฉพาะที่ใช้สำหรับคีย์บอร์ดที่ตอบสนองได้ดีของเรา
- Heltec ESP32 Dev-Boards: ไลบรารีอย่างเป็นทางการสำหรับฮาร์ดแวร์ Heltec
- Adafruit GFX Library: ไลบรารีที่จำเป็นสำหรับจอแสดงผล OLED
ขั้นตอนที่ 4: ติดตั้ง Robojax Helper Library จากไฟล์ .ZIP
ตามที่อธิบายในวิดีโอที่ 18:53 มีการใช้ไลบรารีตัวช่วยแบบกำหนดเองสำหรับโปรเจกต์นี้ คุณต้องติดตั้งจากไฟล์ .zip
- ดาวน์โหลดไลบรารี .zip "Robojax HealthTech Lora 32" จากลิงก์ที่ให้ไว้ในหน้าทรัพยากร
- ใน Arduino IDE คลิกที่
Sketch > Include Library > Add .ZIP Library... - นำทางไปยังตำแหน่งที่คุณบันทึกไฟล์ .zip ที่ดาวน์โหลดไว้ เลือกไฟล์แล้วคลิก "Open"
- ไลบรารีจะถูกติดตั้งและพร้อมใช้งาน
หลังจากติดตั้งทุกอย่างเรียบร้อยแล้ว ตรวจสอบให้แน่ใจว่าคุณเลือกบอร์ดที่ถูกต้องจากเมนู: Tools > Board > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3
การเดินสายไฟเข้ากับชิ้นส่วน
คุณจะต้องบัดกรีหัวเข็มเข้ากับบอร์ด Heltec เพื่อเชื่อมต่อโรตารีเอนโค้ดเดอร์ ดังที่แสดงในวิดีโอ (11:25) จำเป็นต้องวางแผนอย่างรอบคอบเพื่อให้แน่ใจว่าขาสัญญาณสามารถเข้าถึงได้เมื่อบอร์ดอยู่ในกล่องแล้ว

แผนภาพที่ 1: Core Messenger (เฉพาะโรตารีเอนโค้ดเดอร์)
เชื่อมต่อขาทั้ง 5 ขาจากโมดูลโรตารีเอนโค้ดเดอร์เข้ากับบอร์ด Heltec ดังนี้:
GNDบนเอนโค้ดเดอร์ →GNDบนบอร์ด HeltecVCCบนเอนโค้ดเดอร์ →3.3Vบนบอร์ด HeltecSW(สวิตช์) →GPIO 4DT(ข้อมูล/ขา B) →GPIO 6CLK(สัญญาณนาฬิกา/ขา A) →GPIO 5

แผนภาพที่ 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 - การทดสอบระยะแบบสด
/*
ESP32 Project: Build a Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
File: Heltec_ESP32_LoRa_V3_chat_TX_RX.ino
written on 29 Jun, 2025 by Ahmad Shamshiri www.Roboajx.com
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* ARDUINO CODE DESCRIPTION: SECURE LoRa Keyboard Transceiver (chat system)
* =====================================================================
*
* HARDWARE COMPONENTS:
* -------------------
* - Main Controller: Heltec WiFi LoRa 32 V3
* - Enclosure: Meshnology N33 case with 3000mAh battery
* - Input: Rotary encoder with push-button
* - Feedback: Built-in OLED display
*
* SYSTEM FUNCTIONALITY:
* -------------------
NEW CONTROLS:
- Rotate Knob: Select character.
- Short Press (Click): Add selected character.
- Long Press (Hold): Insert a space (like a spacebar).
- Double Click: Backspace.
*
* [2] SECURE WIRELESS TRANSMISSION:
* - All message encrypted before LoRa transmission
* - Home position command transmitted as special secure packet
* - Uses 433MHz/915MHZ LoRa band for reliable communication
*
* [3] POWER MANAGEMENT:
* - Optimized for battery operation (3000mAh)
* - Low-power modes between transmissions
*
* FOR COMPLETE SETUP INSTRUCTIONS:
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* DISCLAIMER:
* This code is provided "AS IS" without warranty of any kind. The author
* shall not be held liable for any damages arising from the use of this code.
*
* LICENSE:
* This work is licensed under the GNU General Public License v3.0
* Permissions beyond the scope of this license may be available at Robojax.com
*
* SHARING TERMS:
* You are free to share, copy and modify this code for non-commercial purposes
* PROVIDED you:
* 1. Keep this entire comment block intact with the original code
* 2. Include the original Robojax.com link
* 3. Keep the YouTube tutorial link (if applicable)
* 4. Clearly indicate any modifications made
*
*
* ********************************************************************
*/
// Core Heltec and display libraries
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include "WiFi.h"
// LoRa and security libraries
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
#include <cstring>
// The user-approved Rotary Encoder library
#include "AiEsp32RotaryEncoder.h"
// --- PIN DEFINITIONS & CONFIGURATION ---
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
#define ROTARY_ENCODER_A_PIN 5 //pin A of rotary encoder
#define ROTARY_ENCODER_B_PIN 6 //pin B of rotary encoder
#define ROTARY_ENCODER_BUTTON_PIN 4 //switch pin of rotary encoder
#define RF_FREQUENCY 915432000 //operating frequey of LoRa
#define TX_OUTPUT_POWER 20 //output power should be between 2 to 21 in dBm.
const char *userKey = "wahJ8uy$gt~_3r";//secureity key for private communication
#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4
// --- LORA PARAMETERS ---
#define LORA_BANDWIDTH 0
#define LORA_SPREADING_FACTOR 7
#define LORA_CODINGRATE 1
#define LORA_PREAMBLE_LENGTH 8
#define LORA_SYMBOL_TIMEOUT 0
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#define BUFFER_SIZE 256
#define USER_BUTTON_PIN 0
// --- GLOBAL VARIABLES & STATE ---
enum AppState { COMPOSING, READING };
AppState currentState = COMPOSING;
String messageToCompose = "";
String lastReceivedMessage = "No messages yet.";
bool hasUnreadMessage = false;
volatile bool newPacketAvailable = false;
char rxpacket[BUFFER_SIZE];
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
const char* charset = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?>";
char selectedChar;
static RadioEvents_t RadioEvents;
bool lora_idle = true;
mbedtls_aes_context aes;
unsigned long shortPressAfterMiliseconds = 50;
unsigned long longPressAfterMiliseconds = 1000;
unsigned long doubleClickTimeout = 400;
static unsigned long buttonDownTime = 0;
static unsigned long buttonUpTime = 0;
static bool buttonWasDown = false;
static bool doubleClickWaiting = false;
// --- FUNCTION PROTOTYPES ---
void OnTxDone(void);
void OnTxTimeout(void);
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr);
void encryptAES(uint8_t *data, const char *key);
void decryptAES(uint8_t *data, const char *key);
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize);
void updateDisplay();
void sendData();
void VextON(void);
void processButton();
// --- INTERRUPT SERVICE ROUTINES ---
void IRAM_ATTR readEncoderISR() {
rotaryEncoder.readEncoder_ISR();
}
// --- BUTTON HANDLER ACTIONS ---
void on_button_short_click() {
if (currentState != COMPOSING) return;
if (selectedChar == '>') {
sendData();
} else {
messageToCompose += selectedChar;
}
updateDisplay();
}
void on_button_long_click() {
if (currentState != COMPOSING) return;
messageToCompose += " ";
updateDisplay();
}
void on_button_double_click() {
if (currentState != COMPOSING) return;
if (messageToCompose.length() > 0) {
messageToCompose.remove(messageToCompose.length() - 1);
}
updateDisplay();
}
// --- SETUP ---
void setup() {
Serial.begin(115200);
Serial.println("LoRa Keyboard Transceiver - Definitive Version");
VextON();
delay(100);
Mcu.begin(HELTEC_BOARD,SLOW_CLK_TPYE);
WiFi.mode(WIFI_OFF);
btStop();
display.init();
display.setFont(ArialMT_Plain_10);
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, strlen(charset) - 1, true);
rotaryEncoder.disableAcceleration();
pinMode(USER_BUTTON_PIN, INPUT_PULLUP);
RadioEvents.TxDone = OnTxDone;
RadioEvents.TxTimeout = OnTxTimeout;
RadioEvents.RxDone = OnRxDone;
Radio.Init(&RadioEvents);
Radio.SetChannel(RF_FREQUENCY);
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,
0, true, 0, 0, LORA_IQ_INVERSION_ON, true);
updateDisplay();
}
// --- MAIN LOOP ---
void loop() {
if(lora_idle) {
lora_idle = false;
Serial.println("Entering RX mode...");
Radio.Rx(0);
}
Radio.IrqProcess();
if (newPacketAvailable) {
newPacketAvailable = false;
hasUnreadMessage = true;
lastReceivedMessage = String(rxpacket);
Serial.print("Received Decrypted Message: ");
Serial.println(lastReceivedMessage);
updateDisplay();
}
if (currentState == COMPOSING) {
if (rotaryEncoder.encoderChanged()) {
updateDisplay();
}
processButton();
}
if (digitalRead(USER_BUTTON_PIN) == LOW) {
delay(50);
if (digitalRead(USER_BUTTON_PIN) == LOW) {
if (currentState == COMPOSING) {
if (lastReceivedMessage != "No messages yet.") {
currentState = READING;
hasUnreadMessage = false;
updateDisplay();
}
} else {
currentState = COMPOSING;
updateDisplay();
}
while (digitalRead(USER_BUTTON_PIN) == LOW);
}
}
delay(10);
}
// --- LORA EVENT HANDLERS & HELPERS ---
void OnTxDone(void) {
Serial.println("TX done.");
Radio.Sleep();
lora_idle = true;
}
void OnTxTimeout(void) {
Serial.println("TX Timeout.");
Radio.Sleep();
lora_idle = true;
}
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr) {
Radio.Sleep();
memset(rxpacket, 0, sizeof(rxpacket));
memcpy(rxpacket, payload, size > BUFFER_SIZE ? BUFFER_SIZE : size);
decryptAES((uint8_t*)rxpacket, userKey);
newPacketAvailable = true;
lora_idle = true;
}
void sendData() {
if (messageToCompose.length() > 0) {
lora_idle = false;
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 3000);
uint8_t data[BUFFER_SIZE];
memset(data, 0, BUFFER_SIZE);
strncpy((char*)data, messageToCompose.c_str(), BUFFER_SIZE - 1);
encryptAES(data, userKey);
Serial.print("Sending message: ");
Serial.println(messageToCompose);
Radio.Send(data, 16);
messageToCompose = "";
}
}
// --- DISPLAY & BUTTON LOGIC ---
void updateDisplay() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);
if (currentState == COMPOSING) {
selectedChar = charset[rotaryEncoder.readEncoder()];
display.setFont(ArialMT_Plain_10);
// --- THIS IS THE FIX for the notification ---
String msgLabel;
if (hasUnreadMessage) {
msgLabel = ">> NEW MSG <<"; // Make the notification very obvious
} else {
msgLabel = "Msg:";
}
display.drawStringMaxWidth(0, 0, 128, msgLabel + " " + messageToCompose);
display.setFont(ArialMT_Plain_16);
int16_t labelWidth = display.getStringWidth("Select: ");
display.drawString(0, 35, "Select: ");
display.setFont(ArialMT_Plain_24);
display.drawString(labelWidth, 32, String(selectedChar));
} else { // currentState == READING
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "Received:");
display.setFont(ArialMT_Plain_10);
display.drawStringMaxWidth(0, 18, 128, lastReceivedMessage);
}
display.display();
}
void processButton() {
if (doubleClickWaiting && (millis() - buttonUpTime > doubleClickTimeout)) {
doubleClickWaiting = false;
on_button_short_click();
}
bool isButtonDown = rotaryEncoder.isEncoderButtonDown();
if (isButtonDown && !buttonWasDown) {
buttonDownTime = millis();
buttonWasDown = true;
}
else if (!isButtonDown && buttonWasDown) {
unsigned long pressDuration = millis() - buttonDownTime;
if (pressDuration >= longPressAfterMiliseconds) {
on_button_long_click();
doubleClickWaiting = false;
}
else if (pressDuration >= shortPressAfterMiliseconds) {
if (doubleClickWaiting) {
on_button_double_click();
doubleClickWaiting = false;
} else {
doubleClickWaiting = true;
buttonUpTime = millis();
}
}
buttonWasDown = false;
}
}
// --- UTILITIES ---
void VextON(void) {
pinMode(Vext,OUTPUT);
digitalWrite(Vext, LOW);
}
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize) {
memset(processedKey, 0, keySize);
size_t len = strlen(userKey);
if (len > keySize) len = keySize;
memcpy(processedKey, userKey, len);
}
void encryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, data, data);
mbedtls_aes_free(&aes);
}
void decryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, data, data);
mbedtls_aes_free(&aes);
}
/*
ESP32 Project: Build a Secure, Long-Range LoRa Chat Device Heltec WiFi LoRa 32
File: Heltec_ESP32_LoRa_V3_chat_TX_RX_buzzer.ino
written on 29 Jun, 2025 by Ahmad Shamshiri www.Roboajx.com
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* ARDUINO CODE DESCRIPTION: SECURE LoRa Keyboard Transceiver (chat system)
* =====================================================================
*
* HARDWARE COMPONENTS:
* -------------------
* - Main Controller: Heltec WiFi LoRa 32 V3
* - Enclosure: Meshnology N33 case with 3000mAh battery
* - Input: Rotary encoder with push-button
* - Audiable: Active buzzer (connectedt o pin 40)
* - Feedback: Built-in OLED display
*
* SYSTEM FUNCTIONALITY:
* -------------------
NEW CONTROLS:
- Rotate Knob: Select character.
- Short Press (Click): Add selected character.
- Long Press (Hold): Insert a space (like a spacebar).
- Double Click: Backspace.
*
* [2] SECURE WIRELESS TRANSMISSION:
* - All message encrypted before LoRa transmission
* - Home position command transmitted as special secure packet
* - Uses 433MHz/915MHZ LoRa band for reliable communication
*
* [3] POWER MANAGEMENT:
* - Optimized for battery operation (3000mAh)
* - Low-power modes between transmissions
*
* FOR COMPLETE SETUP INSTRUCTIONS:
* Please watch the tutorial video at: https://youtu.be/IsOSvrF60J4
* Resources page for this video: https://robojax.com/RJT642
* =====================================================================
* DISCLAIMER:
* This code is provided "AS IS" without warranty of any kind. The author
* shall not be held liable for any damages arising from the use of this code.
*
* LICENSE:
* This work is licensed under the GNU General Public License v3.0
* Permissions beyond the scope of this license may be available at Robojax.com
*
* SHARING TERMS:
* You are free to share, copy and modify this code for non-commercial purposes
* PROVIDED you:
* 1. Keep this entire comment block intact with the original code
* 2. Include the original Robojax.com link
* 3. Keep the YouTube tutorial link (if applicable)
* 4. Clearly indicate any modifications made
*
*
* ********************************************************************
*/
// Core Heltec and display libraries
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include "WiFi.h"
// LoRa and security libraries
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
#include <cstring>
// The user-approved Rotary Encoder library
#include "AiEsp32RotaryEncoder.h"
// --- PIN DEFINITIONS & CONFIGURATION ---
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
#define ROTARY_ENCODER_A_PIN 5 //pin A of rotary encoder
#define ROTARY_ENCODER_B_PIN 6 //pin B of rotary encoder
#define ROTARY_ENCODER_BUTTON_PIN 4 //switch pin of rotary encoder
#define BUZZER_PIN 40 //the buzzer pin or relay to be triggered
const bool DEBUG = false;
#define RF_FREQUENCY 915432000 //operating frequency of LoRa
#define TX_OUTPUT_POWER 2 //output power should be between 2 to 21 in dBm.
const char *userKey = "wahJ8uy$gt~_3r";//secureity key for private communication
#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4
// --- LORA PARAMETERS ---
#define LORA_BANDWIDTH 0
#define LORA_SPREADING_FACTOR 7
#define LORA_CODINGRATE 1
#define LORA_PREAMBLE_LENGTH 8
#define LORA_SYMBOL_TIMEOUT 0
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#define BUFFER_SIZE 256
#define LOOP_DELAY 10
// Buzzer state variables
unsigned long buzzerStartTime = 0;
bool isBuzzerActive = false;
int beepPhase = 0; // 0=idle, 1=first beep, 2=pause, 3=second beep
#define USER_BUTTON_PIN 0
// --- GLOBAL VARIABLES & STATE ---
enum AppState { COMPOSING, READING };
AppState currentState = COMPOSING;
String messageToCompose = "";
String lastReceivedMessage = "No messages yet.";
bool hasUnreadMessage = false;
volatile bool newPacketAvailable = false;
char rxpacket[BUFFER_SIZE];
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
const char* charset = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?>";
char selectedChar;
static RadioEvents_t RadioEvents;
bool lora_idle = true;
mbedtls_aes_context aes;
unsigned long shortPressAfterMiliseconds = 50;
unsigned long longPressAfterMiliseconds = 1000;
unsigned long doubleClickTimeout = 400;
static unsigned long buttonDownTime = 0;
static unsigned long buttonUpTime = 0;
static bool buttonWasDown = false;
static bool doubleClickWaiting = false;
// --- FUNCTION PROTOTYPES ---
void OnTxDone(void);
void OnTxTimeout(void);
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr);
void encryptAES(uint8_t *data, const char *key);
void decryptAES(uint8_t *data, const char *key);
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize);
void updateDisplay();
void sendData();
void VextON(void);
void processButton();
void buzzer();
void updateBuzzer();
// --- INTERRUPT SERVICE ROUTINES ---
void IRAM_ATTR readEncoderISR() {
rotaryEncoder.readEncoder_ISR();
}
// --- BUTTON HANDLER ACTIONS ---
void on_button_short_click() {
if (currentState != COMPOSING) return;
if (selectedChar == '>') {
sendData();
} else {
messageToCompose += selectedChar;
}
updateDisplay();
}
void on_button_long_click() {
if (currentState != COMPOSING) return;
messageToCompose += " ";
updateDisplay();
}
void on_button_double_click() {
if (currentState != COMPOSING) return;
if (messageToCompose.length() > 0) {
messageToCompose.remove(messageToCompose.length() - 1);
}
updateDisplay();
}
// --- SETUP ---
void setup() {
Serial.begin(115200);
Serial.println("LoRa Keyboard Transceiver - By Robojax");
VextON();
delay(100);
Mcu.begin(HELTEC_BOARD,SLOW_CLK_TPYE);
WiFi.mode(WIFI_OFF);
btStop();
display.init();
display.setFont(ArialMT_Plain_10);
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, strlen(charset) - 1, true);
rotaryEncoder.disableAcceleration();
pinMode(USER_BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
RadioEvents.TxDone = OnTxDone;
RadioEvents.TxTimeout = OnTxTimeout;
RadioEvents.RxDone = OnRxDone;
Radio.Init(&RadioEvents);
Radio.SetChannel(RF_FREQUENCY);
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,
0, true, 0, 0, LORA_IQ_INVERSION_ON, true);
updateDisplay();
}
// --- MAIN LOOP ---
void loop() {
if(lora_idle) {
lora_idle = false;
if(DEBUG)
{
Serial.println("Entering RX mode...");
}
Radio.Rx(0);
}
Radio.IrqProcess();
if (newPacketAvailable) {
newPacketAvailable = false;
hasUnreadMessage = true;
lastReceivedMessage = String(rxpacket);
if(DEBUG)
{
Serial.print("Received Decrypted Message: ");
Serial.println(lastReceivedMessage);
}
updateDisplay();
buzzer();
}
if (currentState == COMPOSING) {
if (rotaryEncoder.encoderChanged()) {
updateDisplay();
}
processButton();
}
if (digitalRead(USER_BUTTON_PIN) == LOW) {
delay(50);
if (digitalRead(USER_BUTTON_PIN) == LOW) {
if (currentState == COMPOSING) {
if (lastReceivedMessage != "No messages yet.") {
currentState = READING;
hasUnreadMessage = false;
updateDisplay();
}
} else {
currentState = COMPOSING;
updateDisplay();
}
while (digitalRead(USER_BUTTON_PIN) == LOW);
}
}
updateBuzzer(); // Update buzzer state (non-blocking)
delay(LOOP_DELAY); // Your 10ms loop delay
}
// --- LORA EVENT HANDLERS & HELPERS ---
void OnTxDone(void) {
if(DEBUG)
{
Serial.println("TX done.");
}
Radio.Sleep();
lora_idle = true;
}
void OnTxTimeout(void) {
if(DEBUG)
{
Serial.println("TX Timeout.");
}
Radio.Sleep();
lora_idle = true;
}
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr) {
Radio.Sleep();
memset(rxpacket, 0, sizeof(rxpacket));
memcpy(rxpacket, payload, size > BUFFER_SIZE ? BUFFER_SIZE : size);
decryptAES((uint8_t*)rxpacket, userKey);
newPacketAvailable = true;
lora_idle = true;
}
void sendData() {
if (messageToCompose.length() > 0) {
lora_idle = false;
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 3000);
uint8_t data[BUFFER_SIZE];
memset(data, 0, BUFFER_SIZE);
strncpy((char*)data, messageToCompose.c_str(), BUFFER_SIZE - 1);
encryptAES(data, userKey);
if(DEBUG)
{
Serial.print("Sending message: ");
Serial.println(messageToCompose);
}
Radio.Send(data, 16);
messageToCompose = "";
}
}
// --- DISPLAY & BUTTON LOGIC ---
void updateDisplay() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);
if (currentState == COMPOSING) {
selectedChar = charset[rotaryEncoder.readEncoder()];
display.setFont(ArialMT_Plain_10);
// --- THIS IS THE FIX for the notification ---
String msgLabel;
if (hasUnreadMessage) {
msgLabel = ">> NEW MSG <<"; // Make the notification very obvious
} else {
msgLabel = "Msg:";
}
display.drawStringMaxWidth(0, 0, 128, msgLabel + " " + messageToCompose);
display.setFont(ArialMT_Plain_16);
int16_t labelWidth = display.getStringWidth("Select: ");
display.drawString(0, 35, "Select: ");
display.setFont(ArialMT_Plain_24);
display.drawString(labelWidth, 32, String(selectedChar));
} else { // currentState == READING
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "Received:");
display.setFont(ArialMT_Plain_10);
display.drawStringMaxWidth(0, 18, 128, lastReceivedMessage);
}
display.display();
}
void processButton() {
if (doubleClickWaiting && (millis() - buttonUpTime > doubleClickTimeout)) {
doubleClickWaiting = false;
on_button_short_click();
}
bool isButtonDown = rotaryEncoder.isEncoderButtonDown();
if (isButtonDown && !buttonWasDown) {
buttonDownTime = millis();
buttonWasDown = true;
}
else if (!isButtonDown && buttonWasDown) {
unsigned long pressDuration = millis() - buttonDownTime;
if (pressDuration >= longPressAfterMiliseconds) {
on_button_long_click();
doubleClickWaiting = false;
}
else if (pressDuration >= shortPressAfterMiliseconds) {
if (doubleClickWaiting) {
on_button_double_click();
doubleClickWaiting = false;
} else {
doubleClickWaiting = true;
buttonUpTime = millis();
}
}
buttonWasDown = false;
}
}
void buzzer()
{
if (!isBuzzerActive) {
isBuzzerActive = true;
buzzerStartTime = millis(); // Start buzzer sequence
beepPhase = 1; // First beep
digitalWrite(BUZZER_PIN, HIGH);
}
}
void updateBuzzer() {
if (!isBuzzerActive) return;
unsigned long currentTime = millis();
unsigned long elapsed = currentTime - buzzerStartTime;
switch (beepPhase) {
case 1: // First beep (300ms ON)
if (elapsed >= 300) {
digitalWrite(BUZZER_PIN, LOW);
buzzerStartTime = currentTime;
beepPhase = 2; // Move to pause
}
break;
case 2: // Pause (200ms OFF)
if (elapsed >= 200) {
digitalWrite(BUZZER_PIN, HIGH);
buzzerStartTime = currentTime;
beepPhase = 3; // Move to second beep
}
break;
case 3: // Second beep (300ms ON)
if (elapsed >= 300) {
digitalWrite(BUZZER_PIN, LOW);
isBuzzerActive = false; // Reset
beepPhase = 0;
}
break;
}
}
// --- UTILITIES ---
void VextON(void) {
pinMode(Vext,OUTPUT);
digitalWrite(Vext, LOW);
}
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize) {
memset(processedKey, 0, keySize);
size_t len = strlen(userKey);
if (len > keySize) len = keySize;
memcpy(processedKey, userKey, len);
}
void encryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, data, data);
mbedtls_aes_free(&aes);
}
void decryptAES(uint8_t *data, const char *key) {
uint8_t processedKey[16];
processKey(key, processedKey, 16);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, processedKey, 128);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, data, data);
mbedtls_aes_free(&aes);
}
Common Course Links
Common Course Files
แหล่งข้อมูลและเอกสารอ้างอิง
-
ภายนอกPurchase Wi-Fi LoRa 32 from Meshnologymeshnology.com
ไฟล์📁
ไฟล์อื่น ๆ
-
Robojax Heltec WiFi LoRa 32 Library Version 1.1.0 20250703
Robojax_HeltecLoRa32_1.1.0_20250702.zip0.09 MB -