edge lab LIVE! starts Tuesday in Munich. Grab a workshop seat and take home the board you build on. Code FRIEND-HACKSTER saves 20%. () Learn more!
roboattic Lab
Published © GPL3+

Custom Long Range LoRa Controller From Scratch

DIY long-range RC controller using RYLR998 and Arduino Nano. Controls a Drone and RC Car from a 10 km range.

IntermediateFull instructions provided10 hours732
Custom Long Range LoRa Controller From Scratch

Things used in this project

Story

Read more

Custom parts and enclosures

Base

Lid

Schematics

Circuit Diagram

Code

Transmitter Code

C/C++
/*
 * ============================================================
 *  Code by: Shahbaz Hashmi Ansari
 *  LoRa RC TRANSMITTER β€” Arduino Nano
 *  Reyax RYLR998 LoRa Module (915MHz, AT Commands)
 *  Dual Analog Joystick + 0.96" SH1106 OLED (I2C)
 *  3D Printed Custom Enclosure + 2x 18650 Battery
 * ============================================================
 */

#include <SoftwareSerial.h>
#include <U8g2lib.h>
#include <Wire.h>

SoftwareSerial loraSerial(2, 3);
U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

#define JOY_L_X    A0   
#define JOY_L_Y    A1  
#define JOY_R_X    A2
#define JOY_R_Y    A3
#define JOY_L_BTN   4
#define JOY_R_BTN   5
#define DEAD_ZONE  60

unsigned long lastSend    = 0;
unsigned long lastDisplay = 0;
unsigned long txCount     = 0;
bool          loraOK      = false;
char          payload[32];
char          loraCmd[64];
char          buf[22];

void sendAT(const char* cmd, int waitMs = 300) {
  loraSerial.println(cmd);
  delay(waitMs);
  while (loraSerial.available()) loraSerial.read();
}

bool sendATCheck(const char* cmd, int waitMs = 500) {
  loraSerial.println(cmd);
  delay(waitMs);
  char resp[20] = "";
  int i = 0;
  while (loraSerial.available() && i < 19)
    resp[i++] = loraSerial.read();
  return strstr(resp, "+OK") != NULL;
}

const char* getDir(int x, int y) {
  bool fwd  = y < (512 - DEAD_ZONE);
  bool bwd  = y > (512 + DEAD_ZONE);
  bool lft  = x < (512 - DEAD_ZONE);
  bool rgt  = x > (512 + DEAD_ZONE);
  if (fwd && lft)  return "FWD-L";
  if (fwd && rgt)  return "FWD-R";
  if (bwd && lft)  return "BWD-L";
  if (bwd && rgt)  return "BWD-R";
  if (fwd)         return "FWD";
  if (bwd)         return "BWD";
  if (lft)         return "LEFT";
  if (rgt)         return "RIGHT";
  return "IDLE";
}

// ── Splash ──
void showSplash() {
  u8g2.firstPage();
  do {

    u8g2.drawRBox(0, 0, 128, 26, 4);
    u8g2.setDrawColor(0);
    u8g2.setFont(u8g2_font_7x13B_tr);
    u8g2.drawStr(8, 10, "roboattic Lab");
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(22, 22, "x  REYAX");
    u8g2.setDrawColor(1);

    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(10, 38, "LoRa RC Controller");
    u8g2.drawStr(22, 50, "RYLR998 Module");

    u8g2.drawHLine(0, 55, 128);
    u8g2.setFont(u8g2_font_5x7_tr);
    u8g2.drawStr(18, 63, "Initializing...");
  } while (u8g2.nextPage());
  delay(2500);
}

// ── LoRa Status Screen ──
void showLoraStatus() {
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(0, 10, "Module Status");
    u8g2.drawHLine(0, 13, 128);

    u8g2.setFont(u8g2_font_6x10_tr);
    if (loraOK) {
      u8g2.drawStr(0, 26, "\x10 RYLR998  [OK]");
    } else {
      u8g2.drawStr(0, 26, "! RYLR998  [FAIL]");
    }
    u8g2.drawStr(0, 38, "  Addr : 1");
    u8g2.drawStr(0, 48, "  Net  : 6");
    u8g2.drawStr(0, 58, "  Band : 915 MHz");
  } while (u8g2.nextPage());
  delay(2000);
}

// ── Draw mini joystick crosshair ──
void drawJoyCross(int cx, int cy, int jx, int jy) {
  u8g2.drawFrame(cx, cy, 14, 14);
  u8g2.drawHLine(cx, cy + 7, 14);  
  u8g2.drawVLine(cx + 7, cy, 14);  
  int dx = map(jx, 0, 1023, 1, 13);
  int dy = map(jy, 0, 1023, 1, 13);
  u8g2.drawBox(cx + dx - 1, cy + dy - 1, 3, 3);
}

