Dự án ESP32: Xây dựng thiết bị trò chuyện LoRa an toàn, tầm xa Heltec WiFi LoRa 32 trên N33
Xây dựng Trình Nhắn Tin LoRa An Toàn, Không Cần Mạng Với Bàn Phím Tùy Chỉnh
Trong một thế giới phụ thuộc vào kết nối liên tục, bạn sẽ làm gì khi Wi-Fi mất, điện cúp và dịch vụ di động biến mất? Dự án này giải quyết chính xác vấn đề đó bằng cách tạo ra một thiết bị liên lạc hoàn toàn tự cung tự cấp, không cần mạng, cho phép bạn gửi và nhận tin nhắn văn bản an toàn, riêng tư trong phạm vi lên tới 13 dặm (21km) hoặc thậm chí xa hơn.
Thiết bị này là một công cụ thực tế cho việc chuẩn bị khẩn cấp, các chuyến phiêu lưu ngoài trời như đi bộ đường dài, hoặc đơn giản cho bất kỳ ai quan tâm đến việc xây dựng một mạng lưới liên lạc riêng tư, mạnh mẽ mà họ kiểm soát. Sử dụng mô-đun Heltec WiFi LoRa 32 V3 mạnh mẽ được đặt bên trong vỏ Meshnology N33 bền bỉ với pin 3000mAh lâu dài, dự án này được thiết kế cho sử dụng thực tế. Tính năng độc đáo nhất là bàn phím của nó—một bộ mã hóa xoay đơn giản cung cấp một cách trực quan và đáng tin cậy để gõ tin nhắn ở bất cứ đâu, bất cứ lúc nào.
Cách Hoạt Động: Sự Kỳ Diệu Của LoRa Và Bộ Mã Hóa Xoay
Dự án kết hợp ba khái niệm chính để tạo ra một trình nhắn tin chức năng:
- Radio LoRa: LoRa (Tầm Xa) là một công nghệ radio cho phép liên lạc qua khoảng cách rất lớn với rất ít năng lượng. Không giống như Wi-Fi hoặc di động, nó hoạt động trên các tần số radio miễn phí, nghĩa là không có SIM hoặc phí hàng tháng. Thiết bị của chúng tôi hoạt động như một bộ thu phát đầy đủ, nghĩa là nó có thể vừa gửi vừa nhận tin nhắn. Khi rảnh, nó lắng nghe các gói tin đến. Sau khi gửi hoặc nhận tin nhắn, nó đưa radio vào chế độ ngủ tạm thời để tiết kiệm năng lượng trước khi quay lại chế độ lắng nghe.
- Bàn Phím Bộ Mã Hóa Xoay: Thay vì một bàn phím cồng kềnh hoặc tốn năng lượng, chúng tôi sử dụng một bộ mã hóa xoay đơn giản để nhập văn bản. Xoay núm sẽ duyệt qua một bộ ký tự hoàn chỉnh (A-Z, a-z, số và ký hiệu). Giao diện nhanh và phản hồi đáng ngạc nhiên, cho phép bạn dễ dàng soạn tin nhắn. Nút nhấn tích hợp của bộ mã hóa có ba chức năng:
- Nhấn Ngắn (Click): Thêm ký tự đã chọn vào tin nhắn của bạn.
- Nhấn Dài (Giữ): Thêm một khoảng trắng, giống như phím cách.
- Nhấn Đúp: Hoạt động như phím xóa lùi để xóa ký tự cuối cùng.
- Bộ Thu Phát & Logic Hiển Thị: Thiết bị có hai chế độ chính: "Soạn" và "Đọc". Bạn gõ tin nhắn ở chế độ Soạn. Khi một tin nhắn mới đến, một thông báo xuất hiện trên màn hình (
>> TIN NHẮN MỚI <<). Sau đó, bạn có thể nhấn Nút Người Dùng trên bo mạch (trên bo Heltec) để chuyển sang chế độ Đọc và xem tin nhắn. Nhấn lần nữa sẽ chuyển bạn trở lại chế độ Soạn để gõ câu trả lời. Tất cả tin nhắn được bảo mật bằng mã hóa AES, đảm bảo cuộc trò chuyện của bạn vẫn riêng tư.
Danh Sách Linh Kiện Và Thành Phần
Để xây dựng dự án này, bạn sẽ cần các thành phần sau. Như đã đề cập trong video, mua qua các liên kết liên kết là một cách tuyệt vời để hỗ trợ người sáng tạo.
- Mô-đun Heltec WiFi LoRa 32 V3
- Vỏ Meshtastic N33 với Pin 3000mAh
- Ăng-ten LoRa 915MHz hoặc phù hợp với khu vực
- Mô-đun Bộ Mã Hóa Xoay (5 chân với VCC, GND, CLK, DT, SW)
- Tùy chọn (cho cảnh báo âm thanh):
- Còi Báo Động Tích Cực 5V
- Transistor NPN 2N2222
- Điện Trở 1kΩ
- Đầu Cắm Chân Và Dây Nhảy
- Mỏ Hàn Và Phụ Kiện
Thiết Lập Arduino IDE Của Bạn
Trước khi bạn có thể tải mã lên, bạn cần cấu hình Arduino IDE với các bo mạch và thư viện chính xác. Quá trình này được trình bày chi tiết trong video bắt đầu khoảng 05:38.
Bước 1: Cài Đặt Bo Mạch ESP32
Nếu bạn chưa làm, bạn cần thêm hỗ trợ cho vi điều khiển ESP32.
- Mở Arduino IDE, đi tới
File > Preferences. - Trong trường "Additional boards manager URLs", thêm URL sau:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - Đi tới
Tools > Board > Boards Manager.... - Tìm kiếm
esp32và cài đặt gói của Espressif Systems.
Bước 2: Thêm Hỗ Trợ Bo Mạch Heltec ESP32
- Trong
File > Preferences, thêm URL JSON của Heltec trên một dòng mới trong hộp "Additional boards manager URLs" (video cho thấy cách làm điều này tại 06:27):https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/releases/download/0.0.7/package_heltec_esp32_index.json - Quay lại
Tools > Board > Boards Manager.... - Tìm kiếm
Heltec ESP32và cài đặt gói.
Bước 3: Cài Đặt Các Thư Viện Cần Thiết Từ Trình Quản Lý Thư Viện
Sử dụng Trình Quản Lý Thư Viện Arduino (Sketch > Include Library > Manage Libraries...), tìm kiếm và cài đặt các thư viện sau:
- Ai Esp32 Rotary Encoder: Thư viện cụ thể được sử dụng cho bàn phím phản hồi của chúng tôi.
- Heltec ESP32 Dev-Boards: Thư viện chính thức cho phần cứng Heltec.
- Adafruit GFX Library: Một phụ thuộc cho màn hình OLED.
Bước 4: Cài Đặt Thư Viện Hỗ Trợ Robojax Từ Tệp .ZIP
Như được giải thích trong video tại 18:53, một thư viện hỗ trợ tùy chỉnh được sử dụng cho dự án này. Bạn phải cài đặt nó từ tệp .zip.
- Tải xuống thư viện .zip "Robojax HealthTech Lora 32" từ liên kết được cung cấp trên trang tài nguyên.
- Trong Arduino IDE, nhấp vào
Sketch > Include Library > Add .ZIP Library.... - Điều hướng đến nơi bạn đã lưu tệp .zip đã tải xuống, chọn nó và nhấp "Open".
- Thư viện sẽ được cài đặt và sẵn sàng để sử dụng.
Sau khi cài đặt mọi thứ, hãy đảm bảo bạn chọn đúng bo mạch từ menu: Tools > Board > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3.
Đấu nối các linh kiện
Bạn sẽ cần hàn các chân cắm (pin header) vào bo mạch Heltec của mình để kết nối bộ mã hóa xoay (rotary encoder). Như được trình bày trong video (11:25), cần lên kế hoạch cẩn thận để đảm bảo các chân có thể tiếp cận được khi bo mạch đã nằm trong vỏ.

