Home » Projects » Electronics Projects » NodeMCU Remote Control System Complete

NodeMCU Remote Control System Complete

NodeMCU remote control

Using NodeMCU remote control is a piece of cake.

Most NodeMCU remote control source code that you find on the Internet uses the ESP-12E as a Web Server. Shipping HTML around is pretty complicated. The software implemented here uses a simpler TCP/IP client server approach. You just define some commands and responses in plain ASCII. The software interprets the plain text as either a command to do something, or a request to provide some information.

Connecting with the ESP-12E can be done simply with any socket client, such as SocketTest. All you need is the IP address and port number for your NodeMCU. This source code runs the device as a Station on your wireless LAN. You can modify it to run with a Static IP address, or even as an Access Point separate from your LAN. If you want to use your Android phone to control the ESP-12E remotely, there are various TCP socket apps you can download.

This example of NodeMCU remote control operates two servos and a relay. You could easily modify this to run things like stepper motors, or read data from sensors, while maintaining the same overall architecture. Enjoy!

NodeMCU Remote Control Software

Here is the source code for simple hardware remote control. This can be used as a template or prototype to control many types of hardware, or read data from sensors. Hope this will help you implement your “Making It Up” dreams and ideas. This project was implemented using Visual Studio Community Edition, Visual Micro (the Arduino IDE plugin for Visual Studio) and ESP8266 Arduino Core.

/*
Second test program with the Arduino Core and NodeMCU.
Implements client-server remote control
for pan-tilt servos and switching a relay.
	- John VE6EY 2016 10 18 http://play.fallows.ca
Connect to ESP-12E IP on port 4000
Commands (ASCII terminated by \n\r)
-------------------------------------------------
+		Remote Control On
X		Remote Control Off
P=nnn	Move Pan Servo to nnn (0..180)
PU, PD	Move Pan Servo up or down one degree
P?		Read Pan Servo position (response: P=nnn)
T=nn	Move Tilt Servo to nn (0..90)
TU, TD	Move Tilt Servo up or down one degree
T?		Read Tilt Servo position (response: T=nn)
R=n		Switch Relay (On n=1, Off n=0)
R?		Read Relay Position (response: R=n)
-------------------------------------------------
*/

#include <Servo.h>
#include <WiFiServer.h>
#include <WiFiClient.h>
#include <ESP8266WiFi.h>

/* Command Defines */
#define WIFI_BUFFER_SIZE 128
#define COMMAND_COUNT 12
#define COMMAND_ERROR_NONE 0
#define COMMAND_ERROR_INVALID_COMMAND 1
#define COMMAND_ERROR_BAD_PARAMETER 2
#define COMMAND_ERROR_NOT_CONNECTED 3
#define REPORT_MON 100
#define REPORT_SER 101
#define REPORT_CLI 102

/* Servo Pins */
#define PIN_SERVO_PAN D6
#define PIN_SERVO_TILT D7

/* Servo Constants */
#define LIMIT_PAN_LOW 0
#define LIMIT_PAN_HIGH 180
#define LIMIT_TILT_LOW 0
#define LIMIT_TILT_HIGH 90
#define SERVO_PAN_MIN 650
#define SERVO_PAN_MAX 2400
#define SERVO_TILT_MIN 800
#define SERVO_TILT_MAX 2400
#define SERVO_TILT_OFFSET 50

/* Relay Pins */
#define PIN_RELAY D5

/* Global Objects and Variables */
const char* ssid = "XXXXXXXX";
const char* password = "XXXXXXXX";

WiFiServer server(4000);
WiFiClient client;
Servo servoPan;
Servo servoTilt;

int posPan;
int posTilt;
int pinPan = PIN_SERVO_PAN;
int pinTilt = PIN_SERVO_TILT;

int status = WL_IDLE_STATUS;
boolean ClientConnected = false;
boolean LastClientConnected = false;
boolean CommandConnected = false;
boolean RelayState = false;

/* Remote Operation Related Variables */
char wifiBuffer[WIFI_BUFFER_SIZE];
char* CommandStrings[] = { "+", "X", "P", "PU", "PD", "T", "TU", "TD", "R", "P?", "T?", "R?" };
enum CommandIDs { setControlOn, setControlOff, setPan, setPanUp, setPanDown, setTilt, setTiltUp, setTiltDown, setRelay, getPan, getTilt, getRelay };


