Initial EtherNet/IP support

This commit is contained in:
thiagoralves 2019-06-17 11:59:48 -05:00
parent 0d0e2f0715
commit a95ce61356
8 changed files with 391 additions and 32 deletions

189
webserver/core/enip.cpp Normal file
View File

@ -0,0 +1,189 @@
//-----------------------------------------------------------------------------
// Copyright 2019 Thiago Alves
// This file is part of the OpenPLC Software Stack.
//
// OpenPLC is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// OpenPLC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with OpenPLC. If not, see <http://www.gnu.org/licenses/>.
//------
//
// This file has all the EtherNet/IP functions supported by the OpenPLC. If any
// other function is to be added to the project, it must be added here
// Thiago Alves, Apr 2019
//-----------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <string.h>
#include "ladder.h"
#define ENIP_MIN_LENGTH 28
struct enip_header
{
unsigned char *command;//[2];
unsigned char *length;//[2];
unsigned char *session_handle;//[4];
unsigned char *status;//[4];
unsigned char *sender_context;//[8];
unsigned char *options;//[4];
unsigned char *data;
};
struct enip_data
{
unsigned char *interface_handle;
unsigned char *timeout;
unsigned char *item_count;
unsigned char *item1_id;
unsigned char *item1_length;
unsigned char *item1_data;
unsigned char *item2_id;
unsigned char *item2_length;
unsigned char *item2_data;
};
thread_local unsigned char enip_session[4];
int respondToError(unsigned char *buffer, int buffer_size, int error_code)
{
return -1;
}
int parseEnipHeader(unsigned char *buffer, int buffer_size, struct enip_header *header)
{
//verify if message is big enough
if (buffer_size < ENIP_MIN_LENGTH)
return -1;
header->command = &buffer[0];
header->length = &buffer[2];
header->session_handle = &buffer[4];
header->status = &buffer[8];
header->sender_context = &buffer[12];
header->options = &buffer[20];
/*
memcpy(header->command, &buffer[0], 2);
memcpy(header->length, &buffer[2], 2);
memcpy(header->session_handle, &buffer[4], 4);
memcpy(header->status, &buffer[8], 4);
memcpy(header->sender_context, &buffer[12], 8);
memcpy(header->options, &buffer[20], 4);
*/
header->data = &buffer[24];
uint16_t enip_data_size = ((uint16_t)header->length[1] << 8) | (uint16_t)header->length[0];
//verify if buffer_size matches enip_data_size
if (buffer_size - 24 < enip_data_size)
return -1;
return enip_data_size;
}
int parseEnipData(struct enip_header *header, struct enip_data *data)
{
data->interface_handle = &header->data[0];
data->timeout = &header->data[4];
data->item_count = &header->data[6];
data->item1_id = &header->data[8];
data->item1_length = &header->data[10];
data->item1_data = &header->data[12];
uint16_t item_length = ((uint16_t)data->item1_length[1] << 8) | (uint16_t)data->item1_length[0];
data->item2_id = &header->data[12+item_length];
data->item2_length = &header->data[14+item_length];
data->item2_data = &header->data[16+item_length];
return 1;
}
int registerEnipSession(struct enip_header *header)
{
unsigned char r[4];
srand((unsigned)time(NULL));
for (int i = 0; i < 4; ++i)
r[i] = rand();
memcpy(header->session_handle, &r[0], 4);
memcpy(&enip_session[0], &r[0], 4);
return ENIP_MIN_LENGTH;
}
//-----------------------------------------------------------------------------
// This function must parse and process the client request and write back the
// response for it. The return value is the size of the response message in
// bytes.
//-----------------------------------------------------------------------------
int processEnipMessage(unsigned char *buffer, int buffer_size)
{
int error_code;
struct enip_header header;
error_code = parseEnipHeader(buffer, buffer_size, &header);
if (error_code < 0)
return respondToError(buffer, buffer_size, error_code);
if (header.command[0] == 0x65)
return registerEnipSession(&header);
else if (header.command[0] == 0x6f)
{
struct enip_data data;
error_code = parseEnipData(&header, &data);
if (error_code < 0)
return respondToError(buffer, buffer_size, error_code);
if (data.item2_id[0] == 0x91)
{
//PCCC type 1 - Unknown
}
else if (data.item2_id[0] == 0xb2)
{
//PCCC type 2 - Unconnected Data Item
}
else if (data.item1_id[0] == 0xa1 && data.item2_id[0] == 0xb1)
{
//PCCC type 3 - Connected Data Item
}
else
{
//Unknown type ID. Respond with error_code
}
}
else
{
unsigned char log_msg[1000];
unsigned char *p = log_msg;
p += sprintf(p, "Unknown EtherNet/IP request: ");
for (int i = 0; i < buffer_size; i++)
{
p += sprintf(p, "%02x ", (unsigned char)buffer[i]);
}
p += sprintf(p, "\n");
printf(log_msg);
return -1;
}
}

