搜索代码

ESP32项目:基于N33的Heltec WiFi LoRa 32构建安全、远距离LoRa聊天设备

本课是……的一部分: WiFi LoRa简介

ESP32项目:基于N33的Heltec WiFi LoRa 32构建安全、远距离LoRa聊天设备

使用自定义键盘构建安全的离网LoRa信使

在一个依赖持续连接的世界里,当Wi-Fi中断、电力停止、蜂窝服务消失时,你会怎么做?本项目通过创建一个完全自给自足的离网通信设备来解决这一确切问题,该设备允许您在长达13英里(21公里)甚至更远的距离上发送和接收安全、私密的文本消息。

该设备是应急准备、徒步等户外探险的实用工具,也适合任何有兴趣构建自己掌控的稳健私密通信网络的人。使用强大的Heltec WiFi LoRa 32 V3模块,封装在耐用的Meshnology N33外壳中,配备持久耐用的3000mAh电池,本项目专为实际使用而设计。其最独特的功能是键盘——一个简单的旋转编码器,可随时随地提供直观可靠的打字方式。


工作原理:LoRa与旋转编码器的魔力

本项目结合三个关键概念来打造一个功能齐全的信使:

  • LoRa无线电:LoRa(远距离)是一种无线电技术,能够以极低的功耗实现远距离通信。与Wi-Fi或蜂窝网络不同,它在免许可无线电频率上运行,这意味着无需SIM卡或月费。我们的设备作为完整的收发器运行,既能发送也能接收消息。空闲时,它会监听传入的数据包。发送或接收消息后,它会暂时让无线电休眠以节省电量,然后返回监听模式。
  • 旋转编码器键盘:我们使用简单的旋转编码器进行文本输入,而不是笨重或耗电的键盘。旋转旋钮可循环浏览完整的字符集(A-Z、a-z、数字和符号)。该界面出奇地快速且响应灵敏,让您轻松编写消息。编码器内置的按钮具有三个功能:
    • 短按(单击):将所选字符添加到您的消息中。
    • 长按(按住):添加空格,如同空格键。
    • 双击:作为退格键删除最后一个字符。
  • 收发器与显示逻辑:该设备有两种主要模式:“编写”和“阅读”。您在编写模式下输入消息。当新消息到达时,屏幕上会出现通知(>> 新消息 <<)。然后您可以按下板载用户按钮(在Heltec板上)切换到阅读模式查看消息。再次按下则切换回编写模式以输入回复。所有消息均使用AES加密保护,确保您的对话保持私密。

组件与零件清单

要构建此项目,您需要以下组件。如视频中所述,通过联盟链接购买是支持创作者的好方法。

  • Heltec WiFi LoRa 32 V3模块
  • 带3000mAh电池的Meshtastic N33外壳
  • 915MHz或适合您所在地区的LoRa天线
  • 旋转编码器模块(5针,带VCC、GND、CLK、DT、SW)
  • 可选(用于声音提醒):
    • 5V有源蜂鸣器
    • 2N2222 NPN晶体管
    • 1kΩ电阻
  • 排针和跳线
  • 烙铁及配件

设置您的Arduino IDE

在上传代码之前,您需要使用正确的开发板和库配置Arduino IDE。此过程在视频中约05:38处有详细说明。

步骤1:安装ESP32开发板

如果您还没有安装,则需要添加对ESP32微控制器的支持。

  1. 打开Arduino IDE,转到文件 > 首选项
  2. 在“附加开发板管理器网址”字段中,添加以下URL:https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. 转到工具 > 开发板 > 开发板管理器...
  4. 搜索esp32并安装Espressif Systems的软件包。

步骤2:添加Heltec ESP32开发板支持

  1. 文件 > 首选项中,在“附加开发板管理器网址”框中换行添加Heltec JSON URL(视频在06:27处展示了操作方法):https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/releases/download/0.0.7/package_heltec_esp32_index.json
  2. 返回工具 > 开发板 > 开发板管理器...
  3. 搜索Heltec ESP32并安装该软件包。

步骤3:从库管理器安装所需库

使用Arduino库管理器(项目 > 加载库 > 管理库...),搜索并安装以下库:

  1. Ai Esp32 Rotary Encoder:用于我们响应式键盘的专用库。
  2. Heltec ESP32 Dev-Boards:Heltec硬件的官方库。
  3. Adafruit GFX Library:OLED显示屏的依赖库。

步骤4:从.ZIP文件安装Robojax辅助库

如视频在18:53处所述,本项目使用了一个自定义辅助库。您必须从.zip文件安装它。

  1. 从资源页面提供的链接下载“Robojax HealthTech Lora 32”.zip库。
  2. 在Arduino IDE中,点击项目 > 加载库 > 添加.ZIP库...
  3. 导航到您保存下载的.zip文件的位置,选择它,然后点击“打开”。
  4. 该库将被安装并可供使用。

安装完所有内容后,请确保从菜单中选择正确的开发板:工具 > 开发板 > Heltec ESP32 Arduino > Heltec WiFi LoRa 32 V3


连接组件

您需要将排针焊接到Heltec开发板上,以便连接旋转编码器。如视频(11:25)所示,需要仔细规划,以确保开发板装入外壳后引脚仍然可访问。

Helte_Wifi_LoRA_Rotary_Encoder

图1:核心信使(仅旋转编码器)

将旋转编码器模块的5个引脚连接到Heltec开发板,连接方式如下:

  • 编码器上的GND → Heltec开发板上的GND
  • 编码器上的VCC → Heltec开发板上的3.3V
  • SW(开关) → GPIO 4
  • DT(数据/B引脚) → GPIO 6
  • CLK(时钟/A引脚) → GPIO 5
Helte_Wifi_LoRA_Rotary_Encoder with buzzer

图2:添加声音提示(蜂鸣器电路)

为了在新消息到达时发出蜂鸣声,您可以添加一个简单的晶体管电路。这是必要的,因为ESP32的引脚无法提供足够的电流直接驱动蜂鸣器。

  • 将Heltec开发板上的GPIO 40连接到1kΩ电阻的一端。
  • 将电阻的另一端连接到2N2222晶体管基极引脚。
  • 将晶体管的发射极引脚连接到GND
  • 将5V蜂鸣器的正极(+)引脚连接到Heltec开发板上的3.3V引脚。
  • 将蜂鸣器的负极(-)引脚连接到晶体管的集电极引脚。

理解并自定义代码

提供的代码可以直接上传,但有几个关键设置您需要了解,以便自定义项目。这些定义位于代码顶部。


// 物理引脚连接的定义
#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);
}

资源与参考

文件📁

其他文件