Tìm kiếm mã

Điều khiển động cơ Servo từ xa hàng dặm! Hướng dẫn Arduino Heltec WiFi LoRa 32 V3 (TX)

Bài học này là một phần của: Giới thiệu về WiFi LoRa

Điều khiển động cơ Servo từ xa hàng dặm! Hướng dẫn Arduino Heltec WiFi LoRa 32 V3 (TX)

Trong hướng dẫn này, chúng tôi sử dụng chính các bản phác thảo từ dự án servo Heltec ESP32 LoRa V3 của mình và giải thích cách chúng hoạt động—không thêm mã nào. Bạn sẽ học được cách bộ phát đọc bộ mã hóa xoay, bảo mật và gửi góc đó qua LoRa, cũng như cách bộ thu giải mã và điều khiển một micro-servo. Tất cả các liên kết linh kiện và mã đều ở bên dưới, và nếu bạn đặt hàng qua các liên kết liên kết của chúng tôi, điều đó sẽ giúp chúng tôi tiếp tục tạo ra các hướng dẫn này.

 

Cài đặt bo mạch Heltec ESP32

Thêm đường dẫn này vào tùy chọn của Arduino IDE như trong video:https://resource.heltec.cn/download/package_heltec_esp32_index.json

1. Phần cứng & thiết lập bộ phát (TX)

Ở phía TX bạn cần:

  • Bo mạch Heltec WiFi LoRa 32 V3 (trong vỏ Meshnology N33, được cấp nguồn bằng pin 3000 mAh)

  • Bộ mã hóa xoay được nối với GPIO 6 (CLK), GPIO 5 (DT), GPIO 4 (SW)

  • Màn hình OLED trên I²C (SDA= 4, SCL= 15)

Bản phác thảo bắt đầu bằng việc bao gồm và khởi tạo mọi thứ chính xác như trong Heltec_ESP32_LoRa_V3_Sevo_TX_AiRotaryEncoder.ino:

cppCopyEdit#include "AiEsp32RotaryEncoder.h"
#include "HT_SSD1306Wire.h"
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
// …
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY, RST_OLED);
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(
    PIN_A, PIN_B, SW_PIN, ROTARY_ENCODER_VCC_PIN, false, true, true);
const int homePosition = 90;
const int MAX_ANGLE    = 180;
int servoAngel = homePosition;


Trong setup(), mã sẽ:

  • Bật nguồn màn hình, đặt phông chữ

  • Gọi rotaryEncoder.begin(), rotaryEncoder.setup(readEncoderISR), rotaryEncoder.setBoundaries(0, MAX_ANGLE, true)rotaryEncoder.setAcceleration(20)

  • Đặt lại bộ mã hóa về homePosition

  • Khởi tạo LoRa qua Mcu.begin(HELTEC_BOARD, SLOW_CLK_TPYE) và thiết lập RadioEvents, kênh, và các tham số chính xác như trong bản phác thảo được cung cấp.

2. Gửi góc một cách an toàn

Mỗi chu kỳ vòng lặp chạy rotary_loop(), hàm này:

  • Đọc bộ mã hóa trong ISR

  • Khi servoAngel thay đổi, đóng gói nó vào bộ đệm 16 byte, mã hóa bằng AES-128 (encryptAES() từ bản phác thảo), và gọi

    cppCopyEditRadio.Send(data, sizeof(data));
    
    
  • Đặt lora_idle = false cho đến khi OnTxDone() kích hoạt và đặt lại nó.

3. Phần cứng & thiết lập bộ thu (RX)

Ở phía RX bạn cần:

  • Bo mạch Heltec WiFi LoRa 32 V3 (cùng vỏ/pin)

  • Micro-servo (ví dụ: SG90) trên GPIO 6 (hoặc bất kỳ chân PWM nào đã kiểm tra)

  • Màn hình OLED

Bản phác thảo trong Heltec_ESP32_LoRa_V3_Sevo_RX.ino bắt đầu với:

cppCopyEdit#include <ESP32Servo.h>
#include "HT_SSD1306Wire.h"
#include "LoRaWan_APP.h"
#include "mbedtls/aes.h"
// …
const int servoPin       = 6;
const int SERVO_DUTY_MIN = 400;  // us
const int SERVO_DUTY_MAX = 2400; // us
Servo    myservo;
int      servoAngel     = homePosition;