void setup()
{
	/* Open serial port for debugging only. */
	Serial.begin(115200);
	delay(100);
	/* Connect NodeMCU to LAN. Provide IP Address over Serial port */
	WiFi.begin(ssid, password);
	while (WiFi.status() != WL_CONNECTED) {
		delay(250);
	}
	Serial.print("WiFi Connected at ");
	Serial.println(WiFi.localIP());

	/* Setup Servos */
	servoPan.attach(pinPan, SERVO_PAN_MIN, SERVO_PAN_MAX);
	servoTilt.attach(pinTilt, SERVO_TILT_MIN, SERVO_TILT_MAX);
	posPan = 20;
	SetPan(posPan);
	posTilt = 30;
	SetTilt(posTilt);
	delay(2000);

	/* Setup Relays*/
	pinMode(PIN_RELAY, OUTPUT);
	SetRelayState(false);

	/* Start Server */
	server.begin();
	delay(1000);
	Report(REPORT_MON, "Ready");
}

void loop()
{
	/* Check whether or not a client is connected once each loop */
	ChangeClientConnected(CheckClientConnection());

	if (ClientConnected && GetClientData())
	{
		yield();
		int m = ProcessCommands(wifiBuffer);

		yield();
		switch (m)
		{
		case COMMAND_ERROR_INVALID_COMMAND:
			Respond("! Invalid Command");
			break;
		case COMMAND_ERROR_BAD_PARAMETER:
			Respond("! Bad Parameter");
			break;
		case COMMAND_ERROR_NOT_CONNECTED:
			Respond("! No Control");
			break;
		default:
			Respond("OK");
			break;
		}
	}
	delay(10);
}

int ProcessCommands(char * commandBuffer)
{
	char * Command;
	char * Parameter;
	char * pch;
	unsigned long ParamValue = 0;
	unsigned long f;
	int i;
	int CommandIndex = -1;

	//* Parse command strings into Command and Param */
	pch = strchr(commandBuffer, '=');
	if (pch == NULL)
	{
		Command = strtok(commandBuffer, "");
		Parameter = NULL;
	}
	else
	{
		Command = strtok(commandBuffer, "=");
		Parameter = strtok(NULL, "=");
	}

	for (i = 0; i < COMMAND_COUNT; i++)
	{
		if (strcmp(Command, CommandStrings[i]) == 0) {
			CommandIndex = i;
			break;
		}

	}
	if (CommandIndex == -1) return COMMAND_ERROR_INVALID_COMMAND;
	if ((!CommandConnected) && (CommandIndex != setControlOn)) return COMMAND_ERROR_NOT_CONNECTED;
	if (IsNumeric(Parameter))
	{
		ParamValue = atol(Parameter);
	}
	if (!CheckCommandValue(CommandIndex, ParamValue))
	{
		return COMMAND_ERROR_BAD_PARAMETER;
	}
	yield();

	switch (CommandIndex)
	{
	case setControlOn:
		ChangeCommandConnected(true);
		break;

	case setControlOff:
		ChangeCommandConnected(false);
		break;

	case setPan:
		SetPan(ParamValue);
		break;

	case setPanUp:
		SetPan(posPan + 1);
		break;

	case setPanDown:
		SetPan(posPan - 1);
		break;

	case setTilt:
		SetTilt(ParamValue);
		break;

	case setTiltUp:
		SetTilt(posTilt + 1);
		break;

	case setTiltDown:
		SetTilt(posTilt - 1);
		break;

	case setRelay:
		if (ParamValue == 0)
		{
			SetRelayState(false);

		}
		else if (ParamValue == 1)
		{
			SetRelayState(true);
		}
		break;

	case getPan:
		Respond("P=" + String(servoPan.read(), DEC));
		break;

	case getTilt:
		Respond("T=" + String(servoTilt.read() - SERVO_TILT_OFFSET, DEC));
		break;

	case getRelay:
		if (RelayState == 1)
		{
			Respond("R=1");
		}
		else Respond("R=0");
		break;
	}

	return COMMAND_ERROR_NONE;
}

void ChangeClientConnected(boolean flag)
{
	/* Only process this routine when the ClientConnected state
	has actually changed. 	Otherwise, return immediately. */
	if (flag != LastClientConnected)
	{
		ClientConnected = flag;
		LastClientConnected = ClientConnected;
		if (ClientConnected)
		{
			Report(REPORT_MON, "Client Connected");
		}
		else
		{
			if (CommandConnected)
			{
				ChangeCommandConnected(false);
			}
			Report(REPORT_MON, "Client Disconnected");
		}
	}
}

void ChangeCommandConnected(boolean flag)
{
	CommandConnected = flag;
	if (CommandConnected)
	{
		Respond("Control On");
	}
	else Respond("Control Off");
}