Sơ đồ 1: Core Messenger (Chỉ có bộ mã hóa xoay)
Kết nối 5 chân từ mô-đun bộ mã hóa xoay với bo mạch Heltec như sau:
GNDtrên bộ mã hóa →GNDtrên bo mạch HeltecVCCtrên bộ mã hóa →3.3Vtrên bo mạch HeltecSW(Công tắc) →GPIO 4DT(Dữ liệu/Chân B) →GPIO 6CLK(Xung nhịp/Chân A) →GPIO 5

Sơ đồ 2: Thêm cảnh báo âm thanh (Mạch còi buzzer)
Để có tiếng bíp khi có tin nhắn mới đến, bạn có thể thêm một mạch transistor đơn giản. Điều này là cần thiết vì các chân của ESP32 không thể cung cấp đủ dòng điện để điều khiển trực tiếp một còi buzzer.
- Kết nối
GPIO 40từ bo mạch Heltec với một đầu của điện trở 1kΩ. - Kết nối đầu còn lại của điện trở với chân Base của transistor 2N2222.
- Kết nối chân Emitter của transistor với
GND. - Kết nối chân dương (+) của còi buzzer 5V với chân
3.3Vtrên bo mạch Heltec. - Kết nối chân âm (-) của còi buzzer với chân Collector của transistor.
Hiểu và tùy chỉnh mã nguồn
Mã nguồn được cung cấp đã sẵn sàng để nạp, nhưng có một vài cài đặt quan trọng bạn nên biết để có thể tùy chỉnh dự án. Các định nghĩa này nằm ở đầu mã nguồn.
// Định nghĩa cho các kết nối chân vật lý
#define ROTARY_ENCODER_A_PIN 5 //chân A của bộ mã hóa xoay
#define ROTARY_ENCODER_B_PIN 6 //chân B của bộ mã hóa xoay
#define ROTARY_ENCODER_BUTTON_PIN 4 //chân công tắc của bộ mã hóa xoay
#define BUZZER_PIN 40 //chân buzzer hoặc relay cần kích hoạt
const bool DEBUG = false;
// Định nghĩa cho cài đặt LoRa và Bảo mật
#define RF_FREQUENCY 915432000 //tần số hoạt động của LoRa
#define TX_OUTPUT_POWER 2 //công suất phát nên nằm trong khoảng 2 đến 21 dBm.
const char *userKey = "xxxBg^Tr%43232";//khóa bảo mật cho liên lạc riêng tư
ROTARY_ENCODER_...: Các chân này xác định nơi bạn kết nối bộ mã hóa xoay của mình. Đảm bảo các số này khớp với cách đấu dây của bạn trong các sơ đồ ở trên.BUZZER_PIN: Điều này xác định chân nào sẽ kích hoạt mạch buzzer.RF_FREQUENCY: Đây là tần số hoạt động của LoRa tính bằng Hertz. Giá trị 915432000 (915 MHz) là tiêu chuẩn cho Bắc Mỹ. Bạn phải thay đổi giá trị này theo khu vực của mình (ví dụ: 868MHz cho Châu Âu, 433MHz cho Châu Á). Tất cả các thiết bị muốn liên lạc với nhau phải sử dụng cùng một tần số.TX_OUTPUT_POWER: Đây là công suất phát tính bằng dBm. Nó có thể nằm trong khoảng từ 2 đến 20. Một con số thấp hơn như2là hoàn hảo cho việc kiểm tra ở khoảng cách ngắn và tiết kiệm pin đáng kể. Để có phạm vi tối đa, bạn có thể tăng giá trị này, nhưng lưu ý rằng công suất cao hơn sẽ làm hao pin nhanh hơn nhiều.userKey: Đây là khóa mã hóa riêng tư của bạn. Nó có thể là bất kỳ tổ hợp ký tự nào. Tất cả các thiết bị phải có cùng một khóa chính xác để giải mã tin nhắn của nhau. Hãy thay đổi khóa này thành mật khẩu duy nhất và bí mật của riêng bạn.
Lấy mã nguồn
Mã Arduino hoàn chỉnh và cuối cùng cho dự án này, như được đề cập trong video, có sẵn để tải xuống trên trang này.
Các chương trong video
- 00:00 - Giới thiệu
- 00:56 - Tổng quan dự án
- 01:29 - Tổng quan linh kiện
- 05:25 - Mở hộp và lắp ráp phần cứng
- 11:19 - Sửa đổi vỏ để luồn dây ra ngoài
- 15:06 - Hướng dẫn đấu dây
- 15:38 - Thiết lập Arduino IDE và thư viện
- 20:21 - Giải thích các cài đặt mã nguồn
- 21:38 - Cách sử dụng bộ mã hóa xoay để trò chuyện
- 24:26 - Kiểm tra phạm vi thực tế
/*
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
Tài nguyên & tài liệu tham khảo
-
Bên ngoàiPurchase Wi-Fi LoRa 32 from Meshnologymeshnology.com
Tập tin📁
Các tập tin khác
-
Robojax Heltec WiFi LoRa 32 Library Version 1.1.0 20250703
Robojax_HeltecLoRa32_1.1.0_20250702.zip0.09 MB -