Trong setup(), nó:

  • Bật nguồn Vext cho màn hình/mô-đun LoRa (VextON())

  • Gọi Radio.Init(&RadioEvents) và cấu hình RX với cùng các tham số LoRa

  • Gắn servo bằng myservo.attach(servoPin, SERVO_DUTY_MIN, SERVO_DUTY_MAX) và đặt nó ở giữa tại homePosition.

4. Nhận, giải mã và điều khiển servo

Phần cốt lõi là callback OnRxDone(uint8_t *payload, …):

cppCopyEditdecryptAES((uint8_t*)rxpacket, userKey);
if (isNumber(rxpacket)) {
  servoAngel = atoi(rxpacket);
  myservo.write(servoAngel);
  delay(15);
}
Serial.println("Angle: " + String(servoAngel));
lora_idle = true;


Nó giải mã khối 16 byte, chuyển đổi thành số nguyên và ngay lập tức cập nhật servo.

5. Hỗ trợ chân PWM & tinh chỉnh servo

Chúng tôi đã kiểm tra các chân ESP32 này để xuất PWM và tất cả đều hoạt động để điều khiển micro-servo:

CopyEdit1, 2, 3, 4, 5, 6, 19, 35, 36, 38, 39, 40, 41, 42, 45, 47, 48


Đối với SG90 tiêu chuẩn, mã của chúng tôi sử dụng dải xung từ 400 µs (0°) đến 2400 µs (180°), mang lại chuyển động mượt mà, quét đầy đủ mà không bị giật.

6. Sơ đồ đấu dây

Dưới đây là các vị trí giữ chỗ nơi bạn có thể chèn sơ đồ TX và RX của mình:

Helte_Wifi_LoRA_Rotary_Encoder
Helte_Wifi_LoRA with battery

Mã & Liên kết liên kết

Tất cả các bản phác thảo trên đều có sẵn để tải xuống trong phần “Mã & Tài nguyên” bên dưới. Nếu bạn muốn tự xây dựng thiết bị này, vui lòng cân nhắc mua mô-đun Heltec LoRa32 V3, vỏ Meshnology N33, bộ mã hóa xoay và servo SG90 qua các liên kết liên kết của chúng tôi. Điều này không tốn thêm chi phí cho bạn và giúp chúng tôi tiếp tục tạo ra các hướng dẫn miễn phí như thế này!


Chương Video để Tham khảo

  • 00:00 Giới thiệu & Tổng quan

  • 00:05 Khái niệm Điều khiển Từ xa

  • 00:19 Kiến thức cơ bản về Truyền thông LoRa

  • 00:23 Xem trước Phần cứng

  • 00:28 Trình diễn Vỏ & Pin

  • 01:03 Tính năng Mô-đun

  • 01:42 Thông số kỹ thuật & Kết nối

  • 02:54 Cấp nguồn cho Servo

  • 03:05 Đấu dây & Sơ đồ chân

  • 09:35 Vị trí Đặt Ăng-ten

  • 11:04 Lắp ráp Vỏ

  • 29:26 Tải lên Các bản phác thảo

  • 35:09 Kiểm tra Phạm vi 1,2 km

  • 36:38 Kiểm tra Phạm vi 1,4 km

  • 38:41 Tóm tắt Hiệu suất

  • 43:04 Kết luận & Hỗ trợ

775-Secure LoRa Servo Angle Transmitter (TX) with Rotary Encoder - Heltec V3
Ngôn ngữ: C++
/*
File: Heltec_ESP32_LoRa_V3_Sevo_TX_AiRotaryEncoder.ino
written on 24 Jun, 2025 by Ahmad Shamshiri

 * =====================================================================
 * ARDUINO CODE DESCRIPTION: SECURE LoRa SERVO CONTROL SYSTEM (TX)
 * =====================================================================
 * 
 * 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
 *  - Output: Servo motor + LoRa wireless transmission
 * 
 * SYSTEM FUNCTIONALITY:
 * -------------------
 * [1] ROTARY ENCODER CONTROL:
 *     - Clockwise/Counter-clockwise rotation adjusts target angle (0°-180°)
 *     - Real-time angle display on OLED screen
 *     - Push-button returns servo to Home position (default: 90°)
 * 
 * [2] SECURE WIRELESS TRANSMISSION:
 *     - All angle values encrypted before LoRa transmission
 *     - Home position command transmitted as special secure packet
 *     - Uses 433MHz 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/EPynuJ7sasY
 * =====================================================================

Watch full video explaination:  https://youtu.be/EPynuJ7sasY
Resources page: https://robojax.com/T635


 * 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
 * 
 * Original tutorial at: https://robojax.com/T635
 * YouTube Video: https://youtu.be/EPynuJ7sasY
 * 
 * ********************************************************************
 */

