diff --git a/core/core_builders/build_normal.sh b/core/core_builders/build_normal.sh index 2185d88..2f17923 100755 --- a/core/core_builders/build_normal.sh +++ b/core/core_builders/build_normal.sh @@ -1,10 +1,10 @@ #!/bin/bash cd core echo Generating object files... -g++ -I ./lib -c Config0.c -g++ -I ./lib -c Res0.c +g++ -std=gnu++11 -I ./lib -c Config0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal +g++ -std=gnu++11 -I ./lib -c Res0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal echo Generating glueVars.cpp ./glue_generator echo Compiling main program -g++ *.cpp *.o -o openplc -I ./lib -pthread -fpermissive +g++ -std=gnu++11 *.cpp *.o -o openplc -I ./lib -pthread -fpermissive -lasiodnp3 -lasiopal -lopendnp3 -lopenpal cd .. diff --git a/core/core_builders/build_rpi.sh b/core/core_builders/build_rpi.sh index 36a3851..903fe18 100755 --- a/core/core_builders/build_rpi.sh +++ b/core/core_builders/build_rpi.sh @@ -1,10 +1,11 @@ #!/bin/bash cd core echo Generating object files... -g++ -I ./lib -c Config0.c -g++ -I ./lib -c Res0.c +g++ -std=gnu++11 -I ./lib -c Config0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal +g++ -std=gnu++11 -I ./lib -c Res0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal + echo Generating glueVars.cpp ./glue_generator echo Compiling main program -g++ *.cpp *.o -o openplc -I ./lib -lrt -lwiringPi -lpthread -fpermissive +g++ -std=gnu++11 *.cpp *.o -o openplc -I ./lib -lrt -lwiringPi -lpthread -fpermissive -lasiodnp3 -lasiopal -lopendnp3 -lopenpal cd .. diff --git a/core/dnp3.cpp b/core/dnp3.cpp new file mode 100644 index 0000000..49b354a --- /dev/null +++ b/core/dnp3.cpp @@ -0,0 +1,411 @@ +//2017 Trevor Aron +// +//File contains code for DNP3 +// +#include +#include +#include +#include +#include + +#include +#include + +#include +//#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ladder.h" + +//some modbus defines +#define MAX_DISCRETE_INPUT 800 +#define MAX_COILS 800 +#define MAX_HOLD_REGS 8192 +#define MAX_INP_REGS 1024 + +#define MIN_16B_RANGE 1024 +#define MAX_16B_RANGE 2047 +#define MIN_32B_RANGE 2048 +#define MAX_32B_RANGE 4095 +#define MIN_64B_RANGE 4096 +#define MAX_64B_RANGE 8191 + +#define OPLC_CYCLE 50000000 + + + +using namespace std; +using namespace opendnp3; +using namespace openpal; +using namespace asiopal; +using namespace asiodnp3; + + +IEC_BOOL dnp3_discrete_input[MAX_DISCRETE_INPUT]; +IEC_BOOL dnp3_coils[MAX_COILS]; +IEC_UINT dnp3_input_regs[MAX_INP_REGS]; +IEC_UINT dnp3_holding_regs[MAX_HOLD_REGS]; + + +// trim string from left +static inline std::string <rim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), + std::not1(std::ptr_fun(std::isspace)))); + return s; +} + +// trim string from right +static inline std::string &rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), + std::not1(std::ptr_fun(std::isspace))).base(), s.end()); + return s; +} + +// trim string from left and right +static inline std::string &trim(std::string &s) { + return ltrim(rtrim(s)); +} + + +//----------------------------------------------------------------------------- +// Class to handle commands from the master +//----------------------------------------------------------------------------- +class CommandCallback: public ICommandHandler { +public: + + //CROB + virtual CommandStatus Select(const ControlRelayOutputBlock& command, uint16_t index) { + return CommandStatus::SUCCESS; + } + virtual CommandStatus Operate(const ControlRelayOutputBlock& command, uint16_t index, OperateType opType) { + auto code = command.functionCode; + CommandStatus return_val; + + if(code == ControlCode::LATCH_ON || code == ControlCode::LATCH_OFF) { + return_val = CommandStatus::SUCCESS; + + IEC_BOOL crob_val = (code == ControlCode::LATCH_ON); + pthread_mutex_lock(&bufferLock); + if(bool_output[index/8][index%8] != NULL) { + *bool_output[index/8][index%8] = crob_val; + } + pthread_mutex_unlock(&bufferLock); + } + else { + return_val = CommandStatus::NOT_SUPPORTED; + } + + return return_val; + } + + //Analog Out + virtual CommandStatus Select(const AnalogOutputInt16& command, uint16_t index) { + return CommandStatus::SUCCESS; + } + virtual CommandStatus Operate(const AnalogOutputInt16& command, uint16_t index, OperateType opType) { + auto ao_val = command.value; + pthread_mutex_lock(&bufferLock); + if(index < MIN_16B_RANGE && int_output[index] != NULL) { + *int_output[index] = ao_val; + } + else if(index < MAX_16B_RANGE && + int_memory[index - MIN_16B_RANGE] != NULL) { + *int_memory[index - MIN_16B_RANGE] = ao_val; + } + else if(index > MAX_16B_RANGE) { + return CommandStatus::OUT_OF_RANGE; + } + pthread_mutex_unlock(&bufferLock); + return CommandStatus::SUCCESS; + } + + //AnalogOut 32 (Int) + virtual CommandStatus Select(const AnalogOutputInt32& command, uint16_t index) { + return CommandStatus::SUCCESS; + } + virtual CommandStatus Operate(const AnalogOutputInt32& command, uint16_t index, OperateType opType) { + auto ao_val = command.value; + + if(index < MIN_32B_RANGE || index >= MAX_32B_RANGE) + return CommandStatus::OUT_OF_RANGE; + + pthread_mutex_lock(&bufferLock); + if(dint_memory[index - MIN_32B_RANGE] != NULL) { + *dint_memory[index - MIN_32B_RANGE] = ao_val; + } + pthread_mutex_unlock(&bufferLock); + + return CommandStatus::SUCCESS; + } + + //AnalogOut 32 (Float) + virtual CommandStatus Select(const AnalogOutputFloat32& command, uint16_t index) { + + return CommandStatus::SUCCESS; + } + virtual CommandStatus Operate(const AnalogOutputFloat32& command, uint16_t index, OperateType opType) { + auto ao_val = command.value; + + if(index < MIN_32B_RANGE || index >= MAX_32B_RANGE) + return CommandStatus::OUT_OF_RANGE; + + pthread_mutex_lock(&bufferLock); + if(dint_memory[index - MIN_32B_RANGE] != NULL) { + *dint_memory[index - MIN_32B_RANGE] = ao_val; + } + pthread_mutex_unlock(&bufferLock); + + return CommandStatus::SUCCESS; + } + + //AnalogOut 64 + virtual CommandStatus Select(const AnalogOutputDouble64& command, uint16_t index) { + return CommandStatus::SUCCESS; + } + virtual CommandStatus Operate(const AnalogOutputDouble64& command, uint16_t index, OperateType opType) { + auto ao_val = command.value; + + if(index < MIN_64B_RANGE || index >= MAX_64B_RANGE) + return CommandStatus::OUT_OF_RANGE; + + pthread_mutex_lock(&bufferLock); + if(lint_memory[index - MIN_64B_RANGE] != NULL) { + *lint_memory[index - MIN_64B_RANGE] = ao_val; + } + pthread_mutex_unlock(&bufferLock); + + return CommandStatus::SUCCESS; + } +protected: + void Start() final {} + void End() final {} +}; + +//------------------------------------------------------------------ +// Function to update DNP3 values every time they may have changed +//------------------------------------------------------------------ +void update_vals(std::shared_ptr outstation){ + UpdateBuilder builder; + // Update Discrete input (Binary input) + for(int i = 1; i < MAX_DISCRETE_INPUT; i++) { + builder.Update(Binary((bool)(*bool_input[i/8][i%8])), i); + + } + // Update Coils (Binary Output) + for(int i = 0; i < MAX_COILS; i++) { + builder.Update(BinaryOutputStatus((bool)(*bool_output[i/8][i%8])), i); + + } + // Update Input Registers (Analog Input) + for (int i = 0; i < MAX_INP_REGS; i++) { + builder.Update(Analog((int)(*int_input[i])), i); + + } + // Update Holding Registers (Analog Output) + for (int i = 0; i < MIN_16B_RANGE; i++) { + builder.Update(AnalogOutputStatus((int)(*int_output[i])), i); + } + // Update Holding registers for memory + for (int i = MIN_16B_RANGE; i < MAX_16B_RANGE; i++) { + if(int_memory[i - MIN_16B_RANGE] != NULL) + builder.Update( + AnalogOutputStatus((int)(*int_memory[i - MIN_16B_RANGE])), + i + ); + } + // Update Holding registers for 32 b memory + for (int i = MIN_32B_RANGE; i < MAX_32B_RANGE; i++) { + if(dint_memory[i - MIN_32B_RANGE] != NULL) + builder.Update( + AnalogOutputStatus((int)(*dint_memory[i - MIN_32B_RANGE])), + i + ); + } + // Update Holding registers for 64 b memory + for (int i = MIN_64B_RANGE; + (i < MAX_64B_RANGE && + i - MIN_64B_RANGE < sizeof(lint_memory) / sizeof(lint_memory[0])); + i++) { + if(lint_memory[i - MIN_64B_RANGE] != NULL) + builder.Update( + AnalogOutputStatus((int)(*lint_memory[i - MIN_64B_RANGE])), + i + ); + } + outstation->Apply(builder.Build()); +} + +//---------------------------------------------------------------------- +// Need to parse 'database_size' first +//---------------------------------------------------------------------- +OutstationStackConfig create_config() { + string line; + ifstream cfgfile("dnp3.cfg"); + bool found = false; + if(cfgfile.is_open()) { + while (getline(cfgfile, line)) { + if (line[0] == '#') + continue; + try { + istringstream iss(line); + string token; + getline(iss, token, '='); + token = trim(token); + if (token == "database_size") { + getline(iss, token, '='); + return OutstationStackConfig( + DatabaseSizes::AllTypes(atoi(token.c_str())) + ); + } + else + continue; + } catch(...) { + cout << "Malformatted Line: " << line << endl; + exit(1); + } + + } + } + return OutstationStackConfig(DatabaseSizes::AllTypes(10)); +} + +//---------------------------------------------------------------------- +// parse dnp3.cfg and set dnp3 settings +//---------------------------------------------------------------------- +OutstationStackConfig parseDNP3Config() { + string line; + ifstream cfgfile("dnp3.cfg"); + OutstationStackConfig config = create_config(); + if(cfgfile.is_open()) { + while (getline(cfgfile, line)) { + if (line[0] == '#') + continue; + try { + istringstream iss(line); + string token; + getline(iss, token, '='); + token = trim(token); + if (token == "local_address") { + getline(iss, token, '='); + config.link.LocalAddr = atoi(token.c_str()); + } else if (token == "remote_address") { + getline(iss, token, '='); + config.link.RemoteAddr = atoi(token.c_str()); + } else if (token == "keep_alive_timeout") { + getline(iss, token, '='); + if(token == "MAX") { + config.link.KeepAliveTimeout = + openpal::TimeDuration::Max(); + } + else { + config.link.KeepAliveTimeout = + openpal::TimeDuration::Seconds(atoi(token.c_str())); + } + } else if (token == "enable_unsolicited") { + getline(iss, token, '='); + if(token == "True") + config.outstation.params.allowUnsolicited = true; + else + config.outstation.params.allowUnsolicited = false; + } else if (token == "select_timeout") { + getline(iss, token, '='); + config.outstation.params.selectTimeout = + openpal::TimeDuration::Seconds(atoi(token.c_str())); + } else if (token == "max_controls_per_request") { + getline(iss, token, '='); + config.outstation.params.maxControlsPerRequest = + atoi(token.c_str()); + } else if (token == "max_rx_frag_size") { + getline(iss, token, '='); + config.outstation.params.maxRxFragSize = + atoi(token.c_str()); + } else if (token == "max_tx_frag_size") { + getline(iss, token, '='); + config.outstation.params.maxTxFragSize = + atoi(token.c_str()); + } else if (token == "event_buffer_size") { + getline(iss, token, '='); + config.outstation.eventBufferConfig = + EventBufferConfig::AllTypes(atoi(token.c_str())); + } else if (token == "sol_confirm_timeout") { + getline(iss, token, '='); + config.outstation.params.solConfirmTimeout = + openpal::TimeDuration::Milliseconds( + atoi(token.c_str()) + ); + } else if (token == "unsol_confirm_timeout") { + getline(iss, token, '='); + config.outstation.params.unsolConfirmTimeout = + openpal::TimeDuration::Milliseconds( + atoi(token.c_str()) + ); + } else if (token == "unsol_retry_timeout") { + getline(iss, token, '='); + config.outstation.params.unsolRetryTimeout = + openpal::TimeDuration::Milliseconds( + atoi(token.c_str()) + ); + } + } + catch(...) { + cout << "Malformatted Line: " << line << endl; + exit(1); + } + } + } + return config; +} + +//------------------------------------------------------------------ +//Function to begin DNP3 server functions +//------------------------------------------------------------------ +void dnp3StartServer(int port) { + + const uint32_t FILTERS = levels::NORMAL; + + // Allocate a single thread to the pool since this is a single outstation + // Log messages to the console + DNP3Manager manager(1, ConsoleLogger::Create()); + + // Create a listener server + auto channel = manager.AddTCPServer("server", FILTERS, ChannelRetry::Default(), "0.0.0.0", port, PrintingChannelListener::Create()); + + // Create a new outstation with a log level, command handler, and + // config info this returns a thread-safe interface used for + // updating the outstation's database. + std::shared_ptr cc = std::make_shared(); + auto outstation = channel->AddOutstation( + "outstation", + cc, + DefaultOutstationApplication::Create(), + parseDNP3Config() + ); + + // Enable the outstation and start communications + outstation->Enable(); + printf("DNP3 Enabled \n"); + + mapUnusedIO(); + + // Continuously update + struct timespec timer_start; + clock_gettime(CLOCK_MONOTONIC, &timer_start); + int i = 0; + for(;;) { + pthread_mutex_lock(&bufferLock); + update_vals(outstation); + pthread_mutex_unlock(&bufferLock); + sleep_until(&timer_start, OPLC_CYCLE); + } +} diff --git a/core/ladder.h b/core/ladder.h index f5bbe23..9f34502 100644 --- a/core/ladder.h +++ b/core/ladder.h @@ -88,6 +88,7 @@ void updateBuffers(); //main.cpp void sleep_thread(int milliseconds); void *modbusThread(); +void sleep_until(struct timespec *ts, int delay); //server.cpp void startServer(int port); @@ -96,6 +97,9 @@ void startServer(int port); int processModbusMessage(unsigned char *buffer, int bufferSize); void mapUnusedIO(); +//dnp3.cpp +void dnp3StartServer(int port); + //persistent_storage.cpp void *persistentStorage(void *args); int readPersistentStorage(); diff --git a/core/main.cpp b/core/main.cpp index ddced38..0f5aa81 100644 --- a/core/main.cpp +++ b/core/main.cpp @@ -32,13 +32,23 @@ #include "iec_types.h" #include "ladder.h" +#include +#include +#include +#include + + #define OPLC_CYCLE 50000000 +extern int opterr; extern int common_ticktime__; IEC_BOOL __DEBUG; static int tick = 0; +int modbus_port = 502; +int dnp3_port = 20000; + pthread_mutex_t bufferLock; //mutex for the internal buffers //----------------------------------------------------------------------------- @@ -53,7 +63,7 @@ void sleep_thread(int milliseconds) nanosleep(&ts, NULL); } -static void sleep_until(struct timespec *ts, int delay) +void sleep_until(struct timespec *ts, int delay) { ts->tv_nsec += delay; if(ts->tv_nsec >= 1000*1000*1000) @@ -66,7 +76,12 @@ static void sleep_until(struct timespec *ts, int delay) void *modbusThread(void *arg) { - startServer(502); + startServer(modbus_port); +} + +void *dnp3Thread(void *arg) +{ + dnp3StartServer(dnp3_port); } double measureTime(struct timespec *timer_start) @@ -82,8 +97,46 @@ double measureTime(struct timespec *timer_start) return time_used; } +void print_usage() { + printf("Usage: ./openplc -m modbus_port -d dnp3_port\n"); + printf("./openplc will run with modbus on port 502 and "); + printf("dnp3 on port 20000\n"); + printf("Selecting only modbus or only dnp3 will only run that "); + printf("protocol\n"); +} + int main(int argc,char **argv) { + + bool modbus_flag = false; + bool dnp3_flag = false; + + int opt; + opterr = 0; + + while ((opt = getopt (argc, argv, "m:d:")) != -1) { + switch (opt) { + case 'm': + modbus_flag = true; + modbus_port = atoi(optarg); + break; + case 'd': + dnp3_flag = true; + dnp3_port = atoi(optarg); + break; + case '?': + if (isprint (optopt)) + fprintf (stderr, "Unknown option `-%c'.\n", optopt); + else + fprintf (stderr, "Unknown option character `\\x%x'.\n", optopt); + print_usage(); + exit(1); + break; + default: + abort(); + } + } + setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); printf("OpenPLC Software running...\n"); @@ -108,8 +161,15 @@ int main(int argc,char **argv) //====================================================== initializeHardware(); updateBuffers(); - pthread_t thread; - pthread_create(&thread, NULL, modbusThread, NULL); + pthread_t modbus_thread; + pthread_t dnp3_thread; + + if(modbus_flag || (!modbus_flag && !dnp3_flag)) { + pthread_create(&modbus_thread, NULL, modbusThread, NULL); + } + if(dnp3_flag || (!modbus_flag && !dnp3_flag)) { + pthread_create(&dnp3_thread, NULL, dnp3Thread, NULL); + } //====================================================== // PERSISTENT STORAGE INITIALIZATION diff --git a/dnp3.cfg b/dnp3.cfg new file mode 100644 index 0000000..5ec272a --- /dev/null +++ b/dnp3.cfg @@ -0,0 +1,61 @@ +# ---------------------------------------------------------------- +# Configuration file for DNP3 +#----------------------------------------------------------------- + + +# Use this file to fill out DNP3 settings for your Open PLC +# Uncomment settings as you want them + + +# Link Settings +#----------------------------------------------------------------- + +# local address +local_address = 10 + +# master address allowed +remote_address = 1 + +# keep alive timeout +# time (s) or MAX +# keep_alive_timeout = MAX + + +# Parameters +#----------------------------------------------------------------- + +# enable unsolicited reporting if master allows it +# True or False +enable_unsolicited = True + +# how long (seconds) the outstation will allow a operate +# to follow a select +# select_timeout = 10 + +# max control commands for a single APDU +# max_controls_per_request = 16 + +# maximum fragment size the outstation will recieve +# default is the max value +# max_rx_frag_size = 2048 + +# maximum fragment size the outstation will send if +# it needs to fragment. Default is the max falue +# max_tx_frag_size = 2048 + +# size of the event buffer +event_buffer_size = 10 + +# number of values the outstation will report at once +# AKA database size +database_size = 10 + +#Timeout for solicited confirms +# in MS +# sol_confirm_timeout = 5000 + +#Timeout for unsolicited confirms (ms) +# unsol_conrfirm_timeout = 5000 + +#Timeout for unsolicited retries (ms) +# unsol_retry_timeout = 5000