TCPServer example using SYSTEM_MODE(MANUAL)

There was recently a question about adapting the TCPServer telnet example code for use in MANUAL mode so your own code could run even when the network goes away. I wrote a little sample program that does this, and also instruments the amount of time spent outside of loop(), inside of our own code, and in Particle.process() so you can play around with it and see how system thread modes affect these things, too. The code is really designed so you can play around with it, disconnect the Wi-Fi or Internet, etc. and see what happens, more than definitive set of best practices for manual mode.

// Manual System Mode Telnet Example
#include "Particle.h"

SYSTEM_MODE(MANUAL);

// System Thread enabled is not required for using manual mode. However, if you have code that needs to
// run constantly you should use it, even with manual mode. The reason is that certain operations when
// connecting to the cloud will block either outside of loop(), so your loop() won't be called for 20
// seconds or longer) or in Particle.process (normally very quick, it might take 2-3 seconds). These
// delays go away when the System thread is used so your loop() will be called very regularly.
SYSTEM_THREAD(ENABLED);

TCPServer server = TCPServer(23);
TCPClient client;

enum State { PARTICLE_CONNECT, PARTICLE_CONNECT_WAIT, SERVER_CONNECT_WAIT, SERVER_HANDLE_CLIENT };
State state = PARTICLE_CONNECT;
unsigned long stateTime = 0;
String localIP;
unsigned long lastLoopExit = 0;

// This sample code will output a message via serial if it takes longer than timeWarnMs in one of these cases:
// 1. Outside of loop - it takes an unusually long time before your loop is called called again
// 2. Your code inside loop
// 3. Time to make the Particle.process call
const unsigned long timeWarnMs = 100;

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

	// You can put Particle.variable and Particle.function calls here.

	// From CLI, use something like:
	// particle get test5 localip
	// to get the IP address of the Photon (replace "test5" with your device name)
	// Important note: localIP is set below, in the PARTICLE_CONNECT_WAIT handler
	// because we don't know our localIP until WiFi is up. If you were to call
	// WiFi.localIP() here in manual or semi-automatic mode, it would return 0.0.0.0.
	Particle.variable("localip", localIP);


	// Do not put Particle.publish calls here, put them
	// below in the handler for the PARTICLE_CONNECT_WAIT state because the cloud
	// needs to be active
}

void loop() {
	unsigned long enterLoop = millis();

	if ((lastLoopExit != 0) && ((enterLoop - lastLoopExit) > timeWarnMs)) {
		Serial.printlnf("%ld ms since last loop call", (enterLoop - lastLoopExit));
	}

	if (Particle.connected()) {
		switch(state) {
		case PARTICLE_CONNECT_WAIT:
			Serial.println("connected to cloud!");
			state = SERVER_CONNECT_WAIT;

			// This is actually known a little earlier, when WiFi is connected, but we don't explicitly
			// check for that in this sample code.
			localIP = WiFi.localIP();

			// If you want to subscribe to an event or publish some data, it's safe to do so here.
			// Also, a good place to subscribe to a multicast UDP.

			// It appears you must server.stop() and server.begin() if you lose your Wi-Fi connection.
			server.begin();
			break;

		case SERVER_CONNECT_WAIT:
			if (client.connected()) {
				// A TCP client has connected to the server.
				state = SERVER_HANDLE_CLIENT;
				stateTime = millis();
			}
			else {
				// Check for an incoming connection. This is called repeatedly until a connection is made.
				client = server.available();
			}
			break;

		case SERVER_HANDLE_CLIENT:
			if (client.connected()) {
				int count = 0;

				// Echo bytes back to the client while we have input bytes, but not too many.
				// (If there are lots of bytes outstanding we want to process them in chunks to
				// avoid starving the rest of the system by spending too much time in the loop)
				while (client.available() && count++ < 128) {
					int c = client.read();

					// By the way, the actual telnet program will sent a bunch of telnet escape sequences
					// which appears as random garbage characters in serial. This isn't really a bug,
					// it's just that in the interest of clarity this isn't really a telnet server.
					client.write(c);
					Serial.write(c);
				}

				if (count > 0) {
					// We received bytes, reset the timeout counter
					stateTime = millis();
				}
				else {
					// If we don't receive any bytes in 15 seconds, disconnect the client
					// In a real program you'd probably make this timeout much longer.
					if (millis() - stateTime > 15000) {
						Serial.println("client timeout");
						client.stop();
						state = SERVER_CONNECT_WAIT;
					}
				}
			}
			else {
				// Disconnected
				Serial.println("client disconnected");
				client.stop();
				state = SERVER_CONNECT_WAIT;
			}
			break;
		}
	}
	else {
		// Particle.connected() is false here

		switch(state) {
		case PARTICLE_CONNECT:
			// Not connected to the cloud, either we just started up or the connection
			// was broken and we need to reconnect
			Serial.println("attempting to connect");
			Particle.connect();
			stateTime = millis();
			state = PARTICLE_CONNECT_WAIT;
			break;

		case PARTICLE_CONNECT_WAIT:
			if (millis() - stateTime > 60000) {
				// Allow 60 seconds for connecting; if we fail to connect, try again
				Serial.println("failed to connect");
				state = PARTICLE_CONNECT;
			}
			break;

		default:
			Serial.println("cloud connection lost, retry connect");

			// Important: If you're using a server socket, be sure to stop and begin it again
			// otherwise you won't be able to make a new connection to the server after losing
			// your network connection.
			client.stop();
			server.stop();
			state = PARTICLE_CONNECT;
			break;
		}
	}

	// Put your code that needs to run whether you're connected or not here

	if ((millis() - enterLoop) > timeWarnMs) {
		Serial.printlnf("%ld ms spent in loop (our code)", (millis() - enterLoop));
	}

	// Only necessary in manual mode
	unsigned long beforeProcess = millis();

	Particle.process();

	if ((millis() - beforeProcess) > timeWarnMs) {
		Serial.printlnf("%ld ms spent in Particle.process", (millis() - beforeProcess));
	}


	lastLoopExit = millis();
}