58
webserver/core/interactive_server.cpp Executable file → Normal file
View File

@ -42,6 +42,8 @@ bool run_modbus = 0;
int modbus_port = 502;
bool run_dnp3 = 0;
int dnp3_port = 20000;
bool run_enip = 0;
int enip_port = 44818;
unsigned char server_command[1024];
int command_index = 0;
bool processing_command = 0;
@ -51,13 +53,14 @@ time_t end_time;
//Global Threads
pthread_t modbus_thread;
pthread_t dnp3_thread;
pthread_t enip_thread;
//-----------------------------------------------------------------------------
// Start the Modbus Thread
//-----------------------------------------------------------------------------
void *modbusThread(void *arg)
{
startServer(modbus_port);
startServer(modbus_port, MODBUS_PROTOCOL);
}
//-----------------------------------------------------------------------------
@ -68,6 +71,14 @@ void *dnp3Thread(void *arg)
dnp3StartServer(dnp3_port);
}
//-----------------------------------------------------------------------------
// Start the Enip Thread
//-----------------------------------------------------------------------------
void *enipThread(void *arg)
{
startServer(enip_port, ENIP_PROTOCOL);
}
//-----------------------------------------------------------------------------
// Read the argument from a command function
//-----------------------------------------------------------------------------
@ -216,9 +227,9 @@ void processCommand(unsigned char *buffer, int client_fd)
else if (strncmp(buffer, "start_modbus(", 13) == 0)
{
processing_command = true;
sprintf(log_msg, "Issued start_modbus() command to start on port: %d\n", readCommandArgument(buffer));
log(log_msg);
modbus_port = readCommandArgument(buffer);
sprintf(log_msg, "Issued start_modbus() command to start on port: %d\n", modbus_port);
log(log_msg);
if (run_modbus)
{
sprintf(log_msg, "Modbus server already active. Restarting on port: %d\n", modbus_port);
@ -251,9 +262,9 @@ void processCommand(unsigned char *buffer, int client_fd)
else if (strncmp(buffer, "start_dnp3(", 11) == 0)
{
processing_command = true;
sprintf(log_msg, "Issued start_dnp3() command to start on port: %d\n", readCommandArgument(buffer));
log(log_msg);
dnp3_port = readCommandArgument(buffer);
sprintf(log_msg, "Issued start_dnp3() command to start on port: %d\n", dnp3_port);
log(log_msg);
if (run_dnp3)
{
sprintf(log_msg, "DNP3 server already active. Restarting on port: %d\n", dnp3_port);
@ -283,6 +294,41 @@ void processCommand(unsigned char *buffer, int client_fd)
}
processing_command = false;
}
else if (strncmp(buffer, "start_enip(", 11) == 0)
{
processing_command = true;
enip_port = readCommandArgument(buffer);
sprintf(log_msg, "Issued start_enip() command to start on port: %d\n", enip_port);
log(log_msg);
if (run_enip)
{
sprintf(log_msg, "EtherNet/IP server already active. Restarting on port: %d\n", enip_port);
log(log_msg);
//Stop Enip server
run_enip = 0;
pthread_join(enip_thread, NULL);
sprintf(log_msg, "EtherNet/IP server was stopped\n");
log(log_msg);
}
//Start Enip server
run_enip = 1;
pthread_create(&enip_thread, NULL, enipThread, NULL);
processing_command = false;
}
else if (strncmp(buffer, "stop_enip()", 11) == 0)
{
processing_command = true;
sprintf(log_msg, "Issued stop_enip() command\n");
log(log_msg);
if (run_enip)
{
run_enip = 0;
pthread_join(enip_thread, NULL);
sprintf(log_msg, "EtherNet/IP server was stopped\n");
log(log_msg);
}
processing_command = false;
}
else if (strncmp(buffer, "runtime_logs()", 14) == 0)
{
processing_command = true;
@ -411,4 +457,4 @@ void startInteractiveServer(int port)
closeSocket(client_fd);
sprintf(log_msg, "Terminating interactive server thread\r\n");
log(log_msg);
}
}

10
webserver/core/ladder.h Executable file → Normal file
View File

@ -26,6 +26,10 @@
#include <pthread.h>
#include <stdint.h>
#define MODBUS_PROTOCOL 0
#define DNP3_PROTOCOL 1
#define ENIP_PROTOCOL 2
//Internal buffers for I/O and memory. These buffers are defined in the
//auto-generated glueVars.cpp file
#define BUFFER_SIZE 1024
@ -117,7 +121,7 @@ extern int log_index;
void handleSpecialFunctions();
//server.cpp
void startServer(int port);
void startServer(int port, int protocol_type);
int getSO_ERROR(int fd);
void closeSocket(int fd);
bool SetSocketBlockingEnabled(int fd, bool blocking);
@ -126,6 +130,7 @@ bool SetSocketBlockingEnabled(int fd, bool blocking);
void startInteractiveServer(int port);
extern bool run_modbus;
extern bool run_dnp3;
extern bool run_enip;
extern time_t start_time;
extern time_t end_time;
@ -133,6 +138,9 @@ extern time_t end_time;
int processModbusMessage(unsigned char *buffer, int bufferSize);
void mapUnusedIO();
//enip.cpp
int processEnipMessage(unsigned char *buffer, int buffer_size);
//modbus_master.cpp
void initializeMB();
void *querySlaveDevices(void *arg);

82
webserver/core/server.cpp Executable file → Normal file
View File

@ -35,6 +35,7 @@
#define MAX_INPUT 16
#define MAX_OUTPUT 16
#define MAX_MODBUS 100
#define NET_BUFFER_SIZE 10000
//-----------------------------------------------------------------------------
@ -93,7 +94,7 @@ int createSocket(int port)
socket_fd = socket(AF_INET,SOCK_STREAM,0);
if (socket_fd<0)
{
sprintf(log_msg, "Modbus Server: error creating stream socket => %s\n", strerror(errno));
sprintf(log_msg, "Server: error creating stream socket => %s\n", strerror(errno));
log(log_msg);
return -1;
}
@ -114,14 +115,14 @@ int createSocket(int port)
//Bind socket
if (bind(socket_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
sprintf(log_msg, "Modbus Server: error binding socket => %s\n", strerror(errno));
sprintf(log_msg, "Server: error binding socket => %s\n", strerror(errno));
log(log_msg);
return -1;
}
// we accept max 5 pending connections
listen(socket_fd,5);
sprintf(log_msg, "Modbus Server: Listening on port %d\n", port);
sprintf(log_msg, "Server: Listening on port %d\n", port);
log(log_msg);
return socket_fd;
@ -131,18 +132,24 @@ int createSocket(int port)
// Blocking call. Wait here for the client to connect. Returns the file
// descriptor to communicate with the client.
//-----------------------------------------------------------------------------
int waitForClient(int socket_fd)
int waitForClient(int socket_fd, int protocol_type)
{
unsigned char log_msg[1000];
int client_fd;
struct sockaddr_in client_addr;
bool *run_server;
socklen_t client_len;
sprintf(log_msg, "Modbus Server: waiting for new client...\n");
if (protocol_type == MODBUS_PROTOCOL)
run_server = &run_modbus;
else if (protocol_type == ENIP_PROTOCOL)
run_server = &run_enip;
sprintf(log_msg, "Server: waiting for new client...\n");
log(log_msg);
client_len = sizeof(client_addr);
while (run_modbus)
while (*run_server)
{
client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &client_len); //non-blocking call
if (client_fd > 0)
@ -163,8 +170,8 @@ int waitForClient(int socket_fd)
//-----------------------------------------------------------------------------
int listenToClient(int client_fd, unsigned char *buffer)
{
bzero(buffer, 1024);
int n = read(client_fd, buffer, 1024);
bzero(buffer, NET_BUFFER_SIZE);
int n = read(client_fd, buffer, NET_BUFFER_SIZE);
return n;
}
@ -173,8 +180,16 @@ int listenToClient(int client_fd, unsigned char *buffer)
//-----------------------------------------------------------------------------
void processMessage(unsigned char *buffer, int bufferSize, int client_fd)
{
int messageSize = processModbusMessage(buffer, bufferSize);
write(client_fd, buffer, messageSize);
if (protocol_type == MODBUS_PROTOCOL)
{
int messageSize = processModbusMessage(buffer, bufferSize);
write(client_fd, buffer, messageSize);
}
else if (protocol_type == ENIP_PROTOCOL)
{
int messageSize = processEnipMessage(buffer, bufferSize);
write(client_fd, buffer, messageSize);
}
}
//-----------------------------------------------------------------------------
@ -183,20 +198,28 @@ void processMessage(unsigned char *buffer, int bufferSize, int client_fd)
void *handleConnections(void *arguments)
{
unsigned char log_msg[1000];
int client_fd = *(int *)arguments;
unsigned char buffer[1024];
int *args = (int *)arguments;
int client_fd = args[0];
int protocol_type = args[1];
unsigned char buffer[NET_BUFFER_SIZE];
int messageSize;
bool *run_server;
if (protocol_type == MODBUS_PROTOCOL)
run_server = &run_modbus;
else if (protocol_type == ENIP_PROTOCOL)
run_server = &run_enip;
sprintf(log_msg, "Modbus Server: Thread created for client ID: %d\n", client_fd);
sprintf(log_msg, "Server: Thread created for client ID: %d\n", client_fd);
log(log_msg);
while(run_modbus)
while(*run_server)
{
//unsigned char buffer[1024];
//unsigned char buffer[NET_BUFFER_SIZE];
//int messageSize;
messageSize = listenToClient(client_fd, buffer);
if (messageSize <= 0 || messageSize > 1024)
if (messageSize <= 0 || messageSize > NET_BUFFER_SIZE)
{
// something has gone wrong or the client has closed connection
if (messageSize == 0)
@ -212,7 +235,7 @@ void *handleConnections(void *arguments)
break;
}
processMessage(buffer, messageSize, client_fd);
processMessage(buffer, messageSize, client_fd, protocol_type);
}
//printf("Debug: Closing client socket and calling pthread_exit in server.cpp\n");
close(client_fd);
@ -230,13 +253,21 @@ void startServer(int port)
{
unsigned char log_msg[1000];
int socket_fd, client_fd;
socket_fd = createSocket(port);
mapUnusedIO();
bool *run_server;
while(run_modbus)
socket_fd = createSocket(port);
if (protocol_type == MODBUS_PROTOCOL)
{
client_fd = waitForClient(socket_fd); //block until a client connects
mapUnusedIO();
run_server = &run_modbus;
}
else if (protocol_type == ENIP_PROTOCOL)
run_server = &run_enip;
while(*run_server)
{
client_fd = waitForClient(socket_fd, protocol_type); //block until a client connects
if (client_fd < 0)
{
sprintf(log_msg, "Modbus Server: Error accepting client!\n");
@ -245,13 +276,14 @@ void startServer(int port)
else
{
int arguments[1];
int arguments[2];
pthread_t thread;
int ret = -1;
sprintf(log_msg, "Modbus Server: Client accepted! Creating thread for the new client ID: %d...\n", client_fd);
log(log_msg);
arguments[0] = client_fd;
ret = pthread_create(&thread, NULL, handleConnections, arguments);
arguments[1] = protocol_type;
ret = pthread_create(&thread, NULL, handleConnections, (void*)arguments);
if (ret==0)
{
pthread_detach(thread);
@ -262,4 +294,4 @@ void startServer(int port)
close(client_fd);
sprintf(log_msg, "Terminating Modbus thread\r\n");
log(log_msg);
}
}

Binary file not shown.

22
webserver/openplc.py Executable file → Normal file
View File

@ -178,6 +178,28 @@ class runtime:
s.close()
except:
print("Error connecting to OpenPLC runtime")
def start_enip(self, port_num):
if (self.status() == "Running"):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 43628))
s.send('start_enip(' + str(port_num) + ')\n')
data = s.recv(1000)
s.close()
except:
print("Error connecting to OpenPLC runtime")
def stop_enip(self):
if (self.status() == "Running"):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 43628))
s.send('stop_enip()\n')
data = s.recv(1000)
s.close()
except:
print("Error connecting to OpenPLC runtime")
def logs(self):
if (self.status() == "Running"):

23
webserver/pages.py Executable file → Normal file
View File

@ -884,6 +884,8 @@ settings_tail = """
var modbus_text = document.getElementById('modbus_server_port');
var dnp3_checkbox = document.getElementById('dnp3_server');
var dnp3_text = document.getElementById('dnp3_server_port');
var enip_checkbox = document.getElementById('enip_server');
var enip_text = document.getElementById('enip_server_port');
var auto_run_checkbox = document.getElementById('auto_run');
var auto_run_text = document.getElementById('auto_run_text');
@ -905,6 +907,15 @@ settings_tail = """
dnp3_text.disabled = true;
}
if (enip_checkbox.checked == true)
{
enip_text.disabled = false;
}
else
{
enip_text.disabled = true;
}
if (auto_run_checkbox.checked == true)
{
auto_run_text.value = 'true';
@ -925,6 +936,11 @@ settings_tail = """
setupCheckboxes();
}
document.getElementById('enip_server').onchange = function()
{
setupCheckboxes();
}
document.getElementById('auto_run').onchange = function()
{
setupCheckboxes();
@ -936,6 +952,8 @@ settings_tail = """
var modbus_port = document.forms["uploadForm"]["modbus_server_port"].value;
var dnp3_checkbox = document.forms["uploadForm"]["dnp3_server"].checked;
var dnp3_port = document.forms["uploadForm"]["dnp3_server_port"].value;
var enip_checkbox = document.forms["uploadForm"]["enip_server"].checked;
var enip_port = document.forms["uploadForm"]["enip_server_port"].value;
if (modbus_checkbox && (Number(modbus_port) < 0 || Number(modbus_port) > 65535))
{
@ -947,6 +965,11 @@ settings_tail = """
alert("Please select a port number between 0 and 65535");
return false;
}
if (enip_checkbox && (Number(enip_port) < 0 || Number(enip_port) > 65535))
{
alert("Please select a port number between 0 and 65535");
return false;
}
return true;
}
</script>

39
webserver/webserver.py Executable file → Normal file
View File

@ -53,6 +53,13 @@ def configure_runtime():
else:
print("Disabling DNP3")
openplc_runtime.stop_dnp3()
elif (row[0] == "Enip_port"):
if (row[1] != "disabled"):
print("Enabling EtherNet/IP on port " + str(int(row[1])))
openplc_runtime.start_enip(int(row[1]))
else:
print("Disabling EtherNet/IP")
openplc_runtime.stop_enip()
except Error as e:
print("error connecting to the database" + str(e))
else:
@ -1684,6 +1691,8 @@ def settings():
modbus_port = str(row[1])
elif (row[0] == "Dnp3_port"):
dnp3_port = str(row[1])
elif (row[0] == "Enip_port"):
enip_port = str(row[1])
elif (row[0] == "Start_run_mode"):
start_run = str(row[1])
elif (row[0] == "Slave_polling"):
@ -1728,6 +1737,28 @@ def settings():
<label for='dnp3_server_port'><b>DNP3 Server Port</b></label>
<input type='text' id='dnp3_server_port' name='dnp3_server_port' value='""" + dnp3_port + "'>"
return_str += """
<br>
<br>
<br>
<label class="container">
<b>Enable EtherNet/IP Server</b>"""
if (enip_port == 'disabled'):
return_str += """
<input id="enip_server" type="checkbox">
<span class="checkmark"></span>
</label>
<label for='enip_server_port'><b>EtherNet/IP Server Port</b></label>
<input type='text' id='enip_server_port' name='enip_server_port' value='44818'>"""
else:
return_str += """
<input id="enip_server" type="checkbox" checked>
<span class="checkmark"></span>
</label>
<label for='enip_server_port'><b>EtherNet/IP Server Port</b></label>
<input type='text' id='enip_server_port' name='enip_server_port' value='""" + enip_port + "'>"
return_str += """
<br>
<br>
@ -1776,6 +1807,7 @@ def settings():
elif (flask.request.method == 'POST'):
modbus_port = flask.request.form.get('modbus_server_port')
dnp3_port = flask.request.form.get('dnp3_server_port')
enip_port = flask.request.form.get('enip_server_port')
start_run = flask.request.form.get('auto_run_text')
slave_polling = flask.request.form.get('slave_polling_period')
slave_timeout = flask.request.form.get('slave_timeout')
@ -1799,6 +1831,13 @@ def settings():
cur.execute("UPDATE Settings SET Value = ? WHERE Key = 'Dnp3_port'", (str(dnp3_port),))
conn.commit()
if (enip_port == None):
cur.execute("UPDATE Settings SET Value = 'disabled' WHERE Key = 'Enip_port'")
conn.commit()
else:
cur.execute("UPDATE Settings SET Value = ? WHERE Key = 'Enip_port'", (str(enip_port),))
conn.commit()
if (start_run == 'true'):
cur.execute("UPDATE Settings SET Value = 'true' WHERE Key = 'Start_run_mode'")
conn.commit()