#include <Wire.h>               
#include "HT_SSD1306Wire.h"
#include "WiFi.h"
static SSD1306Wire  display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED); // addr , freq , i2c group , resolution , rst


const int TX_POWER = 2;//dBm from 2 to 20. when powered via battery 2 to 14dBm is the best option
const int MAX_ANGLE = 180;//the most common is 180, but you can set it as needed

String labelAngle = "Angle";
const int homePosition = 90; //initial position


//endcoder
const int SW_PIN = 4;//define a pin for rotary encode switch
const int PIN_A  = 6;
const int PIN_B  = 5;//
const int ANGLE_STEP  = 6;//
const bool debug= false;//to print debug data in serial moinitor set it to true, else false

int servoAngel = homePosition;
int oldAngleValue = servoAngel;
#include "mbedtls/aes.h"//for securing data
#include <cstring>  // For memset, memcpy
mbedtls_aes_context aes;
const char *userKey = "hyhT676#h~_1a"; //Security key. 


#include "LoRaWan_APP.h"
#include "AiEsp32RotaryEncoder.h"
#include "Arduino.h"
#define ROTARY_ENCODER_VCC_PIN -1

//instead of changing here, rather change numbers above
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(
            PIN_A, 
            PIN_B, 
            SW_PIN, 
            ROTARY_ENCODER_VCC_PIN, 
            ANGLE_STEP);



#define RF_FREQUENCY                                915432000 // Hz

#define TX_OUTPUT_POWER                             TX_POWER        // dBm from 2 to 20. when powered via battery 2 to 14dBm

#define LORA_BANDWIDTH                              0         // [0: 125 kHz,
                                                              //  1: 250 kHz,
                                                              //  2: 500 kHz,
                                                              //  3: Reserved]
#define LORA_SPREADING_FACTOR                       7         // [SF7..SF12]
#define LORA_CODINGRATE                             1         // [1: 4/5,
                                                              //  2: 4/6,
                                                              //  3: 4/7,
                                                              //  4: 4/8]
#define LORA_PREAMBLE_LENGTH                        8         // Same for Tx and Rx
#define LORA_SYMBOL_TIMEOUT                         0         // Symbols
#define LORA_FIX_LENGTH_PAYLOAD_ON                  false
#define LORA_IQ_INVERSION_ON                        false


#define RX_TIMEOUT_VALUE                            1000
#define BUFFER_SIZE                                 64 // Define the payload size here

char txpacket[BUFFER_SIZE];
char rxpacket[BUFFER_SIZE];

double txNumber;

bool lora_idle=true;

static RadioEvents_t RadioEvents;
unsigned long lastTxTime = 0;
void OnTxDone( void );
void OnTxTimeout( void );
void decryptAES(uint8_t *data, const char *key);
void encryptAES(uint8_t *data, const char *key);
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize);
void VextON(void);

void rotary_loop();//prototyp function: rotary encoder
void IRAM_ATTR readEncoderISR();//prototyp function: rotary encoder
void rotary_onButtonClick();//prototyp function: rotary encoder

void setup() {
  Serial.begin(115200);
  Serial.println();

  VextON();
  delay(100);

	//we must initialize rotary encoder
	rotaryEncoder.begin();
	rotaryEncoder.setup(readEncoderISR);
	bool circleValues = false;
	rotaryEncoder.setBoundaries(0, MAX_ANGLE, circleValues); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
	/*Rotary acceleration introduced 25.2.2021.
   * in case range to select is huge, for example - select a value between 0 and 1000 and we want 785
   * without accelerateion you need long time to get to that number
   * Using acceleration, faster you turn, faster will the value raise.
   * For fine tuning slow down.
   */
	//rotaryEncoder.disableAcceleration(); //acceleration is now enabled by default - disable if you dont need it
	rotaryEncoder.setAcceleration(20); //or set the value - larger number = more accelearation; 0 or 1 means disabled acceleration
  rotaryEncoder.reset(homePosition); //set home position

  // Initialising the UI will init the display too.
  display.init();
  display.setFont(ArialMT_Plain_10);
  //LoRa stuff
  Mcu.begin(HELTEC_BOARD,SLOW_CLK_TPYE);
	
    txNumber=0;

    RadioEvents.TxDone = OnTxDone;
    RadioEvents.TxTimeout = OnTxTimeout;
    
    Radio.Init( &RadioEvents );
    Radio.SetChannel( RF_FREQUENCY );
    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 );   

}