4 Likes

@rickkas7, Would you elaborate on why Particle.variable and subscribe is best placed in case PARTICLE_CONNECT_WAIT? I had come to believe that such needed to be placed in setup, based on several threads, if I understood correctly. Do they get lost if the cloud connection gets lost? Or some other reason?

1 Like

setup() is the place to put Particle.variable, Particle.function, etc. in automatic mode (the default). [EDIT: It’s actually only publish and subscribe only the cause problems, see note below]

The cloud functions only work when the cloud is available. These functions will block until the cloud is available, which can lead to some quite unpredictable timeout delays if a cloud connection can’t be made, like 30 seconds. So if they goal is to keep your loop running smoothly in semi-automatic or manual mode, you need to make the Particle cloud calls in loop(), and only when the cloud is already connected. But still only make the calls once per cloud connection, not on every loop!

I’d like to add something here.
Particle.variable() and Particle.function() calls only register variables/functions for the local background cloud task but don’t actually get in contact with the cloud when called, so they can be called even without WiFi on.
Particle.publish() and (AFAIK) Particle.subscribe() on the other hand need the cloud to be present in order to register the subscription or - even more obviously - publish.

2 Likes

I have updated the code example for Particle.variable and Particle.function. You’re right, they do function normally when the cloud is not available, it’s just publish and subscribe that cause delays.

3 Likes

@rickkas7 This is very interesting. Thanks again. What about the case in which I wish to have the TCP server and client run over wifi only, without the cloud - do I need to need to detect loss of wifi and then invoke server.stop and server.begin upon reconnect with wifi (again on wifi only without the cloud)? I ask because one of my two-photon implementations that uses wifi only TCP seems to require periodic reboot of the server photon. That would answer why I have that issue.

This code, only slightly modified from above, seems to work for Wi-Fi only. It does appear that you need to stop and begin the TCPServer after losing a Wi-Fi connection.

// Manual System Mode Telnet Example - Wi-Fi only, no Internet/cloud access
#include "Particle.h"

SYSTEM_MODE(MANUAL);

// System Thread enabled is not required for using manual mode. However, if you have code that needs to
// run constantly you should use it, even with manual mode. The reason is that certain operations when
// connecting to the cloud will block either outside of loop(), so your loop() won't be called for 20
// seconds or longer) or in Particle.process (normally very quick, it might take 2-3 seconds). These
// delays go away when the System thread is used so your loop() will be called very regularly.
SYSTEM_THREAD(ENABLED);

TCPServer server = TCPServer(23);
TCPClient client;

