I'm attempting to write a Particle program that runs on a Particle B-SoM board running OS 6.1.1. It is a TCP client that connects to a TCP server and periodically writes some command strings for the server to consume and act on. I can repeat the problem talking to a simple Python server.
After the client connects to the server and sends data periodically for about 3-5 minutes, the client will lose connection and the server will not detect any error. The server continues to sit in a TCP recv function and doesn't error out with any connection errors. Sometimes restarting the server works to re-establish connection but more often, the client requires the Particle board to be re-powered before it connects to the server again.
We are using an Adafruit ethernet featherwing connector and I've tried it with more than one. We did a couple of small modifications when attaching the connector. We changed the enabling pin as the default pin was already being used and added jumpers for a couple of lines which don't have pins in the standard featherwing output.
This is an example of the outputs I'm seeing on the Particle TCP client side when it loses the connection:
Entering calibration stage SPAN
Connected value = 1
sendFlowVolumesToNI: 2,10,5,0
0000153348 [system] WARN: Internet available, cloud not reachable
0000153348 [system] WARN: Cloud handshake failed, code=-220
0000153599 [system] INFO: Cloud: disconnecting
0000153599 [system] INFO: Cloud: disconnected
0000153599 [system] INFO: Cloud: connecting
0000153601 [system] WARN: Failed to load session data from persistent storage
0000153608 [system] INFO: Cloud socket connected
0000153608 [comm.protocol.handshake] INFO: Establish secure connection
0000153611 [comm.dtls] INFO: (CMPL,RENEG,NO_SESS,ERR) restoreStatus=2
Starting Sample Pump 1
0000182777 [wiring] ERROR: recv error = 113
Connected value = 0
Connection lost, try reconnection
Socket Status = 0
Connection closed
0000198614 [comm.dtls] ERROR: handshake failed -6800
0000198615 [comm.dtls] ERROR: Invalid handshake state
0000198616 [comm.protocol.handshake] ERROR: Handshake failed: 17
Failed to connect to server
Ethernet is on
Socket Status = 0
Here is my TCP client class and main loop code:
TCPIPTest.cpp:
/*
* Project myProject
* Author: Your Name
* Date:
* For comprehensive documentation and examples, please visit:
* https://docs.particle.io/firmware/best-practices/firmware-template/
*/
// Include Particle Device OS APIs
#include "Particle.h"
#include "TCPClientHelper.h"
TCPClient client;
// Let Device OS manage the connection to the Particle Cloud
SYSTEM_MODE(AUTOMATIC);
// Run the application and system concurrently in separate threads
SYSTEM_THREAD(ENABLED);
// Show system, cloud connectivity, and application logs over USB
// View logs with CLI using 'particle serial monitor --follow'
SerialLogHandler logHandler(LOG_LEVEL_INFO);
int SamplePump_1_Control(String command);
// TCP Server connection details
byte niServer[4] = {192, 168, 50, 105 };
uint16_t port = 6341;
NICommand currentCommand = NICommand::ZERO;
TCPClientHelper tcpHelper(niServer, port);
// setup() runs once, when the device is first turned on
void setup() {
Serial.begin(9600); // Start serial communication for debugging50
delay(1000); // Allow time for serial to initialize
Particle.function("pumpControl", SamplePump_1_Control);
tcpHelper.Initialize(false);
}
// loop() runs over and over again, as quickly as it can execute.
void loop() {
int Z1Flow = 10;
int S1Flow = 5;
int S2Flow = 0;
int zeroTime = 0;
int spanTime = 0;
int offTime = 0;
int pumptime = 0;
switch (currentCommand)
{
case NICommand::ZERO:
Serial.println("Entering calibration stage ZERO");
tcpHelper.sendFlowVolumesToNI(NICommand::ZERO, Z1Flow, 0, 0);
delay(zeroTime);
currentCommand = NICommand::SPAN;
break;
case NICommand::SPAN:
Serial.println("Entering calibration stage SPAN");
tcpHelper.sendFlowVolumesToNI(NICommand::SPAN, Z1Flow, S1Flow, S2Flow);
delay(spanTime);
currentCommand = NICommand::SAMPLE_PUMP_1;
break;
case NICommand::SAMPLE_PUMP_1:
Serial.println("Starting Sample Pump 1");
tcpHelper.sendPumpOnOff(NICommand::SAMPLE_PUMP_1, true );
delay(pumptime);
currentCommand = NICommand::OFF;
break;
default:
Serial.println("Entering calibration stage OFF");
tcpHelper.sendFlowVolumesToNI(NICommand::OFF, 0, 0, 0);
delay(offTime);
currentCommand = NICommand::ZERO;
break;
}
delay(30000);
}
int SamplePump_1_Control(String command)
{
if (command == "on") {
tcpHelper.sendPumpOnOff(NICommand::SAMPLE_PUMP_1, true );
return 1;
}
else if (command == "off") {
tcpHelper.sendPumpOnOff(NICommand::SAMPLE_PUMP_1, false );
return 1;
}
else {
return -1;
}
}
TCPClientHelper.h
#ifndef TCPCLIENTHELPER_H
#define TCPCLIENTHELPER_H
#include "Particle.h"
enum class NICommand {
OFF = 0,
ZERO = 1,
SPAN = 2,
FLUSH = 3,
SAMPLE_PUMP_1 = 4,
SAMPLE_PUMP_2 = 5,
SUCTION_PUMP_1 = 6,
SUCTION_PUMP_2 = 7,
};
// Define the TCPClientHelper class
class TCPClientHelper {
public:
// Constructor that takes server and port as parameters
TCPClientHelper(const byte server[4], uint16_t port);
void Initialize(bool closeConnectionAfterCommands);
// Function to send flow volumes (three integers) to the TCP server
bool sendFlowVolumesToNI(NICommand cailbrationStage, int volume1, int volume2, int volume3);
bool sendPumpOnOff(NICommand pumpCommand, bool onOff );
// Helper functions to modify server and port after initialization
void setServer(const byte server[4]);
void setPort(uint16_t port);
bool checkClientConnection();
void closeClientConnection();
private:
byte server[4]; // Server IP address or hostname
uint16_t port; // Server port
TCPClient client; // TCPClient object for managing the connection
bool closeConnectionAfterCommands;
};
#endif
TCPClient.cpp
#include "TCPClientHelper.h"
// Constructor that accepts the server address and port
TCPClientHelper::TCPClientHelper(const byte server[4], uint16_t port) {
memcpy(this->server, server, 4);
this->port = port;
}
void TCPClientHelper::Initialize(bool closeConnectionAfterCommands)
{
this->closeConnectionAfterCommands = closeConnectionAfterCommands;
// Disable Listening Mode if not required
if (System.featureEnabled(FEATURE_DISABLE_LISTENING_MODE)) {
Serial.println("FEATURE_DISABLE_LISTENING_MODE enabled");
} else {
Serial.println("Disabling Listening Mode...");
System.enableFeature(FEATURE_DISABLE_LISTENING_MODE);
}
Serial.println("Checking if Ethernet is on...");
if (Ethernet.isOn()) {
Serial.println("Ethernet is on");
uint8_t macAddrBuf[8] = {};
uint8_t* macAddr = Ethernet.macAddress(macAddrBuf);
if (macAddr != nullptr) {
Serial.printf("Ethernet MAC: %02x %02x %02x %02x %02x %02x\r\n",
macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
}
Ethernet.connect();
waitFor(Ethernet.ready, 30000);
Serial.printf("Ethernet.ready: %d\r\n", Ethernet.ready());
IPAddress localIP = Ethernet.localIP();
Serial.printf("localIP: %s\r\n", localIP.toString().c_str());
IPAddress gatewayIP = Ethernet.gatewayIP();
Serial.printf("gatewayIP: %s\r\n", gatewayIP.toString().c_str());
} else {
Serial.println("Ethernet is off or not detected.");
}
if (System.featureEnabled(FEATURE_ETHERNET_DETECTION)) {
Serial.println("FEATURE_ETHERNET_DETECTION enabled");
} else {
Serial.println("Enabling Ethernet...");
System.enableFeature(FEATURE_ETHERNET_DETECTION);
delay(500);
System.reset();
}
Serial.printlnf("Connecting to: %d.%d.%d.%d", server[0], server[1], server[2], server[3]);
if (!client.connect(IPAddress(server), port))
Serial.println("Failed to connect to server");
else
Serial.println("Connected to server");
client.setTimeout(100);
}
bool TCPClientHelper::checkClientConnection()
{
// If this line is not in here, the TCPClient never detects if the server is offline.
// It just continues to write values to it's socket buffer. Shouldn't a
int heartbeat = client.read();
bool clientConnected = client.connected();
Serial.printf("Connected value = %d\n", clientConnected);
if (!clientConnected) {
Serial.println("Connection lost, try reconnection");
Serial.printlnf("Socket Status = %d", client.status());
closeClientConnection();
if (!client.connect(IPAddress(server), port)) {
Serial.println("Failed to connect to server");
if (Ethernet.isOn())
Serial.println("Ethernet is on");
Serial.printlnf("Socket Status = %d", client.status());
return false;
}
else {
Serial.println("Reconnected...");
}
}
return client.connected();
}
// Function to send flow volumes to the server
bool TCPClientHelper::sendFlowVolumesToNI(NICommand cailbrationStage, int volume1, int volume2, int volume3) {
if (checkClientConnection()) {
String data = String::format("%d,%d,%d,%d", (int)cailbrationStage, volume1,volume2,volume3);
// Send the string to the TCP server
client.print(data);
Serial.printlnf("sendFlowVolumesToNI: " + data);
if (closeConnectionAfterCommands)
{
closeClientConnection();
}
return true;
}
return false;
}
bool TCPClientHelper::sendPumpOnOff(NICommand pumpCommand, bool onOff )
{
if (checkClientConnection()) {
String data = String::format("%d,%d", (int)pumpCommand, onOff);
// Send the string to the TCP server
client.print(data); // Using println to send a newline-terminated string
Serial.printlnf("sendPumpOnOff: " + data);
if (closeConnectionAfterCommands)
{
closeClientConnection();
}
return true;
}
return false;
}
// Helper function to modify the server address
void TCPClientHelper::setServer(const byte server[4]) {
memcpy(this->server, server, 4);
Serial.printlnf("Server IP updated to: %d.%d.%d.%d", server[0], server[1], server[2], server[3]);
}
// Helper function to modify the port
void TCPClientHelper::setPort(uint16_t port) {
this->port = port;
Serial.printlnf("Port updated to: %d", port);
}
void TCPClientHelper::closeClientConnection()
{
// Close the connection
client.flush();
client.stop();
Serial.println("Connection closed");
}
PythonTestServer.py
import datetime
import socket
# Define the server address and port
server_address = '192.168.50.105' # Listen on all network interfaces
port = 6341 # Replace with the port number to match your client
def get_timestamp():
"""Return the current time as a formatted string."""
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def start_server():
"""Start a TCP server that listens for incoming connections."""
# Create a TCP/IP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Bind the socket to the address and port
server_socket.bind((server_address, port))
server_socket.listen(1) # Listen for incoming connections, backlog of 5 clients
print(f"Server started, listening on {server_address}:{port}")
while True:
try:
# Wait for a connection
client_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")
# Handle the client connection
while True:
try:
# Read the data from the client, expecting a newline-delimited CSV string
timestamp = get_timestamp()
print(f"{timestamp} Entering recv ...")
data = client_socket.recv(1024).decode('utf-8')
# If no data is received, the client has closed the connection
if not data:
print(f"Connection closed by {client_address}")
break
print(data)
# Split the data on newline to handle multiple CSV lines in one packet
lines = data.strip().split("\r\n")
for line in lines:
try:
# Parse the CSV data into individual numeric values
values = [int(x) for x in line.split(',')]
# Print the received numeric values
timestamp = get_timestamp()
print(f"{timestamp} - Received data: {values}")
except ValueError as e:
print(f"Invalid data received: {line}: {e}")
except socket.error as e:
# Handle socket error during data reception
print(f"Socket error: {e}")
break # Break out to close the client socket and wait for a new connection
except socket.error as e:
# Handle errors during the connection establishment
print(f"Error accepting connection: {e}")
finally:
# Ensure the client socket is closed in case of error
if 'client_socket' in locals():
client_socket.close()
print(f"Connection with {client_address} closed")
if __name__ == '__main__':
start_server()