void displayAngle() {
    display.clear();  // Clear display before new content
    
    // Line 1: Text: Angle
    display.setTextAlignment(TEXT_ALIGN_LEFT);

    // Line 2: Temperature value in 24pt font
    display.setFont(ArialMT_Plain_24);
    
    // Format
    String angleString = String(servoAngel) + "°"; // 

    display.setFont(ArialMT_Plain_16);
    display.drawString(0, 0, labelAngle);        
    display.setFont(ArialMT_Plain_24);
    display.drawString(0, 15, angleString);  

    display.display();  // Update OLED
}



void VextON(void)
{
  pinMode(Vext,OUTPUT);
  digitalWrite(Vext, LOW);
}

void VextOFF(void) //Vext default OFF
{
  pinMode(Vext,OUTPUT);
  digitalWrite(Vext, HIGH);
}

void sendData()
{

  String txData = String(servoAngel) ; 

  uint8_t data[BUFFER_SIZE];       
  memset(data, 0, sizeof(data));  // Zero-padding
  strncpy((char*)data, txData.c_str(), sizeof(data) - 1); // Copy string safely

  encryptAES(data, userKey);  // Encrypt before sending  
  if(lora_idle == true)
    {
      //delay(1000);
      Radio.Send(data,  sizeof(data));
      if(debug){
      Serial.print("Sending: ");
      Serial.println((char *)data);
      }
      lora_idle = false;
      oldAngleValue =servoAngel;//keep record of angle change
    }
    Radio.IrqProcess( );  
}



void loop() {
  rotary_loop();
  // clear the display
  display.clear();

  displayAngle(); // 

  if(oldAngleValue != servoAngel)
  {
	  sendData();
  }
  //delay(100);

}


void OnTxDone( void )
{
        if(debug){
	        Serial.println("TX done......");
        }
	lora_idle = true;
}

void OnTxTimeout( void )
{
    Radio.Sleep( );
        if(debug){
	        Serial.println("TX Timeout......");
        }
    lora_idle = true;
}


/**
 * Converts a user-provided plaintext key into a fixed-length 16-byte (128-bit)
 * or 32-byte (256-bit) key.
 */
void processKey(const char *userKey, uint8_t *processedKey, size_t keySize) {
    memset(processedKey, 0, keySize); // Fill with zeros
    size_t len = strlen(userKey);
    if (len > keySize) len = keySize; // Truncate if too long
    memcpy(processedKey, userKey, len); // Copy valid key part
}

/**
 * Encrypts a 16-byte (one block) message using AES-128.
 */
void encryptAES(uint8_t *data, const char *key) {
    uint8_t processedKey[16]; // 128-bit key
    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);
}

/**
 * Decrypts a 16-byte (one block) message using AES-128.
 */
void decryptAES(uint8_t *data, const char *key) {
    uint8_t processedKey[16]; // 128-bit key
    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);
}



void rotary_onButtonClick()
{
	static unsigned long lastTimePressed = 0;
	//ignore multiple press in that time milliseconds
	if (millis() - lastTimePressed < 500)
	{
		return;
	}
	lastTimePressed = millis();
  
	        if(debug){
	          Serial.print("button pressed ");
	          Serial.print(millis());
	          Serial.println(" milliseconds after restart");
          }
}

void rotary_loop()
{
	//dont print anything unless value changed
	if (rotaryEncoder.encoderChanged())
	{
		        if(debug){
	            Serial.print("Value: ");
		          Serial.println(rotaryEncoder.readEncoder());              
            }
    servoAngel = rotaryEncoder.readEncoder();

	}
	if (rotaryEncoder.isEncoderButtonClicked())
	{
    rotaryEncoder.reset(homePosition); 
    servoAngel = homePosition;
		rotary_onButtonClick();
	}
}

void IRAM_ATTR readEncoderISR()
{
	rotaryEncoder.readEncoder_ISR();
}

Tài nguyên & tài liệu tham khảo

Tập tin📁

Thư viện Arduino (tệp nén)