enum State { WIFI_CONNECT, WIFI_CONNECT_WAIT, SERVER_CONNECT_WAIT, SERVER_HANDLE_CLIENT };
State state = WIFI_CONNECT;
unsigned long stateTime = 0;
String localIP;
unsigned long lastLoopExit = 0;

// This sample code will output a message via serial if it takes longer than timeWarnMs in one of these cases:
// 1. Outside of loop - it takes an unusually long time before your loop is called called again
// 2. Your code inside loop
// 3. Time to make the Particle.process call
const unsigned long timeWarnMs = 100;

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

void loop() {
	unsigned long enterLoop = millis();

	if ((lastLoopExit != 0) && ((enterLoop - lastLoopExit) > timeWarnMs)) {
		Serial.printlnf("%ld ms since last loop call", (enterLoop - lastLoopExit));
	}

	if (WiFi.ready()) {
		switch(state) {
		case WIFI_CONNECT_WAIT:
			Serial.println("connected to Wi-Fi!");
			state = SERVER_CONNECT_WAIT;

			// The other example using the cloud uses a Particle.variable here, but this example avoids
			// using the cloud at all so it just prints the IP address to serial
			localIP = WiFi.localIP();
			Serial.println(localIP);

			// It appears you must server.stop() and server.begin() if you lose your Wi-Fi connection.
			server.begin();
			break;

		case SERVER_CONNECT_WAIT:
			if (client.connected()) {
				// A TCP client has connected to the server.
				state = SERVER_HANDLE_CLIENT;
				stateTime = millis();
			}
			else {
				// Check for an incoming connection. This is called repeatedly until a connection is made.
				client = server.available();
			}
			break;

		case SERVER_HANDLE_CLIENT:
			if (client.connected()) {
				int count = 0;

				// Echo bytes back to the client while we have input bytes, but not too many.
				// (If there are lots of bytes outstanding we want to process them in chunks to
				// avoid starving the rest of the system by spending too much time in the loop)
				while (client.available() && count++ < 128) {
					int c = client.read();

					// By the way, the actual telnet program will sent a bunch of telnet escape sequences
					// which appears as random garbage characters in serial. This isn't really a bug,
					// it's just that in the interest of clarity this isn't really a telnet server.
					client.write(c);
					Serial.write(c);
				}

				if (count > 0) {
					// We received bytes, reset the timeout counter
					stateTime = millis();
				}
				else {
					// If we don't receive any bytes in 15 seconds, disconnect the client
					// In a real program you'd probably make this timeout much longer.
					if (millis() - stateTime > 15000) {
						Serial.println("client timeout");
						client.stop();
						state = SERVER_CONNECT_WAIT;
					}
				}
			}
			else {
				// Disconnected
				Serial.println("client disconnected");
				client.stop();
				state = SERVER_CONNECT_WAIT;
			}
			break;
		}
	}
	else {
		// WiFi.ready() is false here

		switch(state) {
		case WIFI_CONNECT:
			// Not connected to the cloud, either we just started up or the connection
			// was broken and we need to reconnect
			Serial.println("attempting to connect to WiFi");
			WiFi.connect();
			stateTime = millis();
			state = WIFI_CONNECT_WAIT;
			break;

		case WIFI_CONNECT_WAIT:
			if (millis() - stateTime > 60000) {
				// Allow 60 seconds for connecting; if we fail to connect, try again
				Serial.println("failed to connect");
				state = WIFI_CONNECT;
			}
			break;

		default:
			Serial.println("WiFi connection lost, retry connect");
			WiFi.disconnect();

			// Important: If you're using a server socket, be sure to stop and begin it again
			// otherwise you won't be able to make a new connection to the server after losing
			// your network connection.
			client.stop();
			server.stop();
			state = WIFI_CONNECT;
			break;
		}
	}

	// Put your code that needs to run whether you're connected or not here

	if ((millis() - enterLoop) > timeWarnMs) {
		Serial.printlnf("%ld ms spent in loop (our code)", (millis() - enterLoop));
	}

	// Only necessary in manual mode
	unsigned long beforeProcess = millis();

	Particle.process();

	if ((millis() - beforeProcess) > timeWarnMs) {
		Serial.printlnf("%ld ms spent in Particle.process", (millis() - beforeProcess));
	}


	lastLoopExit = millis();
}

2 Likes

Thanks! @rickkas7 I’ll give it a try