boolean CheckClientConnection()
{
	/* If we have a running WiFiClient and there is a remote connection,
	just confirm the connection */
	if (client && client.connected())
	{
		return true;
	}

	/* If we have a running WiFiClient but the remote has disconnected,
	disable WiFiClient and report no connection */
	if (client && !client.connected())
	{
		client.stop();
		return false;
	}

	/* At this point we are ready for a new remote connection. 
	Create the WiFiClient and confirn the connection */
	if (server.hasClient())
	{
		if ((!client) || (!client.connected()))
		{
			if (client) client.stop();
			client = server.available();
			return true;
		}
	}
}

boolean CheckCommandValue(int index, long value)
{
	/* Basic error and range checking for command parameter values received */
	boolean retVal = true;
	switch (index)
	{
	case setPan:
		retVal = ((value >= LIMIT_PAN_LOW) && (value <= LIMIT_PAN_HIGH));
		break;

	case setPanDown:
		retVal = posPan > LIMIT_PAN_LOW;
		break;

	case setPanUp:
		retVal = posPan < LIMIT_PAN_HIGH;
		break;

	case setTilt:
		retVal = ((value >= LIMIT_TILT_LOW) && (value <= LIMIT_TILT_HIGH));
		break;

	case setTiltDown:
		retVal = posTilt > LIMIT_TILT_LOW;
		break;

	case setTiltUp:
		retVal = posTilt < LIMIT_TILT_HIGH;
		break;

	case setRelay:
		retVal = ((value == 0) || (value == 1));
		break;
	}
	return retVal;
}

boolean GetClientData()
{
	/* If the remote connection has sent data, read the data
	and put it into the WiFiBuffer. 	Tell the main loop that
	data is available for processing. Otherwise, return immediately and
	report that no data is available. */
	if (client.available())
	{
		String c = client.readStringUntil('\r');
		client.flush();
		c.toUpperCase();
		Report(REPORT_CLI, c);
		c.toCharArray(wifiBuffer, WIFI_BUFFER_SIZE);
		return true;
	}
	else
	{
		return false;
	}
}

boolean IsNumeric(const char * param)
{
	int counter = 0;
	if (param == NULL)
	{
		return false;
	}
	while (*param)
	{
		if (!isDigit(*param)) {
			if ((counter == 0) && (*param == '-')) {
				// ignore error if first character is "-"
			}
			else
			{
				return false;
			}
		}
		param++;
		counter++;
	}
	return true;
	/* Source: Peter Oakes. Modified to accept negative value. */
}

void Report(int flag, String value)
{
	/* Provides status on Serial port if active */
	String buf = "";
	if (Serial)
	{
		switch (flag)
		{

		case REPORT_SER:
			buf = ">" + value;
			break;
		case REPORT_CLI:
			buf = "<" + value;
			break;
		default:
			buf = value;
			break;
		}
		Serial.println(buf);
	}
}

void Respond(String value)
{
	/* Sends Server resonses to remote Client*/
	if (client)
	{
		client.println(value);
	}
	/* Mirror to Serial port if connected */
	Report(REPORT_SER, value);
}

void SetPan(int val)
{
	posPan = val;
	servoPan.write(posPan);
	delay(25);
}

void SetRelayState(boolean flag)
{
	RelayState = flag;
	if (RelayState)
	{
		digitalWrite(PIN_RELAY, HIGH);
	}
	else
	{
		digitalWrite(PIN_RELAY, LOW);
	}
}

void SetTilt(int val)
{
	posTilt = val;
	/* Offset normalizes Servo position to 0..90 true */
	servoTilt.write(posTilt + SERVO_TILT_OFFSET);
	delay(25);
}

4 comments

  1. Hi I would like to know more about the hardware setup you have used.
    I can’t quitre see what components you are using in your screenshot.

    Does the NodeMCU run on 3 volt logic?
    Do you have to what are you using to boost the voltage / current for the relay / steppers?

    • John VE6EY says:

      Hi Rupert. The circuit was shown in the previous post The relay was driven through a transistor. The servo motor control voltage was taken from NodeMCU digital pins. Yes, NodeMCU is 3.3V. This was enough to pulse the servo control,but the motors and relay/driver needed a separate 5V supply. Hope that answers your question. If you like, I can e-mail you the schematic. Cheers

    • John VE6EY says:

      At the end of the article is a black horizontal bar with a magnifying glass. When you hover your mouse over this bar, you will see “Show Source”. Just press down and the code will be displayed.

Leave a Reply