← 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