// ── Main HUD β€” Clean product-like UI ──
void updateHUD(int speed, int steer, const char* lDir, const char* rDir,
               bool lBtn, bool rBtn, int rawLX, int rawLY, int rawRX, int rawRY) {
  u8g2.firstPage();
  do {
    u8g2.drawBox(0, 0, 128, 11);
    u8g2.setDrawColor(0);
    u8g2.setFont(u8g2_font_5x7_tr);
    u8g2.drawStr(2, 8, "LORA RC TX");
    snprintf(buf, sizeof(buf), "#%lu", txCount);
    u8g2.drawStr(128 - (strlen(buf) * 5) - 2, 8, buf);
    u8g2.setDrawColor(1);
    u8g2.drawVLine(64, 11, 42);

    // ══════ LEFT PANEL β€” DRIVE ══════
    u8g2.setFont(u8g2_font_5x7_tr);
    u8g2.drawStr(2, 20, "DRIVE");
    u8g2.drawHLine(2, 22, 26);

    // Direction label (bold)
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(2, 33, lDir);

    // Speed bar
    u8g2.setFont(u8g2_font_5x7_tr);
    u8g2.drawStr(2, 42, "SPD");
    u8g2.drawFrame(20, 36, 26, 7);
    u8g2.drawBox(20, 36, map(speed, 0, 100, 0, 26), 7);
    snprintf(buf, sizeof(buf), "%d%%", speed);
    u8g2.drawStr(48, 42, buf);

    // Mini joystick visualizer (left)
    drawJoyCross(48, 18, rawLX, rawLY);

    // ══════ RIGHT PANEL β€” STEER ══════
    u8g2.setFont(u8g2_font_5x7_tr);
    u8g2.drawStr(68, 20, "STEER");
    u8g2.drawHLine(68, 22, 30);

    // Direction label
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(68, 33, rDir);

    // Steer bar (center-origin: left/right from midpoint)
    u8g2.setFont(u8g2_font_5x7_tr);
    u8g2.drawStr(68, 42, "STR");
    u8g2.drawFrame(86, 36, 26, 7);
    u8g2.drawVLine(99, 36, 7);  // center mark
    if (steer > 0)
      u8g2.drawBox(99, 36, map(steer, 0, 100, 0, 13), 7);
    else if (steer < 0)
      u8g2.drawBox(99 + map(steer, 0, -100, 0, -13), 36,
                   map(abs(steer), 0, 100, 0, 13), 7);

    // Mini joystick visualizer (right)
    drawJoyCross(114, 18, rawRX, rawRY);

    // ══════ BOTTOM STATUS BAR ══════
    u8g2.drawHLine(0, 53, 128);

    u8g2.setFont(u8g2_font_5x7_tr);
    // Button indicators
    u8g2.drawStr(2, 63, "L");
    if (lBtn)
      u8g2.drawBox(10, 56, 8, 7);
    else
      u8g2.drawFrame(10, 56, 8, 7);

    u8g2.drawStr(22, 63, "R");
    if (rBtn)
      u8g2.drawBox(30, 56, 8, 7);
    else
      u8g2.drawFrame(30, 56, 8, 7);

    // TX indicator
    u8g2.drawStr(100, 63, "TX");
    u8g2.drawDisc(118, 59, 3); 
  } while (u8g2.nextPage());
}

void setup() {
  Serial.begin(9600);
  loraSerial.begin(9600);

  pinMode(JOY_L_BTN, INPUT_PULLUP);
  pinMode(JOY_R_BTN, INPUT_PULLUP);

  u8g2.begin();
  showSplash();

  Serial.println(F("================================="));
  Serial.println(F(" LoRa RC TX - roboattic Lab"));
  Serial.println(F("================================="));
  delay(500);

  loraOK = sendATCheck("AT");
  Serial.println(loraOK ? F("RYLR998: OK!") : F("RYLR998: FAIL!"));

  sendAT("AT+ADDRESS=1");
  sendAT("AT+NETWORKID=6");
  sendAT("AT+BAND=915000000");
  sendAT("AT+CRFOP=22");

  Serial.println(F("Addr:1 Net:6 915MHz 22dBm"));

  showLoraStatus();
  Serial.println(F("Transmitting..."));
}

void loop() {
  analogRead(JOY_L_X); delay(1); int rawLX = analogRead(JOY_L_X);
  analogRead(JOY_L_Y); delay(1); int rawLY = analogRead(JOY_L_Y);
  analogRead(JOY_R_X); delay(1); int rawRX = analogRead(JOY_R_X);
  analogRead(JOY_R_Y); delay(1); int rawRY = analogRead(JOY_R_Y);

  bool lBtn = (digitalRead(JOY_L_BTN) == LOW);
  bool rBtn = (digitalRead(JOY_R_BTN) == LOW);

  int corrLX = 1023 - rawLY;  
  int corrLY = 1023 - rawLX;   
  int corrRX = 1023 - rawRY;
  int corrRY = 1023 - rawRX;

  int lxOut = map(corrLX, 0, 1023, 0, 255);
  int lyOut = map(1023 - corrLY, 0, 1023, 0, 255); 
  int rxOut = map(corrRX, 0, 1023, 0, 255);
  int ryOut = map(1023 - corrRY, 0, 1023, 0, 255);

  const char* lDir = getDir(corrLX, corrLY);
  const char* rDir = getDir(corrRX, corrRY);

  int speed = map(abs(lyOut - 127), 0, 127, 0, 100);
  int steer = map(rxOut, 0, 255, -100, 100);

  if (millis() - lastSend >= 150) {
    lastSend = millis();
    txCount++;

    snprintf(payload, sizeof(payload), "%d,%d,%d,%d,%d,%d",
             lxOut, lyOut, rxOut, ryOut,
             lBtn ? 1 : 0, rBtn ? 1 : 0);

    snprintf(loraCmd, sizeof(loraCmd), "AT+SEND=2,%d,%s",
             strlen(payload), payload);

    while (loraSerial.available()) loraSerial.read(); 
    loraSerial.println(loraCmd);

    if (txCount % 5 == 0) {
      Serial.print(F("TX >> ")); Serial.println(payload);
    }
  }

  // ── Update OLED every 150ms ──
  if (millis() - lastDisplay >= 150) {
    lastDisplay = millis();
    updateHUD(speed, steer, lDir, rDir, lBtn, rBtn,
              corrLX, corrLY, corrRX, corrRY);
  }
}

Credits

roboattic Lab
28 projects β€’ 20 followers
YouTube Content CreatorRobotics Enthusiast

Comments