Skip to Content

← Control an ESP32 PLC from a Mobile App via Bluetooth LE

Hydraulic moving floor (BLE app)ESP32 PLC 38RBLECommunication

Control an ESP32 PLC from a Mobile App via Bluetooth LE — full example

Drive valves and read 4-20 mA sensors from a smartphone over BLE UART. Complete Arduino example for the industrial ESP32 PLC 38R, no network needed.

Complete, runnable program for the ESP32 PLC 38R (ble-mobile-app-control-38r.ino): wiring header, requirements and integration notes included.

Download the full project pack — freeThis example + the related ones + bill of materials

Read-only preview.

/*
 * COMPLETE EXAMPLE — Hydraulic equipment control from a mobile app over BLE
 *
 * Hardware:  ESP32 PLC 38R (Industrial Shields)
 * Based on:  hydraulic moving floor project, AppLink.cpp / main.cpp
 *
 * Wiring:
 *   I0_4  4-20 mA temperature sensor (5-90 degC)
 *   I0_5  4-20 mA pressure sensor (0-250 bar)
 *   R0_1  Main solenoid valve (EVG)
 *   R0_2  Unload solenoid valve (EVD)
 *   R0_3  Load solenoid valve (EVC)
 *
 * BLE protocol (Nordic UART service):
 *   PLC -> app (notify): "[T]23.5[P]120[C1]42" (temperature, pressure, counter)
 *   app -> PLC (write):  "[M]1*" load - "[M]2*" unload - "[M]0*" stop
 *
 * The mobile app (or any BLE scanner such as nRF Connect) connects with no
 * network needed: ideal for mobile equipment without infrastructure.
 */

#include 
#include 
#include 

#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHAR_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHAR_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

#define I_TEMP   I0_4
#define I_PRES   I0_5
#define R_EVG    R0_1
#define R_EVD    R0_2
#define R_EVC    R0_3

enum Motion { MOTION_NONE, MOTION_LOAD, MOTION_UNLOAD };
Motion motion = MOTION_NONE;
uint32_t counterLoad = 0;
bool deviceConnected = false;
BLECharacteristic *txChar;

// Conversion from 4-20 mA (0-1023 reading) to engineering units
float toTemperature(int raw) { return 5.0 + (raw / 1023.0) * 85.0; }   // 5-90 C
int   toPressure(int raw)    { return (int)((raw / 1023.0) * 250.0); } // 0-250 bar

void setMotion(Motion m) {
  motion = m;
  switch (m) {
    case MOTION_LOAD:                       // load: EVC + EVG open
      digitalWrite(R_EVC, HIGH); digitalWrite(R_EVD, LOW);
      digitalWrite(R_EVG, HIGH);
      counterLoad++;
      break;
    case MOTION_UNLOAD:                     // unload: EVD + EVG open
      digitalWrite(R_EVC, LOW);  digitalWrite(R_EVD, HIGH);
      digitalWrite(R_EVG, HIGH);
      break;
    default:                                // idle: everything closed
      digitalWrite(R_EVC, LOW);  digitalWrite(R_EVD, LOW);
      digitalWrite(R_EVG, LOW);
  }
}

// ------------------------------------------------- BLE callbacks
class ServerCB : public BLEServerCallbacks {
  void onConnect(BLEServer *s)    { deviceConnected = true; }
  void onDisconnect(BLEServer *s) { deviceConnected = false; s->startAdvertising(); }
};

class RxCB : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *c) {
    String cmd = String(c->getValue().c_str());      // e.g. "[M]1*"
    if (cmd.startsWith("[M]")) {
      int v = cmd.substring(3, cmd.indexOf('*')).toInt();
      setMotion(v == 1 ? MOTION_LOAD : v == 2 ? MOTION_UNLOAD : MOTION_NONE);
    }
  }
};

void setup() {
  Serial.begin(115200);
  pinMode(I_TEMP, INPUT); pinMode(I_PRES, INPUT);
  pinMode(R_EVG, OUTPUT); pinMode(R_EVD, OUTPUT); pinMode(R_EVC, OUTPUT);
  setMotion(MOTION_NONE);

  BLEDevice::init("PLC-HIDRAULICO");
  BLEServer *server = BLEDevice::createServer();
  server->setCallbacks(new ServerCB());

  BLEService *svc = server->createService(SERVICE_UUID);
  txChar = svc->createCharacteristic(CHAR_TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
  txChar->addDescriptor(new BLE2902());
  BLECharacteristic *rxChar =
      svc->createCharacteristic(CHAR_RX_UUID, BLECharacteristic::PROPERTY_WRITE);
  rxChar->setCallbacks(new RxCB());

  svc->start();
  server->getAdvertising()->start();
  Serial.println("BLE advertising as PLC-HIDRAULICO");
}

void loop() {
  // Telemetry every second while an app is connected
  static uint32_t tLast = 0;
  if (deviceConnected && millis() - tLast > 1000) {
    tLast = millis();
    float temp = toTemperature(analogRead(I_TEMP));
    int   pres = toPressure(analogRead(I_PRES));

    String frame = "[T]" + String(temp, 1) +
                   "[P]" + String(pres) +
                   "[M]" + String((int)motion) +
                   "[C1]" + String(counterLoad);
    txChar->setValue(frame.c_str());
    txChar->notify();
  }

  // Safety: pressure out of range -> immediate stop
  if (motion != MOTION_NONE && toPressure(analogRead(I_PRES)) > 230) {
    setMotion(MOTION_NONE);
    if (deviceConnected) { txChar->setValue("[E]SOBREPRESION"); txChar->notify(); }
  }

  delay(50);
}
Download the full project pack — freeThis example + the related ones + bill of materials