// ================================================================================================= // eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus // MIT license - see license.md for details // ================================================================================================= #ifndef _MODBUS_SERVER_TCP_TEMP_H #define _MODBUS_SERVER_TCP_TEMP_H #include #include // NOLINT #include "ModbusServer.h" #undef LOCAL_LOG_LEVEL // #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE #include "Logging.h" extern "C" { #include #include } using std::vector; using std::mutex; using std::lock_guard; template class ModbusServerTCP : public ModbusServer { public: // Constructor ModbusServerTCP(); // Destructor: closes the connections ~ModbusServerTCP(); // activeClients: return number of clients currently employed uint16_t activeClients(); // start: create task with TCP server to accept requests bool start(uint16_t port, uint8_t maxClients, uint32_t timeout, int coreID = -1); // stop: drop all connections and kill server task bool stop(); protected: // Prevent copy construction and assignment ModbusServerTCP(ModbusServerTCP& m) = delete; ModbusServerTCP& operator=(ModbusServerTCP& m) = delete; inline void isInstance() { } uint8_t numClients; TaskHandle_t serverTask; uint16_t serverPort; uint32_t serverTimeout; bool serverGoDown; mutex clientLock; struct ClientData { ClientData() : task(nullptr), client(0), timeout(0), parent(nullptr) {} ClientData(TaskHandle_t t, CT& c, uint32_t to, ModbusServerTCP *p) : task(t), client(c), timeout(to), parent(p) {} ~ClientData() { if (client) { client.stop(); } if (task != nullptr) { vTaskDelete(task); LOG_D("Killed client task %d\n", (uint32_t)task); } } TaskHandle_t task; CT client; uint32_t timeout; ModbusServerTCP *parent; }; ClientData **clients; // serve: loop function for server task static void serve(ModbusServerTCP *myself); // worker: loop function for client tasks static void worker(ClientData *myData); // receive: read data from TCP ModbusMessage receive(CT& client, uint32_t timeWait); // accept: start a task to receive requests and respond to a given client bool accept(CT& client, uint32_t timeout, int coreID = -1); // clientAvailable: return true,. if a client slot is currently unused bool clientAvailable() { return (numClients - activeClients()) > 0; } }; // Constructor template ModbusServerTCP::ModbusServerTCP() : ModbusServer(), numClients(0), serverTask(nullptr), serverPort(502), serverTimeout(20000), serverGoDown(false) { clients = new ClientData*[numClients](); } // Destructor: closes the connections template ModbusServerTCP::~ModbusServerTCP() { for (uint8_t i = 0; i < numClients; ++i) { if (clients[i] != nullptr) { delete clients[i]; } } delete[] clients; serverGoDown = true; } // activeClients: return number of clients currently employed template uint16_t ModbusServerTCP::activeClients() { uint8_t cnt = 0; for (uint8_t i = 0; i < numClients; ++i) { // Current slot could have been previously used - look for cleared task handles if (clients[i] != nullptr) { // Empty task handle? if (clients[i]->task == nullptr) { // Yes. Delete entry and init client pointer lock_guard cL(clientLock); delete clients[i]; LOG_V("Delete client %d\n", i); clients[i] = nullptr; } } if (clients[i] != nullptr) cnt++; } return cnt; } // start: create task with TCP server to accept requests template bool ModbusServerTCP::start(uint16_t port, uint8_t maxClients, uint32_t timeout, int coreID) { // Task already running? if (serverTask != nullptr) { // Yes. stop it first stop(); } // Does the required number of slots fit? if (numClients != maxClients) { // No. Drop array and allocate a new one delete[] clients; // Now allocate a new one numClients = maxClients; clients = new ClientData*[numClients](); } serverPort = port; serverTimeout = timeout; serverGoDown = false; // Create unique task name char taskName[18]; snprintf(taskName, 18, "MBserve%04X", port); // Start task to handle the client xTaskCreatePinnedToCore((TaskFunction_t)&serve, taskName, SERVER_TASK_STACK, this, 5, &serverTask, coreID >= 0 ? coreID : NULL); LOG_D("Server task %s started (%d).\n", taskName, (uint32_t)serverTask); // Wait two seconds for it to establish delay(2000); return true; } // stop: drop all connections and kill server task template bool ModbusServerTCP::stop() { // Check for clients still connected for (uint8_t i = 0; i < numClients; ++i) { // Client is alive? if (clients[i] != nullptr) { // Yes. Close the connection delete clients[i]; clients[i] = nullptr; } } if (serverTask != nullptr) { // Signal server task to stop serverGoDown = true; delay(5000); LOG_D("Killed server task %d\n", (uint32_t)(serverTask)); serverTask = nullptr; serverGoDown = false; } return true; } // accept: start a task to receive requests and respond to a given client template bool ModbusServerTCP::accept(CT& client, uint32_t timeout, int coreID) { // Look for an empty client slot for (uint8_t i = 0; i < numClients; ++i) { // Empty slot? if (clients[i] == nullptr) { // Yes. allocate new client data in slot clients[i] = new ClientData(0, client, timeout, this); // Create unique task name char taskName[18]; snprintf(taskName, 18, "MBsrv%02Xclnt", i); // Start task to handle the client xTaskCreatePinnedToCore((TaskFunction_t)&worker, taskName, SERVER_TASK_STACK, clients[i], 5, &clients[i]->task, coreID >= 0 ? coreID : NULL); LOG_D("Started client %d task %d\n", i, (uint32_t)(clients[i]->task)); return true; } } LOG_D("No client slot available.\n"); return false; } template void ModbusServerTCP::serve(ModbusServerTCP *myself) { // need a local scope here to delete the server at termination time if (1) { // Set up server with given port ST server(myself->serverPort); // Start it server.begin(); // Loop until being killed while (!myself->serverGoDown) { // Do we have clients left to use? if (myself->clientAvailable()) { // Yes. accept one, when it connects CT ec = server.accept(); // Did we get a connection? if (ec) { // Yes. Forward it to the Modbus server myself->accept(ec, myself->serverTimeout, 0); LOG_D("Accepted connection - %d clients running\n", myself->activeClients()); } } // Give scheduler room to breathe delay(10); } LOG_E("Server going down\n"); // We must go down SERVER_END; } vTaskDelete(NULL); } template void ModbusServerTCP::worker(ClientData *myData) { // Get own reference data in handier form CT myClient = myData->client; uint32_t myTimeOut = myData->timeout; // TaskHandle_t myTask = myData->task; ModbusServerTCP *myParent = myData->parent; unsigned long myLastMessage = millis(); LOG_D("Worker started, timeout=%d\n", myTimeOut); // loop forever, if timeout is 0, or until timeout was hit while (myClient.connected() && (!myTimeOut || (millis() - myLastMessage < myTimeOut))) { ModbusMessage response; // Data buffer to hold prepared response // Get a request if (myClient.available()) { response.clear(); ModbusMessage m = myParent->receive(myClient, 100); // has it the minimal length (6 bytes TCP header plus serverID plus FC)? if (m.size() >= 8) { { LOCK_GUARD(cntLock, myParent->m); myParent->messageCount++; } // Extract request data ModbusMessage request; request.add(m.data() + 6, m.size() - 6); // Protocol ID shall be 0x0000 - is it? if (m[2] == 0 && m[3] == 0) { // ServerID shall be at [6], FC at [7]. Check both if (myParent->isServerFor(request.getServerID())) { // Server is correct - in principle. Do we serve the FC? MBSworker callBack = myParent->getWorker(request.getServerID(), request.getFunctionCode()); if (callBack) { // Yes, we do. // Invoke the worker method to get a response ModbusMessage data = callBack(request); // Process Response // One of the predefined types? if (data[0] == 0xFF && (data[1] == 0xF0 || data[1] == 0xF1)) { // Yes. Check it switch (data[1]) { case 0xF0: // NIL response.clear(); LOG_D("NIL response\n"); break; case 0xF1: // ECHO response = request; if (request.getFunctionCode() == WRITE_MULT_REGISTERS || request.getFunctionCode() == WRITE_MULT_COILS) { response.resize(6); } LOG_D("ECHO response\n"); break; default: // Will not get here! break; } } else { // No. User provided data response response = data; LOG_D("Data response\n"); } } else { // No, function code is not served here response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_FUNCTION); } } else { // No, serverID is not served here response.setError(request.getServerID(), request.getFunctionCode(), INVALID_SERVER); } } else { // No, protocol ID was something weird response.setError(request.getServerID(), request.getFunctionCode(), TCP_HEAD_MISMATCH); } } delay(1); // Do we have a response to send? if (response.size() >= 3) { // Yes. Do it now. // Cut off length and request data, then update TCP header m.resize(4); m.add(static_cast(response.size())); // Append response m.append(response); myClient.write(m.data(), m.size()); HEXDUMP_V("Response", m.data(), m.size()); // count error responses if (response.getError() != SUCCESS) { LOCK_GUARD(cntLock, myParent->m); myParent->errorCount++; } } // We did something communicationally - rewind timeout timer myLastMessage = millis(); } delay(1); } if (millis() - myLastMessage >= myTimeOut) { // Timeout! LOG_D("Worker stopping due to timeout.\n"); } else { // Disconnected! LOG_D("Worker stopping due to client disconnect.\n"); } // Read away all that may still hang in the buffer while (myClient.read() != -1) {} // Now stop the client myClient.stop(); { lock_guard cL(myParent->clientLock); myData->task = nullptr; } delay(50); vTaskDelete(NULL); } // receive: get request via Client connection template ModbusMessage ModbusServerTCP::receive(CT& client, uint32_t timeWait) { unsigned long lastMillis = millis(); // Timer to check for timeout ModbusMessage m; // to take read data uint16_t lengthVal = 0; uint16_t cnt = 0; const uint16_t BUFFERSIZE(300); uint8_t buffer[BUFFERSIZE]; // wait for sufficient packet data or timeout while ((millis() - lastMillis < timeWait) && ((cnt < 6) || (cnt < lengthVal)) && (cnt < BUFFERSIZE)) { // Is there data waiting? if (client.available()) { buffer[cnt] = client.read(); // Are we at the TCP header length field byte #1? if (cnt == 4) lengthVal = buffer[cnt] << 8; // Are we at the TCP header length field byte #2? if (cnt == 5) { lengthVal |= buffer[cnt]; lengthVal += 6; } cnt++; // Rewind EOT and timeout timers lastMillis = millis(); } else { delay(1); // Give scheduler room to breathe } } // Did we receive some data? if (cnt) { // Yes. Is it too much? if (cnt >= BUFFERSIZE) { // Yes, likely a buffer overflow of some sort // Adjust message size in TCP header buffer[4] = (cnt >> 8) & 0xFF; buffer[5] = cnt & 0xFF; LOG_E("Potential buffer overrun (>%d)!\n", cnt); } // Get as much buffer as was read m.add(buffer, cnt); } return m; } #endif