Commit a8bc4204 authored by Ivan Tyagov's avatar Ivan Tyagov

Extend PLC Poc to control 4 relays.

parent f6dc8932
__LOCATED_VAR(BOOL,__QX0_0_0_0,Q,X,0,0,0,0)
__LOCATED_VAR(BOOL,__QX0_0_0_0,Q,X,0,0,0,0)
__LOCATED_VAR(BOOL,__QX0_0_1_1,Q,X,0,0,1,1)
__LOCATED_VAR(BOOL,__QX0_0_2_2,Q,X,0,0,2,2)
__LOCATED_VAR(BOOL,__QX0_0_3_3,Q,X,0,0,3,3)
/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */
/*
* Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
*
* This file is part of the Modbus library for Beremiz and matiec.
*
* This Modbus library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this Modbus library. If not, see <http://www.gnu.org/licenses/>.
*
* This code is made available on the understanding that it will not be
* used in safety-critical situations without a full and competent review.
*/
#include <stdio.h>
#include <string.h> /* required for memcpy() */
#include <errno.h>
#include <time.h>
#include <signal.h>
#include <unistd.h> /* required for pause() */
#include "mb_slave_and_master.h"
#include "MB_0.h"
#define MAX_MODBUS_ERROR_CODE 11
static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = {
/* 0 */ "", /* un-used -> no error! */
/* 1 */ "illegal/unsuported function",
/* 2 */ "illegal data address",
/* 3 */ "illegal data value",
/* 4 */ "slave device failure",
/* 5 */ "acknowledge -> slave intends to reply later",
/* 6 */ "slave device busy",
/* 7 */ "negative acknowledge",
/* 8 */ "memory parity error",
/* 9 */ "", /* undefined by Modbus */
/* 10*/ "gateway path unavalilable",
/* 11*/ "gateway target device failed to respond"
};
/* Execute a modbus client transaction/request */
static int __execute_mb_request(int request_id){
switch (client_requests[request_id].mb_function){
case 1: /* read coils */
return read_output_bits(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].count,
client_requests[request_id].coms_buffer,
(int) client_requests[request_id].count,
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 2: /* read discrete inputs */
return read_input_bits( client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].count,
client_requests[request_id].coms_buffer,
(int) client_requests[request_id].count,
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 3: /* read holding registers */
return read_output_words(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].count,
client_requests[request_id].coms_buffer,
(int) client_requests[request_id].count,
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 4: /* read input registers */
return read_input_words(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].count,
client_requests[request_id].coms_buffer,
(int) client_requests[request_id].count,
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 5: /* write single coil */
return write_output_bit(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].coms_buffer[0],
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 6: /* write single register */
return write_output_word(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].coms_buffer[0],
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 7: break; /* function not yet supported */
case 8: break; /* function not yet supported */
case 9: break; /* function not yet supported */
case 10: break; /* function not yet supported */
case 11: break; /* function not yet supported */
case 12: break; /* function not yet supported */
case 13: break; /* function not yet supported */
case 14: break; /* function not yet supported */
case 15: /* write multiple coils */
return write_output_bits(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].count,
client_requests[request_id].coms_buffer,
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
case 16: /* write multiple registers */
return write_output_words(client_requests[request_id].slave_id,
client_requests[request_id].address,
client_requests[request_id].count,
client_requests[request_id].coms_buffer,
client_nodes[client_requests[request_id].client_node_id].mb_nd,
client_requests[request_id].retries,
&(client_requests[request_id].mb_error_code),
&(client_requests[request_id].resp_timeout),
&(client_requests[request_id].coms_buf_mutex));
default: break; /* should never occur, if file generation is correct */
}
fprintf(stderr, "Modbus plugin: Modbus function %d not supported\n", request_id); /* should never occur, if file generation is correct */
return -1;
}
/* pack bits from unpacked_data to packed_data */
static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) {
u8 bit;
u16 byte, coils_processed;
if ((0 == bit_count) || (65535-start_addr < bit_count-1))
return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
packed_data[byte] = 0;
for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) {
if(unpacked_data[start_addr + coils_processed])
packed_data[byte] |= bit; /* set bit */
else packed_data[byte] &= ~bit; /* reset bit */
}
}
return 0;
}
/* unpack bits from packed_data to unpacked_data */
static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) {
u8 temp, bit;
u16 byte, coils_processed;
if ((0 == bit_count) || (65535-start_addr < bit_count-1))
return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
temp = packed_data[byte] ;
for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) {
unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0;
}
}
return 0;
}
static int __read_inbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) {
int res = __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);
if (res >= 0) {
/* update the flag and counter of Modbus requests we have processed. */
((server_mem_t *)mem_map)->flag_read_req_counter++;
((server_mem_t *)mem_map)->flag_read_req_flag = 1;
}
return res;
}
static int __read_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) {
int res = __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);
if (res >= 0) {
/* update the flag and counter of Modbus requests we have processed. */
((server_mem_t *)mem_map)->flag_read_req_counter++;
((server_mem_t *)mem_map)->flag_read_req_flag = 1;
}
return res;
}
static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) {
int res = __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);
if (res >= 0) {
/* update the flag and counter of Modbus requests we have processed. */
((server_mem_t *)mem_map)->flag_write_req_counter++;
((server_mem_t *)mem_map)->flag_write_req_flag = 1;
}
return res;
}
static int __read_inwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
if ((start_addr + word_count) > MEM_AREA_SIZE)
return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
/* update the flag and counter of Modbus requests we have processed. */
((server_mem_t *)mem_map)->flag_read_req_counter++;
((server_mem_t *)mem_map)->flag_read_req_flag = 1;
/* use memcpy() because loop with pointers (u16 *) caused alignment problems */
memcpy(/* dest */ (void *)data_words,
/* src */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]),
/* size */ word_count * 2);
return 0;
}
static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
if ((start_addr + word_count) > MEM_AREA_SIZE)
return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
/* update the flag and counter of Modbus requests we have processed. */
((server_mem_t *)mem_map)->flag_read_req_counter++;
((server_mem_t *)mem_map)->flag_read_req_flag = 1;
/* use memcpy() because loop with pointers (u16 *) caused alignment problems */
memcpy(/* dest */ (void *)data_words,
/* src */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
/* size */ word_count * 2);
return 0;
}
static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
if ((start_addr + word_count) > MEM_AREA_SIZE)
return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
/* update the flag and counter of Modbus requests we have processed. */
((server_mem_t *)mem_map)->flag_write_req_counter++;
((server_mem_t *)mem_map)->flag_write_req_flag = 1;
/* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned.
* It is not therefore safe to cast it to an u16 data type.
* The following code cannot be used. memcpy() is used instead.
*/
/*
for (count = 0; count < word_count ; count++)
((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count];
*/
memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
/* src */ (void *)data_words,
/* size */ word_count * 2);
return 0;
}
#include <pthread.h>
static void *__mb_server_thread(void *_server_node) {
server_node_t *server_node = _server_node;
mb_slave_callback_t callbacks = {
&__read_inbits,
&__read_outbits,
&__write_outbits,
&__read_inwords,
&__read_outwords,
&__write_outwords,
(void *)&(server_node->mem_area)
};
// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
// mb_slave_run() should never return!
mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id);
fprintf(stderr, "Modbus plugin: Modbus server for node %s died unexpectedly!\n", server_node->location); /* should never occur */
return NULL;
}
#define timespec_add(ts, sec, nsec) { \
ts.tv_sec += sec; \
ts.tv_nsec += nsec; \
if (ts.tv_nsec >= 1000000000) { \
ts.tv_sec ++; \
ts.tv_nsec -= 1000000000; \
} \
}
static void *__mb_client_thread(void *_index) {
int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast)
// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
/* loop the communication with the client
*
* When the client thread has difficulty communicating with remote client and/or server (network issues, for example),
* then the communications get delayed and we will fall behind in the period.
*
* This is OK. Note that if the condition variable were to be signaled multiple times while the client thread is inside the same
* Modbus transaction, then all those signals would be ignored.
* However, and since we keep the mutex locked during the communication cycle, it is not possible to signal the condition variable
* during that time (it is only possible while the thread is blocked during the call to pthread_cond_wait().
*
* This means that when network issues eventually get resolved, we will NOT have a bunch of delayed activations to handle
* in quick succession (which would goble up CPU time).
*
* Notice that the above property is valid whether the communication cycle is run with the mutex locked, or unlocked.
* Since it makes it easier to implement the correct semantics for the other activation methods if the communication cycle
* is run with the mutex locked, then that is what we do.
*
* Note that during all the communication cycle we will keep locked the mutex
* (i.e. the mutex used together with the condition variable that will activate a new communication cycle)
*
* Note that we never get to explicitly unlock this mutex. It will only be unlocked by the pthread_cond_wait()
* call at the end of the cycle.
*/
pthread_mutex_lock(&(client_nodes[client_node_id].mutex));
while (1) {
/*
struct timespec cur_time;
clock_gettime(CLOCK_MONOTONIC, &cur_time);
fprintf(stderr, "Modbus client thread (%d) - new cycle (%ld:%ld)!\n", client_node_id, cur_time.tv_sec, cur_time.tv_nsec);
*/
int req;
for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){
/* just do the requests belonging to the client */
if (client_requests[req].client_node_id != client_node_id)
continue;
/* only do the request if:
* - this request was explictly asked to be executed by the client program
* OR
* - the client thread was activated periodically
* (in which case we execute all the requests belonging to the client node)
*/
if ((client_requests[req].flag_exec_req == 0) && (client_nodes[client_requests[req].client_node_id].periodic_act == 0))
continue;
/*
fprintf(stderr, "Modbus client thread (%d): RUNNING Modbus request %d (periodic = %d flag_exec_req = %d)\n",
client_node_id, req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req );
*/
int res_tmp = __execute_mb_request(req);
client_requests[req].tn_error_code = 0; // assume success
switch (res_tmp) {
case PORT_FAILURE: {
if (res_tmp != client_nodes[client_node_id].prev_error)
fprintf(stderr, "Modbus plugin: Error connecting Modbus client %s to remote server.\n", client_nodes[client_node_id].location);
client_nodes[client_node_id].prev_error = res_tmp;
client_requests[req].tn_error_code = 1; // error accessing IP network, or serial interface
break;
}
case INVALID_FRAME: {
if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
fprintf(stderr, "Modbus plugin: Modbus client request configured at location %s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location);
client_requests[req].prev_error = res_tmp;
client_requests[req].tn_error_code = 2; // reply received from server was an invalid frame
break;
}
case TIMEOUT: {
if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
fprintf(stderr, "Modbus plugin: Modbus client request configured at location %s timed out waiting for reply from server.\n", client_requests[req].location);
client_requests[req].prev_error = res_tmp;
client_requests[req].tn_error_code = 3; // server did not reply before timeout expired
break;
}
case MODBUS_ERROR: {
if (client_requests[req].prev_error != client_requests[req].mb_error_code) {
fprintf(stderr, "Modbus plugin: Modbus client request configured at location %s was unsuccesful. Server/slave returned error code 0x%2x", client_requests[req].location, client_requests[req].mb_error_code);
if (client_requests[req].mb_error_code <= MAX_MODBUS_ERROR_CODE ) {
fprintf(stderr, "(%s)", modbus_error_messages[client_requests[req].mb_error_code]);
fprintf(stderr, ".\n");
}
}
client_requests[req].prev_error = client_requests[req].mb_error_code;
client_requests[req].tn_error_code = 4; // server returned a valid Modbus error frame
break;
}
default: {
if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) {
fprintf(stderr, "Modbus plugin: Modbus client %s has reconnected to server/slave.\n", client_nodes[client_node_id].location);
}
if ((res_tmp >= 0) && (client_requests[req] .prev_error != 0)) {
fprintf(stderr, "Modbus plugin: Modbus client request configured at location %s has succesfully resumed comunication.\n", client_requests[req].location);
}
client_nodes[client_node_id].prev_error = 0;
client_requests[req] .prev_error = 0;
break;
}
}
/* Set the flag_tn_error_code and flag_mb_error_code that are mapped onto
* located BYTE variables, so the user program
* knows how the communication is going.
*/
client_requests[req].flag_mb_error_code = client_requests[req].mb_error_code;
client_requests[req].flag_tn_error_code = client_requests[req].tn_error_code;
/* We have just finished excuting a client transcation request.
* If the current cycle was activated by user request we reset the flag used to ask to run it
*/
if (0 != client_requests[req].flag_exec_req) {
client_requests[req].flag_exec_req = 0;
client_requests[req].flag_exec_started = 0;
}
//fprintf(stderr, "Modbus plugin: RUNNING<---> of Modbus request %d (periodic = %d flag_exec_req = %d)\n",
// req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req );
}
// Wait for signal (from timer or explicit request from user program) before starting the next cycle
{
// No need to lock the mutex. Is is already locked just before the while(1) loop.
// Read the comment there to understand why.
// pthread_mutex_lock(&(client_nodes[client_node_id].mutex));
/* the client thread has just finished a cycle, so all the flags used to signal an activation
* and specify the activation source (periodic, user request, ...)
* get reset here, before waiting for a new activation.
*/
client_nodes[client_node_id].periodic_act = 0;
client_nodes[client_node_id].execute_req = 0;
while (client_nodes[client_node_id].execute_req == 0)
pthread_cond_wait(&(client_nodes[client_node_id].condv),
&(client_nodes[client_node_id].mutex));
// We run the communication cycle with the mutex locked.
// Read the comment just above the while(1) to understand why.
// pthread_mutex_unlock(&(client_nodes[client_node_id].mutex));
}
}
// humour the compiler.
return NULL;
}
/* Thread that simply implements a periodic 'timer',
* i.e. periodically sends signal to the thread running __mb_client_thread()
*
* Note that we do not use a posix timer (timer_create() ) because there doesn't seem to be a way
* of having the timer notify the thread that is portable across Xenomai and POSIX.
* - SIGEV_THREAD : not supported by Xenomai
* - SIGEV_THREAD_ID : Linux specific (i.e. non POSIX)
* Even so, I did not get it to work under Linux (issues with the header files)
* - SIGEV_SIGNAL : Will not work, as signal is sent to random thread in process!
*/
static void *__mb_client_timer_thread(void *_index) {
int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast)
struct timespec next_cycle;
int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */
int period_nsec = (client_nodes[client_node_id].comm_period %1000)*1000000; /* comm_period is in ms */
// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
if (client_nodes[client_node_id].comm_period <= 0) {
// No periodic activation required => nothing to do!
while (1) pause(); // wait to be canceled when program terminates (shutdown() is called)
return NULL; // not really necessary, just makes it easier to understand the code.
}
// get the current time
clock_gettime(CLOCK_MONOTONIC, &next_cycle);
while(1) {
// Determine absolute time instant for starting the next cycle
struct timespec prev_cycle, now;
prev_cycle = next_cycle;
timespec_add(next_cycle, period_sec, period_nsec);
/* NOTE:
* It is probably un-necessary to check for overflow of timer!
* Even in 32 bit systems this will take at least 68 years since the computer booted
* (remember, we are using CLOCK_MONOTONIC, which should start counting from 0
* every time the system boots). On 64 bit systems, it will take over
* 10^11 years to overflow.
*/
clock_gettime(CLOCK_MONOTONIC, &now);
if (next_cycle.tv_sec < prev_cycle.tv_sec) {
/* Timer overflow. See NOTE B above */
next_cycle = now;
timespec_add(next_cycle, period_sec, period_nsec);
}
while (0 != clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL));
/* signal the client node's condition variable on which the client node's thread should be waiting... */
/* Since the communication cycle is run with the mutex locked, we use trylock() instead of lock() */
if (pthread_mutex_trylock (&(client_nodes[client_node_id].mutex)) == 0) {
client_nodes[client_node_id].execute_req = 1; // tell the thread to execute
client_nodes[client_node_id].periodic_act = 1; // tell the thread the activation was done by periodic timer
pthread_cond_signal (&(client_nodes[client_node_id].condv));
pthread_mutex_unlock(&(client_nodes[client_node_id].mutex));
} else {
/* We never get to signal the thread for activation. But that is OK.
* If it still in the communication cycle (during which the mutex is kept locked)
* then that means that the communication cycle is falling behing in the periodic
* communication cycle, and we therefore need to skip a period.
*/
}
}
return NULL; // humour the compiler -> will never be executed!
}
int __cleanup_0 ();
int __init_0 (int argc, char **argv){
int index;
for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) {
client_nodes[index].mb_nd = -1;
/* see comment in mb_runtime.h to understad why we need to initialize these entries */
switch (client_nodes[index].node_address.naf) {
case naf_tcp:
client_nodes[index].node_address.addr.tcp.host = client_nodes[index].str1;
client_nodes[index].node_address.addr.tcp.service = client_nodes[index].str2;
break;
case naf_rtu:
client_nodes[index].node_address.addr.rtu.device = client_nodes[index].str1;
break;
}
}
for (index=0; index < NUMBER_OF_SERVER_NODES;index++) {
// mb_nd with negative numbers indicate how far it has been initialised (or not)
// -2 --> no modbus node created; no thread created
// -1 --> modbus node created!; no thread created
// >=0 --> modbus node created!; thread created!
server_nodes[index].mb_nd = -2;
server_nodes[index].mem_area.flag_write_req_flag = 0;
server_nodes[index].mem_area.flag_write_req_counter = 0;
server_nodes[index].mem_area.flag_read_req_counter = 0;
server_nodes[index].mem_area.flag_read_req_flag = 0;
/* see comment in mb_runtime.h to understad why we need to initialize these entries */
switch (server_nodes[index].node_address.naf) {
case naf_tcp:
server_nodes[index].node_address.addr.tcp.host = server_nodes[index].str1;
server_nodes[index].node_address.addr.tcp.service = server_nodes[index].str2;
break;
case naf_rtu:
server_nodes[index].node_address.addr.rtu.device = server_nodes[index].str1;
break;
}
}
/* modbus library init */
/* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus
* extension currently in the user's project. This file (MB_xx.c) is handling only one instance,
* but must initialize the library for all instances. Only the first call to mb_slave_and_master_init()
* will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored
* by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments.
*/
if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) {
fprintf(stderr, "Modbus plugin: Error starting modbus library\n");
// return imediately. Do NOT goto error_exit, as we did not get to
// start the modbus library!
return -1;
}
/* init each client request */
/* Must be done _before_ launching the client threads!! */
for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
/* make sure flags connected to user program MB transaction start request are all reset */
client_requests[index].flag_exec_req = 0;
client_requests[index].flag_exec_started = 0;
/* init the mutex for each client request */
/* Must be done _before_ launching the client threads!! */
if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) {
fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %s\n", client_nodes[client_requests[index].client_node_id].location);
goto error_exit;
}
}
/* init each client connection to remote modbus server, and launch thread */
/* NOTE: All client_nodes[].init_state are initialised to 0 in the code
* generated by the modbus plugin
*/
for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){
/* establish client connection */
client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address);
if (client_nodes[index].mb_nd < 0){
fprintf(stderr, "Modbus plugin: Error creating modbus client node %s\n", client_nodes[index].location);
goto error_exit;
}
client_nodes[index].init_state = 1; // we have created the node
/* initialize the mutex variable that will be used by the thread handling the client node */
bzero(&(client_nodes[index].mutex), sizeof(pthread_mutex_t));
if (pthread_mutex_init(&(client_nodes[index].mutex), NULL) < 0) {
fprintf(stderr, "Modbus plugin: Error creating mutex for modbus client node %s\n", client_nodes[index].location);
goto error_exit;
}
client_nodes[index].init_state = 2; // we have created the mutex
/* initialize the condition variable that will be used by the thread handling the client node */
bzero(&(client_nodes[index].condv), sizeof(pthread_cond_t));
if (pthread_cond_init(&(client_nodes[index].condv), NULL) < 0) {
fprintf(stderr, "Modbus plugin: Error creating condition variable for modbus client node %s\n", client_nodes[index].location);
goto error_exit;
}
client_nodes[index].execute_req = 0; //variable associated with condition variable
client_nodes[index].init_state = 3; // we have created the condition variable
/* launch a thread to handle this client node timer */
{
int res = 0;
pthread_attr_t attr;
res |= pthread_attr_init(&attr);
res |= pthread_create(&(client_nodes[index].timer_thread_id), &attr, &__mb_client_timer_thread, (void *)((char *)NULL + index));
if (res != 0) {
fprintf(stderr, "Modbus plugin: Error starting timer thread for modbus client node %s\n", client_nodes[index].location);
goto error_exit;
}
}
client_nodes[index].init_state = 4; // we have created the timer
/* launch a thread to handle this client node */
{
int res = 0;
pthread_attr_t attr;
res |= pthread_attr_init(&attr);
res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
if (res != 0) {
fprintf(stderr, "Modbus plugin: Error starting thread for modbus client node %s\n", client_nodes[index].location);
goto error_exit;
}
}
client_nodes[index].init_state = 5; // we have created the thread
}
/* init each local server */
/* NOTE: All server_nodes[].init_state are initialised to 0 in the code
* generated by the modbus plugin
*/
for (index=0; index < NUMBER_OF_SERVER_NODES;index++){
/* create the modbus server */
server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address);
if (server_nodes[index].mb_nd < 0){
fprintf(stderr, "Modbus plugin: Error creating modbus server node %s\n", server_nodes[index].location);
goto error_exit;
}
server_nodes[index].init_state = 1; // we have created the node
/* launch a thread to handle this server node */
{
int res = 0;
pthread_attr_t attr;
res |= pthread_attr_init(&attr);
res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index]));
if (res != 0) {
fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %s\n", server_nodes[index].location);
goto error_exit;
}
}
server_nodes[index].init_state = 2; // we have created the node and thread
}
return 0;
error_exit:
__cleanup_0 ();
return -1;
}
void __publish_0 (){
int index;
for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
/* synchronize the PLC and MB buffers only for the output requests */
if (client_requests[index].req_type == req_output){
// lock the mutex brefore copying the data
if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
// Check if user configured this MB request to be activated whenever the data to be written changes
if (client_requests[index].write_on_change) {
// Let's check if the data did change...
// compare the data in plcv_buffer to coms_buffer
int res;
res = memcmp((void *)client_requests[index].coms_buffer /* buf 1 */,
(void *)client_requests[index].plcv_buffer /* buf 2*/,
REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
// if data changed, activate execution request
if (0 != res)
client_requests[index].flag_exec_req = 1;
}
// copy from plcv_buffer to coms_buffer
memcpy((void *)client_requests[index].coms_buffer /* destination */,
(void *)client_requests[index].plcv_buffer /* source */,
REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
}
}
/* if the user program set the execution request flag, then activate the thread
* that handles this Modbus client transaction so it gets a chance to be executed
* (but don't activate the thread if it has already been activated!)
*
* NOTE that we do this, for both the IN and OUT mapped location, under this
* __publish_() function. The scan cycle of the PLC works as follows:
* - call __retrieve_()
* - execute user programs
* - call __publish_()
* - insert <delay> until time to start next periodic/cyclic scan cycle
*
* In an attempt to be able to run the MB transactions during the <delay>
* interval in which not much is going on, we handle the user program
* requests to execute a specific MB transaction in this __publish_()
* function.
*/
if ((client_requests[index].flag_exec_req != 0) && (0 == client_requests[index].flag_exec_started)) {
int client_node_id = client_requests[index].client_node_id;
/* We TRY to signal the client thread.
* We do this because this function can be called at the end of the PLC scan cycle
* and we don't want it to block at that time.
*/
if (pthread_mutex_trylock(&(client_nodes[client_node_id].mutex)) == 0) {
client_nodes[client_node_id].execute_req = 1; // tell the thread to execute
pthread_cond_signal (&(client_nodes[client_node_id].condv));
pthread_mutex_unlock(&(client_nodes[client_node_id].mutex));
/* - upon success, set flag_exec_started
* - both flags (flag_exec_req and flag_exec_started) will be reset
* once the transaction has completed.
*/
client_requests[index].flag_exec_started = 1;
} else {
/* The mutex is locked => the client thread is currently executing MB transactions.
* We will try to activate it in the next PLC cycle...
* For now, do nothing.
*/
}
}
}
}
void __retrieve_0 (){
int index;
for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
/*just do the input requests */
if (client_requests[index].req_type == req_input){
if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
// copy from coms_buffer to plcv_buffer
memcpy((void *)client_requests[index].plcv_buffer /* destination */,
(void *)client_requests[index].coms_buffer /* source */,
REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
}
}
}
}
int __cleanup_0 (){
int index, close;
int res = 0;
/* kill thread and close connections of each modbus client node */
for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) {
close = 0;
if (client_nodes[index].init_state >= 5) {
// thread was launched, so we try to cancel it!
close = pthread_cancel(client_nodes[index].thread_id);
close |= pthread_join (client_nodes[index].thread_id, NULL);
if (close < 0)
fprintf(stderr, "Modbus plugin: Error closing thread for modbus client node %s\n", client_nodes[index].location);
}
res |= close;
close = 0;
if (client_nodes[index].init_state >= 4) {
// timer thread was launched, so we try to cancel it!
close = pthread_cancel(client_nodes[index].timer_thread_id);
close |= pthread_join (client_nodes[index].timer_thread_id, NULL);
if (close < 0)
fprintf(stderr, "Modbus plugin: Error closing timer thread for modbus client node %s\n", client_nodes[index].location);
}
res |= close;
close = 0;
if (client_nodes[index].init_state >= 3) {
// condition variable was created, so we try to destroy it!
close = pthread_cond_destroy(&(client_nodes[index].condv));
if (close < 0)
fprintf(stderr, "Modbus plugin: Error destroying condition variable for modbus client node %s\n", client_nodes[index].location);
}
res |= close;
close = 0;
if (client_nodes[index].init_state >= 2) {
// mutex was created, so we try to destroy it!
close = pthread_mutex_destroy(&(client_nodes[index].mutex));
if (close < 0)
fprintf(stderr, "Modbus plugin: Error destroying mutex for modbus client node %s\n", client_nodes[index].location);
}
res |= close;
close = 0;
if (client_nodes[index].init_state >= 1) {
// modbus client node was created, so we try to close it!
close = mb_master_close (client_nodes[index].mb_nd);
if (close < 0){
fprintf(stderr, "Modbus plugin: Error closing modbus client node %s\n", client_nodes[index].location);
// We try to shut down as much as possible, so we do not return noW!
}
client_nodes[index].mb_nd = -1;
}
res |= close;
client_nodes[index].init_state = 0;
}
//fprintf(stderr, "Modbus plugin: __cleanup_%s() 5 close=%d res=%d\n", client_nodes[index].location, close, res);
/* kill thread and close connections of each modbus server node */
for (index=0; index < NUMBER_OF_SERVER_NODES; index++) {
close = 0;
if (server_nodes[index].init_state >= 2) {
// thread was launched, so we try to cancel it!
close = pthread_cancel(server_nodes[index].thread_id);
close |= pthread_join (server_nodes[index].thread_id, NULL);
if (close < 0)
fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %s\n", server_nodes[index].location);
}
res |= close;
close = 0;
if (server_nodes[index].init_state >= 1) {
// modbus server node was created, so we try to close it!
close = mb_slave_close (server_nodes[index].mb_nd);
if (close < 0) {
fprintf(stderr, "Modbus plugin: Error closing node for modbus server %s (%d)\n", server_nodes[index].location, server_nodes[index].mb_nd);
// We try to shut down as much as possible, so we do not return noW!
}
server_nodes[index].mb_nd = -1;
}
res |= close;
server_nodes[index].init_state = 0;
}
/* destroy the mutex of each client request */
for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) {
if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) {
fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %s\n", client_nodes[client_requests[index].client_node_id].location);
// We try to shut down as much as possible, so we do not return noW!
res |= -1;
}
}
/* modbus library close */
//fprintf(stderr, "Shutting down modbus library...\n");
if (mb_slave_and_master_done()<0) {
fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n");
res |= -1;
}
return res;
}
/**********************************************/
/** Functions for Beremiz web interface. **/
/**********************************************/
/*
* Beremiz has a program to run on the PLC (Beremiz_service.py)
* to handle downloading of compiled programs, start/stop of PLC, etc.
* (see runtime/PLCObject.py for start/stop, loading, ...)
*
* This service also includes a web server to access PLC state (start/stop)
* and to change some basic confiuration parameters.
* (see runtime/NevowServer.py for the web server)
*
* The web server allows for extensions, where additional configuration
* parameters may be changed on the running/downloaded PLC.
* Modbus plugin also comes with an extension to the web server, through
* which the basic Modbus plugin configuration parameters may be changed
*
* These parameters are changed _after_ the code (.so file) is loaded into
* memmory. These changes may be applied before (or after) the code starts
* running (i.e. before or after __init_() ets called)!
*
* The following functions are never called from other C code. They are
* called instead from the python code in runtime/Modbus_config.py, that
* implements the web server extension for configuring Modbus parameters.
*/
/* The number of Cient nodes (i.e. the number of entries in the client_nodes array)
* The number of Server nodes (i.e. the numb. of entries in the server_nodes array)
*
* These variables are also used by the Modbus web config code to determine
* whether the current loaded PLC includes the Modbus plugin
* (so it should make the Modbus parameter web interface visible to the user).
*/
const int __modbus_plugin_client_node_count = NUMBER_OF_CLIENT_NODES;
const int __modbus_plugin_server_node_count = NUMBER_OF_SERVER_NODES;
const int __modbus_plugin_param_string_size = MODBUS_PARAM_STRING_SIZE;
/* NOTE: We could have the python code in runtime/Modbus_config.py
* directly access the server_node_t and client_node_t structures,
* however this would create a tight coupling between these two
* disjoint pieces of code.
* Any change to the server_node_t or client_node_t structures would
* require the python code to be changed accordingly. I have therefore
* opted to create get/set functions, one for each parameter.
*
* We also convert the enumerated constants naf_ascii, etc...
* (from node_addr_family_t in modbus/mb_addr.h)
* into strings so as to decouple the python code that will be calling
* these functions from the Modbus library code definitions.
*/
const char *addr_type_str[] = {
[naf_ascii] = "ascii",
[naf_rtu ] = "rtu",
[naf_tcp ] = "tcp"
};
#define __safe_strcnpy(str_dest, str_orig, max_size) { \
strncpy(str_dest, str_orig, max_size); \
str_dest[max_size - 1] = '\0'; \
}
/* NOTE: The host, port and device parameters are strings that may be changed
* (by calling the following functions) after loading the compiled code
* (.so file) into memory, but before the code starts running
* (i.e. before __init_() gets called).
* This means that the host, port and device parameters may be changed
* _before_ they get mapped onto the str1 and str2 variables by __init_(),
* which is why the following functions must access the str1 and str2
* parameters directly.
*/
const char * __modbus_get_ClientNode_config_name(int nodeid) {return client_nodes[nodeid].config_name; }
const char * __modbus_get_ClientNode_host (int nodeid) {return client_nodes[nodeid].str1; }
const char * __modbus_get_ClientNode_port (int nodeid) {return client_nodes[nodeid].str2; }
const char * __modbus_get_ClientNode_device (int nodeid) {return client_nodes[nodeid].str1; }
int __modbus_get_ClientNode_baud (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.baud; }
int __modbus_get_ClientNode_parity (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.parity; }
int __modbus_get_ClientNode_stop_bits (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.stop_bits;}
u64 __modbus_get_ClientNode_comm_period(int nodeid) {return client_nodes[nodeid].comm_period; }
const char * __modbus_get_ClientNode_addr_type (int nodeid) {return addr_type_str[client_nodes[nodeid].node_address.naf];}
const char * __modbus_get_ServerNode_config_name(int nodeid) {return server_nodes[nodeid].config_name; }
const char * __modbus_get_ServerNode_host (int nodeid) {char*x=server_nodes[nodeid].str1; return (x[0]=='\0'?"#ANY#":x); }
const char * __modbus_get_ServerNode_port (int nodeid) {return server_nodes[nodeid].str2; }
const char * __modbus_get_ServerNode_device (int nodeid) {return server_nodes[nodeid].str1; }
int __modbus_get_ServerNode_baud (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.baud; }
int __modbus_get_ServerNode_parity (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.parity; }
int __modbus_get_ServerNode_stop_bits (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.stop_bits;}
u8 __modbus_get_ServerNode_slave_id (int nodeid) {return server_nodes[nodeid].slave_id; }
const char * __modbus_get_ServerNode_addr_type (int nodeid) {return addr_type_str[server_nodes[nodeid].node_address.naf];}
void __modbus_set_ClientNode_host (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);}
void __modbus_set_ClientNode_port (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str2, value, MODBUS_PARAM_STRING_SIZE);}
void __modbus_set_ClientNode_device (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);}
void __modbus_set_ClientNode_baud (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.baud = value;}
void __modbus_set_ClientNode_parity (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.parity = value;}
void __modbus_set_ClientNode_stop_bits (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.stop_bits = value;}
void __modbus_set_ClientNode_comm_period(int nodeid, u64 value) {client_nodes[nodeid].comm_period = value;}
void __modbus_set_ServerNode_host (int nodeid, const char * value) {if (strcmp(value,"#ANY#")==0) value = "";
__safe_strcnpy(server_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);}
void __modbus_set_ServerNode_port (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str2, value, MODBUS_PARAM_STRING_SIZE);}
void __modbus_set_ServerNode_device (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);}
void __modbus_set_ServerNode_baud (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.baud = value;}
void __modbus_set_ServerNode_parity (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.parity = value;}
void __modbus_set_ServerNode_stop_bits (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.stop_bits = value;}
void __modbus_set_ServerNode_slave_id (int nodeid, u8 value) {server_nodes[nodeid].slave_id = value;}
/* File generated by Beremiz (PlugGenerate_C method of modbus Plugin instance) */
/*
* Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
*
* This file is part of the Modbus library for Beremiz and matiec.
*
* This Modbus library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this Modbus library. If not, see <http://www.gnu.org/licenses/>.
*
* This code is made available on the understanding that it will not be
* used in safety-critical situations without a full and competent review.
*/
#include "mb_addr.h"
#include "mb_tcp_private.h"
#include "mb_master_private.h"
#define DEF_REQ_SEND_RETRIES 0
#define MODBUS_PARAM_STRING_SIZE 64
// Used by the Modbus server node
#define MEM_AREA_SIZE 65536
typedef struct{
u16 ro_bits [MEM_AREA_SIZE];
u16 rw_bits [MEM_AREA_SIZE];
u16 ro_words[MEM_AREA_SIZE];
u16 rw_words[MEM_AREA_SIZE];
/* Two flags to count the number of Modbus requests (read and write) we have
* successfully received from any remote Modbus master.
* Two boolean flags that are set whenever we successfully process a
* Modbus request sent from a remote client.
* These flags will be mapped onto located variables
* so the user's IEC 61131-3 code can check whether we are being
* polled by a Modbus master.
* The counters will roll over to 0 upon reaching maximum value.
* The user will probably periodically reset the boolean flags to false,
* and use this as a communication timeout
* (when it remains false in two consecutive periods)
*
* u8 for BOOL variable/flag
* u32 for UDINT variable/counter
*/
u8 flag_write_req_flag;
u8 flag_read_req_flag;
u32 flag_write_req_counter;
u32 flag_read_req_counter;
} server_mem_t;
/*
* Beremiz has a program to run on the PLC (Beremiz_service.py)
* to handle downloading of compiled programs, start/stop of PLC, etc.
* (see runtime/PLCObject.py for start/stop, loading, ...)
*
* This service also includes a web server to access PLC state (start/stop)
* and to change some basic confiuration parameters.
* (see runtime/NevowServer.py for the web server)
*
* The web server allows for extensions, where additional configuration
* parameters may be changed on the running/downloaded PLC.
* Modbus plugin also comes with an extension to the web server, through
* which the basic Modbus plugin configuration parameters may be changed
*
* This means that most values in the server_node_t and client_node_t
* may be changed after the co,piled code (.so file) is loaded into
* memory, and before the code starts executing.
* Since the we will also want to change the host and port (TCP) and the
* serial device (RTU) at this time, it is best if we allocate memory for
* these strings that may be overwritten by the web server (i.e., do not use
* const strings) in the server_node_t and client_node_t structures.
*
* The following structure members
* - node_addr_t.addr.tcp.host
* - node_addr_t.addr.tcp.service (i.e. the port)
* - node_addr_t.addr.rtu.device
* are all char *, and do not allocate memory for the strings.
*
* We therefore include two generic char arrays, str1 and str2,
* that will store the above strings, and the C code will initiliaze
* the node_addre_t.addr string pointers to these strings.
* i.e., either addr.rtu.device will point to str1,
* or
* addr.tcp.host and addr.tcp.service
* will point to str1 and str2 respectively
*/
typedef struct{
const char *location;
const char *config_name;
char str1[MODBUS_PARAM_STRING_SIZE];
char str2[MODBUS_PARAM_STRING_SIZE];
u8 slave_id;
node_addr_t node_address;
int mb_nd; // modbus library node used for this server
int init_state; // store how far along the server's initialization has progressed
/* entries from this point forward are not statically initialized when the variable is declared */
/* they will be initialized by the code itself in the init() function */
pthread_t thread_id; // thread handling this server
server_mem_t mem_area;
} server_node_t;
// Used by the Modbus client node
typedef struct{
const char *location;
const char *config_name;
char str1[MODBUS_PARAM_STRING_SIZE];
char str2[MODBUS_PARAM_STRING_SIZE];
node_addr_t node_address;
int mb_nd; // modbus library node used for this client
int init_state; // store how far along the client's initialization has progressed
u64 comm_period;// period to use when periodically sending requests to remote server
int prev_error; // error code of the last printed error message (0 when no error)
pthread_t thread_id; // thread handling all communication for this client node
pthread_t timer_thread_id; // thread handling periodical timer for this client node
pthread_mutex_t mutex; // mutex to be used with the following condition variable
pthread_cond_t condv; // used to signal the client thread when to start new modbus transactions
int execute_req; /* used, in association with condition variable,
* to signal when to send the modbus request to the server
* Note that we cannot simply rely on the condition variable to signal
* when to activate the client thread, as the call to
* pthread_cond_wait() may return without having been signaled!
* From the manual:
* Spurious wakeups from the
* pthread_cond_timedwait() or pthread_cond_wait() functions may occur.
* Since the return from pthread_cond_timedwait() or pthread_cond_wait()
* does not imply anything about the value of this predicate, the predi-
* cate should be re-evaluated upon such return.
*/
int periodic_act; /* (boolen) flag will be set when the client node's thread was activated
* (by signaling the above condition variable) by the periodic timer.
* Note that this same thread may also be activated (condition variable is signaled)
* by other sources, such as when the user program requests that a specific
* client MB transation be executed (flag_exec_req in client_request_t)
*/
} client_node_t;
// Used by the Modbus client plugin
typedef enum {
req_input,
req_output,
no_request /* just for tests to quickly disable a request */
} iotype_t;
#define REQ_BUF_SIZE 2000
typedef struct{
const char *location;
int client_node_id;
u8 slave_id;
iotype_t req_type;
u8 mb_function;
u16 address;
u16 count;
int retries;
u8 mb_error_code; // modbus error code (if any) of last executed request
u8 tn_error_code; // transaction error code (if any) of last executed request
int prev_error; // error code of the last printed error message (0 when no error)
struct timespec resp_timeout;
u8 write_on_change; // boolean flag. If true => execute MB request when data to send changes
// buffer used to store located PLC variables
u16 plcv_buffer[REQ_BUF_SIZE];
// buffer used to store data coming from / going to server
u16 coms_buffer[REQ_BUF_SIZE];
pthread_mutex_t coms_buf_mutex; // mutex to access coms_buffer[]
/* boolean flag that will be mapped onto a (BOOL) located variable
* (u8 because IEC 61131-3 BOOL are mapped onto u8 in C code! )
* -> allow PLC program to request when to start the MB transaction
* -> will be reset once the MB transaction has completed
*/
u8 flag_exec_req;
/* flag that works in conjunction with flag_exec_req
* (does not really need to be u8 as it is not mapped onto a located variable. )
* -> used by internal logic to indicate that the client thread
* that will be executing the MB transaction
* requested by flag exec_req has already been activated.
* -> will be reset once the MB transaction has completed
*/
u8 flag_exec_started;
/* flag that will be mapped onto a (BYTE) located variable
* (u8 because the flag is a BYTE! )
* -> will store the result of the last executed MB transaction
* 1 -> error accessing IP network, or serial interface
* 2 -> reply received from server was an invalid frame
* 3 -> server did not reply before timeout expired
* 4 -> server returned a valid Modbus error frame
* -> will be reset (set to 0) once this MB transaction has completed sucesfully
*
* In other words, this variable is a copy of tn_error_code, reset after each request attempt completes.
* We map this copy (instead of tn_error_code) onto a located variable in case the user program decides
* to overwrite its value and mess up the plugin logic.
*/
u8 flag_tn_error_code;
/* flag that will be mapped onto a (BYTE) located variable
* (u8 because the flag is a BYTE! )
* -> if flag_tn_error_code is 4, this flag will store the MB error code returned by the MB server in a MB error frame
* -> will be reset (set to 0) once this MB transaction has completed succesfully
*
* In other words, this variable is a copy of mb_error_code, reset after each request attempt completes.
* We map this copy (instead of mb_error_code) onto a located variable in case the user program decides
* to overwrite its value and mess up the plugin logic.
*/
u8 flag_mb_error_code;
} client_request_t;
/* The total number of nodes, needed to support _all_ instances of the modbus plugin */
#define TOTAL_TCPNODE_COUNT 11
#define TOTAL_RTUNODE_COUNT 0
#define TOTAL_ASCNODE_COUNT 0
/* Values for instance 0 of the modbus plugin */
#define MAX_NUMBER_OF_TCPCLIENTS 10
#define NUMBER_OF_TCPSERVER_NODES 0
#define NUMBER_OF_TCPCLIENT_NODES 1
#define NUMBER_OF_TCPCLIENT_REQTS 4
#define NUMBER_OF_RTUSERVER_NODES 0
#define NUMBER_OF_RTUCLIENT_NODES 0
#define NUMBER_OF_RTUCLIENT_REQTS 0
#define NUMBER_OF_ASCIISERVER_NODES 0
#define NUMBER_OF_ASCIICLIENT_NODES 0
#define NUMBER_OF_ASCIICLIENT_REQTS 0
#define NUMBER_OF_SERVER_NODES (NUMBER_OF_TCPSERVER_NODES + \
NUMBER_OF_RTUSERVER_NODES + \
NUMBER_OF_ASCIISERVER_NODES)
#define NUMBER_OF_CLIENT_NODES (NUMBER_OF_TCPCLIENT_NODES + \
NUMBER_OF_RTUCLIENT_NODES + \
NUMBER_OF_ASCIICLIENT_NODES)
#define NUMBER_OF_CLIENT_REQTS (NUMBER_OF_TCPCLIENT_REQTS + \
NUMBER_OF_RTUCLIENT_REQTS + \
NUMBER_OF_ASCIICLIENT_REQTS)
/*initialization following all parameters given by user in application*/
static client_node_t client_nodes[NUMBER_OF_CLIENT_NODES] = {
/*node 0.0*/
{"0.0", "Modbus TCP Client 0.0", "192.168.0.48", "502", {naf_tcp, {.tcp = {NULL, NULL, DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */, 1000 /* communication period */, 0 /* prev_error */}
};
static client_request_t client_requests[NUMBER_OF_CLIENT_REQTS] = {
/*request 0_0_0*/
{"0_0_0", 0, 0, req_output, 5, 0 , 1,
DEF_REQ_SEND_RETRIES, 0 /* mb_error_code */, 0 /* tn_error_code */, 0 /* prev_code */, {0, 10000000} /* timeout */, 0 /* write_on_change */,
{0}, {0}},
/*request 0_0_1*/
{"0_0_1", 0, 0, req_output, 5, 1 , 1,
DEF_REQ_SEND_RETRIES, 0 /* mb_error_code */, 0 /* tn_error_code */, 0 /* prev_code */, {0, 10000000} /* timeout */, 0 /* write_on_change */,
{0}, {0}},
/*request 0_0_2*/
{"0_0_2", 0, 0, req_output, 5, 2 , 1,
DEF_REQ_SEND_RETRIES, 0 /* mb_error_code */, 0 /* tn_error_code */, 0 /* prev_code */, {0, 10000000} /* timeout */, 0 /* write_on_change */,
{0}, {0}},
/*request 0_0_3*/
{"0_0_3", 0, 0, req_output, 5, 3 , 1,
DEF_REQ_SEND_RETRIES, 0 /* mb_error_code */, 0 /* tn_error_code */, 0 /* prev_code */, {0, 10000000} /* timeout */, 0 /* write_on_change */,
{0}, {0}}
};
static server_node_t server_nodes[NUMBER_OF_SERVER_NODES] = {
}
;
/*******************/
/*located variables*/
/*******************/
u16 *__QX0_0_0_0 = &client_requests[0].plcv_buffer[0];
u16 *__QX0_0_1_1 = &client_requests[1].plcv_buffer[0];
u16 *__QX0_0_2_2 = &client_requests[2].plcv_buffer[0];
u16 *__QX0_0_3_3 = &client_requests[3].plcv_buffer[0];
......@@ -207,6 +207,7 @@ void COUNTERST_init__(COUNTERST *data__, BOOL retain) {
__INIT_VAR(data__->OUT,0,retain)
__INIT_VAR(data__->CNT,0,retain)
__INIT_EXTERNAL(INT,RESETCOUNTERVALUE,data__->RESETCOUNTERVALUE,retain)
__INIT_EXTERNAL(BOOL,RELAY0VALUE,data__->RELAY0VALUE,retain)
}
// Code part
......@@ -241,11 +242,9 @@ __end:
void PLC_PRG_init__(PLC_PRG *data__, BOOL retain) {
__INIT_VAR(data__->RESET,__BOOL_LITERAL(FALSE),retain)
__INIT_VAR(data__->CNT0,0,retain)
__INIT_VAR(data__->CNT1,0,retain)
__INIT_VAR(data__->CNT2,0,retain)
__INIT_VAR(data__->CNT3,0,retain)
__INIT_VAR(data__->CNT4,0,retain)
COUNTERST_init__(&data__->COUNTERST0,retain);
__INIT_LOCATED(BOOL,__QX0_0_0_0,data__->RELAY0VALUE,retain)
__INIT_LOCATED_VALUE(data__->RELAY0VALUE,__BOOL_LITERAL(FALSE))
}
// Code part
......
......@@ -112,6 +112,7 @@ typedef struct {
// FB private variables - TEMP, private and located variables
__DECLARE_VAR(INT,CNT)
__DECLARE_EXTERNAL(INT,RESETCOUNTERVALUE)
__DECLARE_EXTERNAL(BOOL,RELAY0VALUE)
} COUNTERST;
......@@ -124,13 +125,10 @@ typedef struct {
// PROGRAM Interface - IN, OUT, IN_OUT variables
__DECLARE_VAR(BOOL,RESET)
__DECLARE_VAR(INT,CNT0)
__DECLARE_VAR(INT,CNT1)
__DECLARE_VAR(INT,CNT2)
__DECLARE_VAR(INT,CNT3)
__DECLARE_VAR(INT,CNT4)
// PROGRAM private variables - TEMP, private and located variables
COUNTERST COUNTERST0;
__DECLARE_LOCATED(BOOL,RELAY0VALUE)
} PLC_PRG;
......
......@@ -3,13 +3,13 @@
// Variables
0;VAR;CONFIG.RESETCOUNTERVALUE;CONFIG.RESETCOUNTERVALUE;INT;INT;
1;FB;CONFIG.RESOURCE1.INSTANCE0;CONFIG.RESOURCE1.INSTANCE0;PLC_PRG;;
2;VAR;CONFIG.RESOURCE1.INSTANCE0.RESET;CONFIG.RESOURCE1.INSTANCE0.RESET;BOOL;BOOL;
3;VAR;CONFIG.RESOURCE1.INSTANCE0.CNT0;CONFIG.RESOURCE1.INSTANCE0.CNT0;INT;INT;
4;VAR;CONFIG.RESOURCE1.INSTANCE0.CNT1;CONFIG.RESOURCE1.INSTANCE0.CNT1;INT;INT;
5;VAR;CONFIG.RESOURCE1.INSTANCE0.CNT2;CONFIG.RESOURCE1.INSTANCE0.CNT2;INT;INT;
6;VAR;CONFIG.RESOURCE1.INSTANCE0.CNT3;CONFIG.RESOURCE1.INSTANCE0.CNT3;INT;INT;
7;VAR;CONFIG.RESOURCE1.INSTANCE0.CNT4;CONFIG.RESOURCE1.INSTANCE0.CNT4;INT;INT;
1;OUT;CONFIG.RELAY0VALUE;CONFIG.RELAY0VALUE;BOOL;BOOL;
2;OUT;CONFIG.RELAY1VALUE;CONFIG.RELAY1VALUE;BOOL;BOOL;
3;OUT;CONFIG.RELAY2VALUE;CONFIG.RELAY2VALUE;BOOL;BOOL;
4;OUT;CONFIG.RELAY3VALUE;CONFIG.RELAY3VALUE;BOOL;BOOL;
5;FB;CONFIG.RESOURCE1.INSTANCE0;CONFIG.RESOURCE1.INSTANCE0;PLC_PRG;;
6;VAR;CONFIG.RESOURCE1.INSTANCE0.RESET;CONFIG.RESOURCE1.INSTANCE0.RESET;BOOL;BOOL;
7;VAR;CONFIG.RESOURCE1.INSTANCE0.CNT0;CONFIG.RESOURCE1.INSTANCE0.CNT0;INT;INT;
8;FB;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0;COUNTERST;;
9;VAR;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.EN;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.EN;BOOL;BOOL;
10;VAR;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.ENO;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.ENO;BOOL;BOOL;
......@@ -17,6 +17,8 @@
12;VAR;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.OUT;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.OUT;INT;INT;
13;VAR;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.CNT;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.CNT;INT;INT;
14;EXT;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.RESETCOUNTERVALUE;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.RESETCOUNTERVALUE;INT;INT;
15;EXT;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.RELAY0VALUE;CONFIG.RESOURCE1.INSTANCE0.COUNTERST0.RELAY0VALUE;BOOL;BOOL;
16;OUT;CONFIG.RESOURCE1.INSTANCE0.RELAY0VALUE;CONFIG.RESOURCE1.INSTANCE0.RELAY0VALUE;BOOL;BOOL;
// Ticktime
......
......@@ -11,13 +11,29 @@
// CONFIGURATION CONFIG
__DECLARE_GLOBAL(INT,CONFIG,RESETCOUNTERVALUE)
__DECLARE_GLOBAL_LOCATION(BOOL,__QX0_0_0_0)
__DECLARE_GLOBAL_LOCATED(BOOL,CONFIG,RELAY0VALUE)
__DECLARE_GLOBAL_LOCATION(BOOL,__QX0_0_1_1)
__DECLARE_GLOBAL_LOCATED(BOOL,CONFIG,RELAY1VALUE)
__DECLARE_GLOBAL_LOCATION(BOOL,__QX0_0_2_2)
__DECLARE_GLOBAL_LOCATED(BOOL,CONFIG,RELAY2VALUE)
__DECLARE_GLOBAL_LOCATION(BOOL,__QX0_0_3_3)
__DECLARE_GLOBAL_LOCATED(BOOL,CONFIG,RELAY3VALUE)
void RESOURCE1_init__(void);
void config_init__(void) {
BOOL retain;
retain = 0;
__INIT_GLOBAL(INT,RESETCOUNTERVALUE,__INITIAL_VALUE(1000),retain)
__INIT_GLOBAL(INT,RESETCOUNTERVALUE,__INITIAL_VALUE(0),retain)
__INIT_GLOBAL_LOCATED(CONFIG,RELAY0VALUE,__QX0_0_0_0,retain)
__INIT_GLOBAL(BOOL,RELAY0VALUE,__INITIAL_VALUE(0),retain)
__INIT_GLOBAL_LOCATED(CONFIG,RELAY1VALUE,__QX0_0_1_1,retain)
__INIT_GLOBAL(BOOL,RELAY1VALUE,__INITIAL_VALUE(0),retain)
__INIT_GLOBAL_LOCATED(CONFIG,RELAY2VALUE,__QX0_0_2_2,retain)
__INIT_GLOBAL(BOOL,RELAY2VALUE,__INITIAL_VALUE(0),retain)
__INIT_GLOBAL_LOCATED(CONFIG,RELAY3VALUE,__QX0_0_3_3,retain)
__INIT_GLOBAL(BOOL,RELAY3VALUE,__INITIAL_VALUE(0),retain)
RESOURCE1_init__();
}
......
#include "beremiz.h"
__DECLARE_GLOBAL_PROTOTYPE(INT,RESETCOUNTERVALUE)
__DECLARE_GLOBAL_PROTOTYPE(BOOL,RELAY0VALUE)
__DECLARE_GLOBAL_PROTOTYPE(BOOL,RELAY1VALUE)
__DECLARE_GLOBAL_PROTOTYPE(BOOL,RELAY2VALUE)
__DECLARE_GLOBAL_PROTOTYPE(BOOL,RELAY3VALUE)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of Beremiz runtime.
#
# Copyright (C) 2020: Mario de Sousa
#
# See COPYING.Runtime file for copyrights details.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library 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
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
##############################################################################################
# This file implements an extension to the web server embedded in the Beremiz_service.py #
# runtime manager (webserver is in runtime/NevowServer.py). #
# #
# The extension implemented in this file allows for runtime configuration #
# of Modbus plugin parameters #
##############################################################################################
import json
import os
import ctypes
import string
import hashlib
from formless import annotate, webform
import runtime.NevowServer as NS
# Directory in which to store the persistent configurations
# Should be a directory that does not get wiped on reboot!
_ModbusConfFiledir = WorkingDir
# List of all Web Extension Setting nodes we are handling.
# One WebNode each for:
# - Modbus TCP client
# - Modbus TCP server
# - Modbus RTU client
# - Modbus RTU slave
# configured in the loaded PLC (i.e. the .so file loaded into memory)
# Each entry will be a dictionary. See _AddWebNode() for the details
# of the data structure in each entry.
_WebNodeList = []
class MB_StrippedString(annotate.String):
def __init__(self, *args, **kwargs):
annotate.String.__init__(self, strip = True, *args, **kwargs)
class MB_StopBits(annotate.Choice):
_choices = [0, 1, 2]
def coerce(self, val, configurable):
return int(val)
def __init__(self, *args, **kwargs):
annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs)
class MB_Baud(annotate.Choice):
_choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
def coerce(self, val, configurable):
return int(val)
def __init__(self, *args, **kwargs):
annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs)
class MB_Parity(annotate.Choice):
# For more info on what this class really does, have a look at the code in
# file twisted/nevow/annotate.py
# grab this code from $git clone https://github.com/twisted/nevow/
#
# Warning: do _not_ name this variable choice[] without underscore, as that name is
# already used for another similar variable by the underlying class annotate.Choice
_choices = [ 0, 1, 2 ]
_label = ["none", "odd", "even"]
def choice_to_label(self, key):
#PLCObject.LogMessage("Modbus web server extension::choice_to_label() " + str(key))
return self._label[key]
def coerce(self, val, configurable):
"""Coerce a value with the help of an object, which is the object
we are configuring.
"""
# Basically, make sure the value the user introduced is valid, and transform
# into something that is valid if necessary or mark it as an error
# (by raising an exception ??).
#
# We are simply using this functions to transform the input value (a string)
# into an integer. Note that although the available options are all
# integers (0, 1 or 2), even though what is shown on the user interface
# are actually strings, i.e. the labels), these parameters are for some
# reason being parsed as strings, so we need to map them back to an
# integer.
#
#PLCObject.LogMessage("Modbus web server extension::coerce " + val )
return int(val)
def __init__(self, *args, **kwargs):
annotate.Choice.__init__(self,
choices = self._choices,
stringify = self.choice_to_label,
*args, **kwargs)
# Parameters we will need to get from the C code, but that will not be shown
# on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii)
#
# The annotate type entry is basically useless and is completely ignored.
# We kee that entry so that this list can later be correctly merged with the
# following lists...
General_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("config_name" , _("") , ctypes.c_char_p, annotate.String),
("addr_type" , _("") , ctypes.c_char_p, annotate.String)
]
# Parameters we will need to get from the C code, and that _will_ be shown
# on the web interface.
TCPclient_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("host" , _("Remote IP Address") , ctypes.c_char_p, MB_StrippedString),
("port" , _("Remote Port Number") , ctypes.c_char_p, MB_StrippedString),
("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer )
]
RTUclient_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString),
("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ),
("parity" , _("Parity") , ctypes.c_int, MB_Parity ),
("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ),
("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer)
]
TCPserver_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("host" , _("Local IP Address") , ctypes.c_char_p, MB_StrippedString),
("port" , _("Local Port Number") , ctypes.c_char_p, MB_StrippedString),
("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer )
]
RTUslave_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString),
("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ),
("parity" , _("Parity") , ctypes.c_int, MB_Parity ),
("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ),
("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer)
]
# Dictionary containing List of Web viewable parameters
# Note: the dictionary key must be the same as the string returned by the
# __modbus_get_ClientNode_addr_type()
# __modbus_get_ServerNode_addr_type()
# functions implemented in C (see modbus/mb_runtime.c)
_client_WebParamListDict = {}
_client_WebParamListDict["tcp" ] = TCPclient_parameters
_client_WebParamListDict["rtu" ] = RTUclient_parameters
_client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin)
_server_WebParamListDict = {}
_server_WebParamListDict["tcp" ] = TCPserver_parameters
_server_WebParamListDict["rtu" ] = RTUslave_parameters
_server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin)
WebParamListDictDict = {}
WebParamListDictDict['client'] = _client_WebParamListDict
WebParamListDictDict['server'] = _server_WebParamListDict
def _SetModbusSavedConfiguration(WebNode_id, newConfig):
""" Stores a dictionary in a persistant file containing the Modbus parameter configuration """
WebNode_entry = _WebNodeList[WebNode_id]
if WebNode_entry["DefaultConfiguration"] == newConfig:
_DelModbusSavedConfiguration(WebNode_id)
WebNode_entry["ModbusSavedConfiguration"] = None
else:
# Add the addr_type and node_type to the data that will be saved to file
# This allows us to confirm the saved data contains the correct addr_type
# when loading from file
save_info = {}
save_info["addr_type"] = WebNode_entry["addr_type"]
save_info["node_type"] = WebNode_entry["node_type"]
save_info["config" ] = newConfig
filename = WebNode_entry["filename"]
with open(os.path.realpath(filename), 'w') as f:
json.dump(save_info, f, sort_keys=True, indent=4)
WebNode_entry["ModbusSavedConfiguration"] = newConfig
def _DelModbusSavedConfiguration(WebNode_id):
""" Deletes the file cotaining the persistent Modbus configuration """
filename = _WebNodeList[WebNode_id]["filename"]
if os.path.exists(filename):
os.remove(filename)
def _GetModbusSavedConfiguration(WebNode_id):
"""
Returns a dictionary containing the Modbus parameter configuration
that was last saved to file. If no file exists, or file contains
wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the
addr_type of the WebNode_id), then return None
"""
filename = _WebNodeList[WebNode_id]["filename"]
try:
#if os.path.isfile(filename):
save_info = json.load(open(os.path.realpath(filename)))
except Exception:
return None
if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]:
return None
if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]:
return None
if "config" not in save_info:
return None
saved_config = save_info["config"]
#if _CheckConfiguration(saved_config):
# return saved_config
#else:
# return None
return saved_config
def _GetModbusPLCConfiguration(WebNode_id):
"""
Returns a dictionary containing the current Modbus parameter configuration
stored in the C variables in the loaded PLC (.so file)
"""
current_config = {}
C_node_id = _WebNodeList[WebNode_id]["C_node_id"]
WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"]
for par_name, x1, x2, x3 in WebParamList:
value = GetParamFuncs[par_name](C_node_id)
if value is not None:
current_config[par_name] = value
return current_config
def _SetModbusPLCConfiguration(WebNode_id, newconfig):
"""
Stores the Modbus parameter configuration into the
the C variables in the loaded PLC (.so file)
"""
C_node_id = _WebNodeList[WebNode_id]["C_node_id"]
SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"]
for par_name in newconfig:
value = newconfig[par_name]
if value is not None:
SetParamFuncs[par_name](C_node_id, value)
def _GetModbusWebviewConfigurationValue(ctx, WebNode_id, argument):
"""
Callback function, called by the web interface (NevowServer.py)
to fill in the default value of each parameter of the web form
Note that the real callback function is a dynamically created function that
will simply call this function to do the work. It will also pass the WebNode_id
as a parameter.
"""
try:
return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name]
except Exception:
return ""
def OnModbusButtonSave(**kwargs):
"""
Function called when user clicks 'Save' button in web interface
The function will configure the Modbus plugin in the PLC with the values
specified in the web interface. However, values must be validated first!
Note that this function does not get called directly. The real callback
function is the dynamic __OnButtonSave() function, which will add the
"WebNode_id" argument, and call this function to do the work.
"""
#PLCObject.LogMessage("Modbus web server extension::OnModbusButtonSave() Called")
newConfig = {}
WebNode_id = kwargs.get("WebNode_id", None)
WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
for par_name, x1, x2, x3 in WebParamList:
value = kwargs.get(par_name, None)
if value is not None:
newConfig[par_name] = value
# First check if configuration is OK.
# Note that this is not currently required, as we use drop down choice menus
# for baud, parity and sop bits, so the values should always be correct!
#if not _CheckWebConfiguration(newConfig):
# return
# store to file the new configuration so that
# we can recoup the configuration the next time the PLC
# has a cold start (i.e. when Beremiz_service.py is retarted)
_SetModbusSavedConfiguration(WebNode_id, newConfig)
# Configure PLC with the current Modbus parameters
_SetModbusPLCConfiguration(WebNode_id, newConfig)
# Update the viewable configuration
# The PLC may have coerced the values on calling _SetModbusPLCConfiguration()
# so we do not set it directly to newConfig
_WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetModbusPLCConfiguration(WebNode_id)
def OnModbusButtonReset(**kwargs):
"""
Function called when user clicks 'Delete' button in web interface
The function will delete the file containing the persistent
Modbus configution
"""
WebNode_id = kwargs.get("WebNode_id", None)
# Delete the file
_DelModbusSavedConfiguration(WebNode_id)
# Set the current configuration to the default (hardcoded in C)
new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"]
_SetModbusPLCConfiguration(WebNode_id, new_config)
#Update the webviewconfiguration
_WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config
# Reset ModbusSavedConfiguration
_WebNodeList[WebNode_id]["ModbusSavedConfiguration"] = None
def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs):
"""
Load from the compiled code (.so file, aloready loaded into memmory)
the configuration parameters of a specific Modbus plugin node.
This function works with both client and server nodes, depending on the
Get/SetParamFunc dictionaries passed to it (either the client or the server
node versions of the Get/Set functions)
"""
WebNode_entry = {}
# Get the config_name from the C code...
config_name = GetParamFuncs["config_name"](C_node_id)
# Get the addr_type from the C code...
# addr_type will be one of "tcp", "rtu" or "ascii"
addr_type = GetParamFuncs["addr_type" ](C_node_id)
# For some operations we cannot use the config name (e.g. filename to store config)
# because the user may be using characters that are invalid for that purpose ('/' for
# example), so we create a hash of the config_name, and use that instead.
config_hash = hashlib.md5(config_name).hexdigest()
#PLCObject.LogMessage("Modbus web server extension::_AddWebNode("+str(C_node_id)+") config_name="+config_name)
# Add the new entry to the global list
# Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in WebNode_entry
# WebNode_entry will be stored as a reference, so we can later insert parameters at will.
global _WebNodeList
_WebNodeList.append(WebNode_entry)
WebNode_id = len(_WebNodeList) - 1
# store all WebNode relevant data for future reference
#
# Note that "WebParamList" will reference one of:
# - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters
WebNode_entry["C_node_id" ] = C_node_id
WebNode_entry["config_name" ] = config_name
WebNode_entry["config_hash" ] = config_hash
WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json")
WebNode_entry["GetParamFuncs"] = GetParamFuncs
WebNode_entry["SetParamFuncs"] = SetParamFuncs
WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type]
WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function)
WebNode_entry["node_type" ] = node_type # 'client', 'server'
# Dictionary that contains the Modbus configuration currently being shown
# on the web interface
# This configuration will almost always be identical to the current
# configuration in the PLC (i.e., the current state stored in the
# C variables in the .so file).
# The configuration viewed on the web will only be different to the current
# configuration when the user edits the configuration, and when
# the user asks to save an edited configuration that contains an error.
WebNode_entry["WebviewConfiguration"] = None
# Upon PLC load, this Dictionary is initialised with the Modbus configuration
# hardcoded in the C file
# (i.e. the configuration inserted in Beremiz IDE when project was compiled)
WebNode_entry["DefaultConfiguration"] = _GetModbusPLCConfiguration(WebNode_id)
WebNode_entry["WebviewConfiguration"] = WebNode_entry["DefaultConfiguration"]
# Dictionary that stores the Modbus configuration currently stored in a file
# Currently only used to decide whether or not to show the "Delete" button on the
# web interface (only shown if "ModbusSavedConfiguration" is not None)
SavedConfig = _GetModbusSavedConfiguration(WebNode_id)
WebNode_entry["ModbusSavedConfiguration"] = SavedConfig
if SavedConfig is not None:
_SetModbusPLCConfiguration(WebNode_id, SavedConfig)
WebNode_entry["WebviewConfiguration"] = SavedConfig
# Define the format for the web form used to show/change the current parameters
# We first declare a dynamic function to work as callback to obtain the default values for each parameter
# Note: We transform every parameter into a string
# This is not strictly required for parameters of type annotate.Integer that will correctly
# accept the default value as an Integer python object
# This is obviously also not required for parameters of type annotate.String, that are
# always handled as strings.
# However, the annotate.Choice parameters (and all parameters that derive from it,
# sucn as Parity, Baud, etc.) require the default value as a string
# even though we store it as an integer, which is the data type expected
# by the set_***() C functions in mb_runtime.c
def __GetWebviewConfigurationValue(ctx, argument):
return str(_GetModbusWebviewConfigurationValue(ctx, WebNode_id, argument))
webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue))
for name, web_label, c_dtype, web_dtype in WebNode_entry["WebParamList"]]
# Configure the web interface to include the Modbus config parameters
def __OnButtonSave(**kwargs):
OnModbusButtonSave(WebNode_id=WebNode_id, **kwargs)
WebSettings = NS.newExtensionSetting("Modbus #"+ str(WebNode_id), config_hash)
WebSettings.addSettings(
"ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...)
_("Modbus Configuration: ") + config_name, # description (user visible label)
webFormInterface, # fields
_("Apply"), # button label
__OnButtonSave) # callback
def __OnButtonReset(**kwargs):
return OnModbusButtonReset(WebNode_id = WebNode_id, **kwargs)
def getModbusConfigStatus():
if WebNode_entry["WebviewConfiguration"] == WebNode_entry["DefaultConfiguration"]:
return "Unchanged"
return "Modified"
WebSettings.addSettings(
"ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...)
_("Modbus Configuration: ") + config_name, # description (user visible label)
[ ("status",
annotate.String(label=_("Current state"),
immutable=True,
default=lambda *k:getModbusConfigStatus())),
], # fields (empty, no parameters required!)
_("Reset"), # button label
__OnButtonReset)
def _runtime_0_modbus_websettings_init():
"""
Callback function, called (by PLCObject.py) when a new PLC program
(i.e. XXX.so file) is transfered to the PLC runtime
and loaded into memory
"""
#PLCObject.LogMessage("Modbus web server extension::OnLoadPLC() Called...")
if PLCObject.PLClibraryHandle is None:
# PLC was loaded but we don't have access to the library of compiled code (.so lib)?
# Hmm... This shold never occur!!
return
# Get the number of Modbus Client and Servers (Modbus plugin)
# configured in the currently loaded PLC project (i.e., the .so file)
# If the "__modbus_plugin_client_node_count"
# or the "__modbus_plugin_server_node_count" C variables
# are not present in the .so file we conclude that the currently loaded
# PLC does not have the Modbus plugin included (situation (2b) described above init())
try:
# XXX TODO : stop reading from PLC .so file. This code is template code
# that can use modbus extension build data, such as client node count.
client_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_client_node_count").value
server_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_server_node_count").value
except Exception:
# Loaded PLC does not have the Modbus plugin => nothing to do
# (i.e. do _not_ configure and make available the Modbus web interface)
return
if client_count < 0: client_count = 0
if server_count < 0: server_count = 0
if (client_count == 0) and (server_count == 0):
# The Modbus plugin in the loaded PLC does not have any client and servers configured
# => nothing to do (i.e. do _not_ configure and make available the Modbus web interface)
return
# Map the get/set functions (written in C code) we will be using to get/set the configuration parameters
# Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c)
GetClientParamFuncs = {}
SetClientParamFuncs = {}
GetServerParamFuncs = {}
SetServerParamFuncs = {}
# XXX TODO : stop reading from PLC .so file. This code is template code
# that can use modbus extension build data
for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters:
ParamFuncName = "__modbus_get_ClientNode_" + name
GetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
GetClientParamFuncs[name].restype = c_dtype
GetClientParamFuncs[name].argtypes = [ctypes.c_int]
for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters:
ParamFuncName = "__modbus_set_ClientNode_" + name
SetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
SetClientParamFuncs[name].restype = None
SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype]
# XXX TODO : stop reading from PLC .so file. This code is template code
# that can use modbus extension build data
for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters:
ParamFuncName = "__modbus_get_ServerNode_" + name
GetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
GetServerParamFuncs[name].restype = c_dtype
GetServerParamFuncs[name].argtypes = [ctypes.c_int]
for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters:
ParamFuncName = "__modbus_set_ServerNode_" + name
SetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
SetServerParamFuncs[name].restype = None
SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype]
for node_id in range(client_count):
_AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs)
for node_id in range(server_count):
_AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs)
def _runtime_0_modbus_websettings_cleanup():
"""
Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
"""
#PLCObject.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...")
# Delete the Modbus specific web interface extensions
# (Safe to ask to delete, even if it has not been added!)
global _WebNodeList
for index, WebNode_entry in enumerate(_WebNodeList):
config_hash = WebNode_entry["config_hash"]
NS.removeExtensionSetting(config_hash)
# Dele all entries...
_WebNodeList = []
......@@ -10,6 +10,7 @@ FUNCTION_BLOCK CounterST
END_VAR
VAR_EXTERNAL
ResetCounterValue : INT;
Relay0Value : BOOL;
END_VAR
IF Reset THEN
......@@ -27,14 +28,13 @@ PROGRAM plc_prg
END_VAR
VAR_OUTPUT
Cnt0 : INT;
Cnt1 : INT;
Cnt2 : INT;
Cnt3 : INT;
Cnt4 : INT;
END_VAR
VAR
CounterST0 : CounterST;
END_VAR
VAR
Relay0Value AT %QX0.0.0.0 : BOOL;
END_VAR
CounterST0(Reset := Reset);
Cnt0 := CounterST0.Out;
......@@ -43,7 +43,11 @@ END_PROGRAM
CONFIGURATION config
VAR_GLOBAL
ResetCounterValue : INT := 1000;
ResetCounterValue : INT := 0;
Relay0Value AT %QX0.0.0.0 : BOOL := 0;
Relay1Value AT %QX0.0.1.1 : BOOL := 0;
Relay2Value AT %QX0.0.2.2 : BOOL := 0;
Relay3Value AT %QX0.0.3.3 : BOOL := 0;
END_VAR
RESOURCE resource1 ON PLC
......
cb6baca0419e9cafc6316a11a81a0317
\ No newline at end of file
7ddbf3c64169e7bffe3e5184c847f848
\ No newline at end of file
......@@ -104,6 +104,7 @@ FUNCTION_BLOCK CounterST
END_VAR
VAR_EXTERNAL
ResetCounterValue : INT;
Relay0Value : BOOL;
END_VAR
IF Reset THEN
......@@ -121,14 +122,13 @@ PROGRAM plc_prg
END_VAR
VAR_OUTPUT
Cnt0 : INT;
Cnt1 : INT;
Cnt2 : INT;
Cnt3 : INT;
Cnt4 : INT;
END_VAR
VAR
CounterST0 : CounterST;
END_VAR
VAR
Relay0Value AT %QX0.0.0.0 : BOOL;
END_VAR
CounterST0(Reset := Reset);
Cnt0 := CounterST0.Out;
......@@ -137,7 +137,11 @@ END_PROGRAM
CONFIGURATION config
VAR_GLOBAL
ResetCounterValue : INT := 1000;
ResetCounterValue : INT := 0;
Relay0Value AT %QX0.0.0.0 : BOOL := 0;
Relay1Value AT %QX0.0.1.1 : BOOL := 0;
Relay2Value AT %QX0.0.2.2 : BOOL := 0;
Relay3Value AT %QX0.0.3.3 : BOOL := 0;
END_VAR
RESOURCE resource1 ON PLC
......
......@@ -26,7 +26,7 @@ void __publish_debug (void){}
#include <stdio.h>
#ifndef TARGET_ONLINE_DEBUG_DISABLE
#define BUFFER_SIZE 22
#define BUFFER_SIZE 20
/* Atomically accessed variable for buffer state */
#define BUFFER_FREE 0
......@@ -50,6 +50,10 @@ extern PLC_PRG RESOURCE1__INSTANCE0;
* Declare global variables from resources and conf
**/
extern __IEC_INT_t CONFIG__RESETCOUNTERVALUE;
extern __IEC_BOOL_p CONFIG__RELAY0VALUE;
extern __IEC_BOOL_p CONFIG__RELAY1VALUE;
extern __IEC_BOOL_p CONFIG__RELAY2VALUE;
extern __IEC_BOOL_p CONFIG__RELAY3VALUE;
extern PLC_PRG RESOURCE1__INSTANCE0;
typedef const struct {
......@@ -59,18 +63,20 @@ typedef const struct {
static dbgvardsc_t dbgvardsc[] = {
{&(CONFIG__RESETCOUNTERVALUE), INT_ENUM},
{&(CONFIG__RELAY0VALUE), BOOL_O_ENUM},
{&(CONFIG__RELAY1VALUE), BOOL_O_ENUM},
{&(CONFIG__RELAY2VALUE), BOOL_O_ENUM},
{&(CONFIG__RELAY3VALUE), BOOL_O_ENUM},
{&(RESOURCE1__INSTANCE0.RESET), BOOL_ENUM},
{&(RESOURCE1__INSTANCE0.CNT0), INT_ENUM},
{&(RESOURCE1__INSTANCE0.CNT1), INT_ENUM},
{&(RESOURCE1__INSTANCE0.CNT2), INT_ENUM},
{&(RESOURCE1__INSTANCE0.CNT3), INT_ENUM},
{&(RESOURCE1__INSTANCE0.CNT4), INT_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.EN), BOOL_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.ENO), BOOL_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.RESET), BOOL_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.OUT), INT_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.CNT), INT_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.RESETCOUNTERVALUE), INT_P_ENUM}
{&(RESOURCE1__INSTANCE0.COUNTERST0.RESETCOUNTERVALUE), INT_P_ENUM},
{&(RESOURCE1__INSTANCE0.COUNTERST0.RELAY0VALUE), BOOL_P_ENUM},
{&(RESOURCE1__INSTANCE0.RELAY0VALUE), BOOL_O_ENUM}
};
typedef void(*__for_each_variable_do_fp)(dbgvardsc_t*);
......
......@@ -42,6 +42,10 @@ int __init_py_ext(int argc,char **argv);
void __cleanup_py_ext(void);
void __retrieve_py_ext(void);
void __publish_py_ext(void);
int __init_0(int argc,char **argv);
void __cleanup_0(void);
void __retrieve_0(void);
void __publish_0(void);
/*
* Retrieve input variables, run PLC and publish output variables
......@@ -53,6 +57,7 @@ void __run(void)
__tick %= greatest_tick_count__;
__retrieve_py_ext();
__retrieve_0();
/*__retrieve_debug();*/
......@@ -60,6 +65,7 @@ void __run(void)
__publish_debug();
__publish_0();
__publish_py_ext();
}
......@@ -80,6 +86,7 @@ int __init(int argc,char **argv)
config_init__();
__init_debug();
init_level=1; if((res = __init_py_ext(argc,argv))){return res;}
init_level=2; if((res = __init_0(argc,argv))){return res;}
return res;
}
/*
......@@ -87,6 +94,7 @@ int __init(int argc,char **argv)
**/
void __cleanup(void)
{
if(init_level >= 2) __cleanup_0();
if(init_level >= 1) __cleanup_py_ext();
__cleanup_debug();
}
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of Beremiz runtime.
#
# Copyright (C) 2020: Mario de Sousa
#
# See COPYING.Runtime file for copyrights details.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library 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
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
##############################################################################################
# This file implements an extension to the web server embedded in the Beremiz_service.py #
# runtime manager (webserver is in runtime/NevowServer.py). #
# #
# The extension implemented in this file allows for runtime configuration #
# of Modbus plugin parameters #
##############################################################################################
import json
import os
import ctypes
import string
import hashlib
from formless import annotate, webform
import runtime.NevowServer as NS
# Directory in which to store the persistent configurations
# Should be a directory that does not get wiped on reboot!
_ModbusConfFiledir = WorkingDir
# List of all Web Extension Setting nodes we are handling.
# One WebNode each for:
# - Modbus TCP client
# - Modbus TCP server
# - Modbus RTU client
# - Modbus RTU slave
# configured in the loaded PLC (i.e. the .so file loaded into memory)
# Each entry will be a dictionary. See _AddWebNode() for the details
# of the data structure in each entry.
_WebNodeList = []
class MB_StrippedString(annotate.String):
def __init__(self, *args, **kwargs):
annotate.String.__init__(self, strip = True, *args, **kwargs)
class MB_StopBits(annotate.Choice):
_choices = [0, 1, 2]
def coerce(self, val, configurable):
return int(val)
def __init__(self, *args, **kwargs):
annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs)
class MB_Baud(annotate.Choice):
_choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
def coerce(self, val, configurable):
return int(val)
def __init__(self, *args, **kwargs):
annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs)
class MB_Parity(annotate.Choice):
# For more info on what this class really does, have a look at the code in
# file twisted/nevow/annotate.py
# grab this code from $git clone https://github.com/twisted/nevow/
#
# Warning: do _not_ name this variable choice[] without underscore, as that name is
# already used for another similar variable by the underlying class annotate.Choice
_choices = [ 0, 1, 2 ]
_label = ["none", "odd", "even"]
def choice_to_label(self, key):
#PLCObject.LogMessage("Modbus web server extension::choice_to_label() " + str(key))
return self._label[key]
def coerce(self, val, configurable):
"""Coerce a value with the help of an object, which is the object
we are configuring.
"""
# Basically, make sure the value the user introduced is valid, and transform
# into something that is valid if necessary or mark it as an error
# (by raising an exception ??).
#
# We are simply using this functions to transform the input value (a string)
# into an integer. Note that although the available options are all
# integers (0, 1 or 2), even though what is shown on the user interface
# are actually strings, i.e. the labels), these parameters are for some
# reason being parsed as strings, so we need to map them back to an
# integer.
#
#PLCObject.LogMessage("Modbus web server extension::coerce " + val )
return int(val)
def __init__(self, *args, **kwargs):
annotate.Choice.__init__(self,
choices = self._choices,
stringify = self.choice_to_label,
*args, **kwargs)
# Parameters we will need to get from the C code, but that will not be shown
# on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii)
#
# The annotate type entry is basically useless and is completely ignored.
# We kee that entry so that this list can later be correctly merged with the
# following lists...
General_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("config_name" , _("") , ctypes.c_char_p, annotate.String),
("addr_type" , _("") , ctypes.c_char_p, annotate.String)
]
# Parameters we will need to get from the C code, and that _will_ be shown
# on the web interface.
TCPclient_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("host" , _("Remote IP Address") , ctypes.c_char_p, MB_StrippedString),
("port" , _("Remote Port Number") , ctypes.c_char_p, MB_StrippedString),
("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer )
]
RTUclient_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString),
("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ),
("parity" , _("Parity") , ctypes.c_int, MB_Parity ),
("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ),
("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer)
]
TCPserver_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("host" , _("Local IP Address") , ctypes.c_char_p, MB_StrippedString),
("port" , _("Local Port Number") , ctypes.c_char_p, MB_StrippedString),
("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer )
]
RTUslave_parameters = [
# param. name label ctype type annotate type
# (C code var name) (used on web interface) (C data type) (web data type)
# (annotate.String,
# annotate.Integer, ...)
("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString),
("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ),
("parity" , _("Parity") , ctypes.c_int, MB_Parity ),
("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ),
("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer)
]
# Dictionary containing List of Web viewable parameters
# Note: the dictionary key must be the same as the string returned by the
# __modbus_get_ClientNode_addr_type()
# __modbus_get_ServerNode_addr_type()
# functions implemented in C (see modbus/mb_runtime.c)
_client_WebParamListDict = {}
_client_WebParamListDict["tcp" ] = TCPclient_parameters
_client_WebParamListDict["rtu" ] = RTUclient_parameters
_client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin)
_server_WebParamListDict = {}
_server_WebParamListDict["tcp" ] = TCPserver_parameters
_server_WebParamListDict["rtu" ] = RTUslave_parameters
_server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin)
WebParamListDictDict = {}
WebParamListDictDict['client'] = _client_WebParamListDict
WebParamListDictDict['server'] = _server_WebParamListDict
def _SetModbusSavedConfiguration(WebNode_id, newConfig):
""" Stores a dictionary in a persistant file containing the Modbus parameter configuration """
WebNode_entry = _WebNodeList[WebNode_id]
if WebNode_entry["DefaultConfiguration"] == newConfig:
_DelModbusSavedConfiguration(WebNode_id)
WebNode_entry["ModbusSavedConfiguration"] = None
else:
# Add the addr_type and node_type to the data that will be saved to file
# This allows us to confirm the saved data contains the correct addr_type
# when loading from file
save_info = {}
save_info["addr_type"] = WebNode_entry["addr_type"]
save_info["node_type"] = WebNode_entry["node_type"]
save_info["config" ] = newConfig
filename = WebNode_entry["filename"]
with open(os.path.realpath(filename), 'w') as f:
json.dump(save_info, f, sort_keys=True, indent=4)
WebNode_entry["ModbusSavedConfiguration"] = newConfig
def _DelModbusSavedConfiguration(WebNode_id):
""" Deletes the file cotaining the persistent Modbus configuration """
filename = _WebNodeList[WebNode_id]["filename"]
if os.path.exists(filename):
os.remove(filename)
def _GetModbusSavedConfiguration(WebNode_id):
"""
Returns a dictionary containing the Modbus parameter configuration
that was last saved to file. If no file exists, or file contains
wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the
addr_type of the WebNode_id), then return None
"""
filename = _WebNodeList[WebNode_id]["filename"]
try:
#if os.path.isfile(filename):
save_info = json.load(open(os.path.realpath(filename)))
except Exception:
return None
if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]:
return None
if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]:
return None
if "config" not in save_info:
return None
saved_config = save_info["config"]
#if _CheckConfiguration(saved_config):
# return saved_config
#else:
# return None
return saved_config
def _GetModbusPLCConfiguration(WebNode_id):
"""
Returns a dictionary containing the current Modbus parameter configuration
stored in the C variables in the loaded PLC (.so file)
"""
current_config = {}
C_node_id = _WebNodeList[WebNode_id]["C_node_id"]
WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"]
for par_name, x1, x2, x3 in WebParamList:
value = GetParamFuncs[par_name](C_node_id)
if value is not None:
current_config[par_name] = value
return current_config
def _SetModbusPLCConfiguration(WebNode_id, newconfig):
"""
Stores the Modbus parameter configuration into the
the C variables in the loaded PLC (.so file)
"""
C_node_id = _WebNodeList[WebNode_id]["C_node_id"]
SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"]
for par_name in newconfig:
value = newconfig[par_name]
if value is not None:
SetParamFuncs[par_name](C_node_id, value)
def _GetModbusWebviewConfigurationValue(ctx, WebNode_id, argument):
"""
Callback function, called by the web interface (NevowServer.py)
to fill in the default value of each parameter of the web form
Note that the real callback function is a dynamically created function that
will simply call this function to do the work. It will also pass the WebNode_id
as a parameter.
"""
try:
return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name]
except Exception:
return ""
def OnModbusButtonSave(**kwargs):
"""
Function called when user clicks 'Save' button in web interface
The function will configure the Modbus plugin in the PLC with the values
specified in the web interface. However, values must be validated first!
Note that this function does not get called directly. The real callback
function is the dynamic __OnButtonSave() function, which will add the
"WebNode_id" argument, and call this function to do the work.
"""
#PLCObject.LogMessage("Modbus web server extension::OnModbusButtonSave() Called")
newConfig = {}
WebNode_id = kwargs.get("WebNode_id", None)
WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
for par_name, x1, x2, x3 in WebParamList:
value = kwargs.get(par_name, None)
if value is not None:
newConfig[par_name] = value
# First check if configuration is OK.
# Note that this is not currently required, as we use drop down choice menus
# for baud, parity and sop bits, so the values should always be correct!
#if not _CheckWebConfiguration(newConfig):
# return
# store to file the new configuration so that
# we can recoup the configuration the next time the PLC
# has a cold start (i.e. when Beremiz_service.py is retarted)
_SetModbusSavedConfiguration(WebNode_id, newConfig)
# Configure PLC with the current Modbus parameters
_SetModbusPLCConfiguration(WebNode_id, newConfig)
# Update the viewable configuration
# The PLC may have coerced the values on calling _SetModbusPLCConfiguration()
# so we do not set it directly to newConfig
_WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetModbusPLCConfiguration(WebNode_id)
def OnModbusButtonReset(**kwargs):
"""
Function called when user clicks 'Delete' button in web interface
The function will delete the file containing the persistent
Modbus configution
"""
WebNode_id = kwargs.get("WebNode_id", None)
# Delete the file
_DelModbusSavedConfiguration(WebNode_id)
# Set the current configuration to the default (hardcoded in C)
new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"]
_SetModbusPLCConfiguration(WebNode_id, new_config)
#Update the webviewconfiguration
_WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config
# Reset ModbusSavedConfiguration
_WebNodeList[WebNode_id]["ModbusSavedConfiguration"] = None
def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs):
"""
Load from the compiled code (.so file, aloready loaded into memmory)
the configuration parameters of a specific Modbus plugin node.
This function works with both client and server nodes, depending on the
Get/SetParamFunc dictionaries passed to it (either the client or the server
node versions of the Get/Set functions)
"""
WebNode_entry = {}
# Get the config_name from the C code...
config_name = GetParamFuncs["config_name"](C_node_id)
# Get the addr_type from the C code...
# addr_type will be one of "tcp", "rtu" or "ascii"
addr_type = GetParamFuncs["addr_type" ](C_node_id)
# For some operations we cannot use the config name (e.g. filename to store config)
# because the user may be using characters that are invalid for that purpose ('/' for
# example), so we create a hash of the config_name, and use that instead.
config_hash = hashlib.md5(config_name).hexdigest()
#PLCObject.LogMessage("Modbus web server extension::_AddWebNode("+str(C_node_id)+") config_name="+config_name)
# Add the new entry to the global list
# Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in WebNode_entry
# WebNode_entry will be stored as a reference, so we can later insert parameters at will.
global _WebNodeList
_WebNodeList.append(WebNode_entry)
WebNode_id = len(_WebNodeList) - 1
# store all WebNode relevant data for future reference
#
# Note that "WebParamList" will reference one of:
# - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters
WebNode_entry["C_node_id" ] = C_node_id
WebNode_entry["config_name" ] = config_name
WebNode_entry["config_hash" ] = config_hash
WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json")
WebNode_entry["GetParamFuncs"] = GetParamFuncs
WebNode_entry["SetParamFuncs"] = SetParamFuncs
WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type]
WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function)
WebNode_entry["node_type" ] = node_type # 'client', 'server'
# Dictionary that contains the Modbus configuration currently being shown
# on the web interface
# This configuration will almost always be identical to the current
# configuration in the PLC (i.e., the current state stored in the
# C variables in the .so file).
# The configuration viewed on the web will only be different to the current
# configuration when the user edits the configuration, and when
# the user asks to save an edited configuration that contains an error.
WebNode_entry["WebviewConfiguration"] = None
# Upon PLC load, this Dictionary is initialised with the Modbus configuration
# hardcoded in the C file
# (i.e. the configuration inserted in Beremiz IDE when project was compiled)
WebNode_entry["DefaultConfiguration"] = _GetModbusPLCConfiguration(WebNode_id)
WebNode_entry["WebviewConfiguration"] = WebNode_entry["DefaultConfiguration"]
# Dictionary that stores the Modbus configuration currently stored in a file
# Currently only used to decide whether or not to show the "Delete" button on the
# web interface (only shown if "ModbusSavedConfiguration" is not None)
SavedConfig = _GetModbusSavedConfiguration(WebNode_id)
WebNode_entry["ModbusSavedConfiguration"] = SavedConfig
if SavedConfig is not None:
_SetModbusPLCConfiguration(WebNode_id, SavedConfig)
WebNode_entry["WebviewConfiguration"] = SavedConfig
# Define the format for the web form used to show/change the current parameters
# We first declare a dynamic function to work as callback to obtain the default values for each parameter
# Note: We transform every parameter into a string
# This is not strictly required for parameters of type annotate.Integer that will correctly
# accept the default value as an Integer python object
# This is obviously also not required for parameters of type annotate.String, that are
# always handled as strings.
# However, the annotate.Choice parameters (and all parameters that derive from it,
# sucn as Parity, Baud, etc.) require the default value as a string
# even though we store it as an integer, which is the data type expected
# by the set_***() C functions in mb_runtime.c
def __GetWebviewConfigurationValue(ctx, argument):
return str(_GetModbusWebviewConfigurationValue(ctx, WebNode_id, argument))
webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue))
for name, web_label, c_dtype, web_dtype in WebNode_entry["WebParamList"]]
# Configure the web interface to include the Modbus config parameters
def __OnButtonSave(**kwargs):
OnModbusButtonSave(WebNode_id=WebNode_id, **kwargs)
WebSettings = NS.newExtensionSetting("Modbus #"+ str(WebNode_id), config_hash)
WebSettings.addSettings(
"ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...)
_("Modbus Configuration: ") + config_name, # description (user visible label)
webFormInterface, # fields
_("Apply"), # button label
__OnButtonSave) # callback
def __OnButtonReset(**kwargs):
return OnModbusButtonReset(WebNode_id = WebNode_id, **kwargs)
def getModbusConfigStatus():
if WebNode_entry["WebviewConfiguration"] == WebNode_entry["DefaultConfiguration"]:
return "Unchanged"
return "Modified"
WebSettings.addSettings(
"ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...)
_("Modbus Configuration: ") + config_name, # description (user visible label)
[ ("status",
annotate.String(label=_("Current state"),
immutable=True,
default=lambda *k:getModbusConfigStatus())),
], # fields (empty, no parameters required!)
_("Reset"), # button label
__OnButtonReset)
def _runtime_0_modbus_websettings_init():
"""
Callback function, called (by PLCObject.py) when a new PLC program
(i.e. XXX.so file) is transfered to the PLC runtime
and loaded into memory
"""
#PLCObject.LogMessage("Modbus web server extension::OnLoadPLC() Called...")
if PLCObject.PLClibraryHandle is None:
# PLC was loaded but we don't have access to the library of compiled code (.so lib)?
# Hmm... This shold never occur!!
return
# Get the number of Modbus Client and Servers (Modbus plugin)
# configured in the currently loaded PLC project (i.e., the .so file)
# If the "__modbus_plugin_client_node_count"
# or the "__modbus_plugin_server_node_count" C variables
# are not present in the .so file we conclude that the currently loaded
# PLC does not have the Modbus plugin included (situation (2b) described above init())
try:
# XXX TODO : stop reading from PLC .so file. This code is template code
# that can use modbus extension build data, such as client node count.
client_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_client_node_count").value
server_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_server_node_count").value
except Exception:
# Loaded PLC does not have the Modbus plugin => nothing to do
# (i.e. do _not_ configure and make available the Modbus web interface)
return
if client_count < 0: client_count = 0
if server_count < 0: server_count = 0
if (client_count == 0) and (server_count == 0):
# The Modbus plugin in the loaded PLC does not have any client and servers configured
# => nothing to do (i.e. do _not_ configure and make available the Modbus web interface)
return
# Map the get/set functions (written in C code) we will be using to get/set the configuration parameters
# Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c)
GetClientParamFuncs = {}
SetClientParamFuncs = {}
GetServerParamFuncs = {}
SetServerParamFuncs = {}
# XXX TODO : stop reading from PLC .so file. This code is template code
# that can use modbus extension build data
for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters:
ParamFuncName = "__modbus_get_ClientNode_" + name
GetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
GetClientParamFuncs[name].restype = c_dtype
GetClientParamFuncs[name].argtypes = [ctypes.c_int]
for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters:
ParamFuncName = "__modbus_set_ClientNode_" + name
SetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
SetClientParamFuncs[name].restype = None
SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype]
# XXX TODO : stop reading from PLC .so file. This code is template code
# that can use modbus extension build data
for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters:
ParamFuncName = "__modbus_get_ServerNode_" + name
GetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
GetServerParamFuncs[name].restype = c_dtype
GetServerParamFuncs[name].argtypes = [ctypes.c_int]
for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters:
ParamFuncName = "__modbus_set_ServerNode_" + name
SetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
SetServerParamFuncs[name].restype = None
SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype]
for node_id in range(client_count):
_AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs)
for node_id in range(server_count):
_AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs)
def _runtime_0_modbus_websettings_cleanup():
"""
Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
"""
#PLCObject.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...")
# Delete the Modbus specific web interface extensions
# (Safe to ask to delete, even if it has not been added!)
global _WebNodeList
for index, WebNode_entry in enumerate(_WebNodeList):
config_hash = WebNode_entry["config_hash"]
NS.removeExtensionSetting(config_hash)
# Dele all entries...
_WebNodeList = []
<?xml version='1.0' encoding='utf-8'?>
<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="0" Name="ModbusRequest_0"/>
<?xml version='1.0' encoding='utf-8'?>
<ModbusRequest xmlns:xsd="http://www.w3.org/2001/XMLSchema" Write_on_change="false" SlaveID="0" Function="05 - Write Single coil"/>
<?xml version='1.0' encoding='utf-8'?>
<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="1" Name="ModbusRequest_1"/>
<?xml version='1.0' encoding='utf-8'?>
<ModbusRequest xmlns:xsd="http://www.w3.org/2001/XMLSchema" Function="05 - Write Single coil" Write_on_change="false" Start_Address="1" SlaveID="0"/>
<?xml version='1.0' encoding='utf-8'?>
<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="2" Name="ModbusRequest_2"/>
<?xml version='1.0' encoding='utf-8'?>
<ModbusRequest xmlns:xsd="http://www.w3.org/2001/XMLSchema" Function="05 - Write Single coil" Start_Address="2" SlaveID="0"/>
<?xml version='1.0' encoding='utf-8'?>
<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="3" Name="ModbusRequest_3"/>
<?xml version='1.0' encoding='utf-8'?>
<ModbusRequest xmlns:xsd="http://www.w3.org/2001/XMLSchema" Function="05 - Write Single coil" SlaveID="0" Start_Address="3"/>
<?xml version='1.0' encoding='utf-8'?>
<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="0" Name="ModbusTCPclient_0"/>
<?xml version='1.0' encoding='utf-8'?>
<ModbusTCPclient xmlns:xsd="http://www.w3.org/2001/XMLSchema" Configuration_Name="Modbus TCP Client 0.0" Remote_IP_Address="192.168.0.48" Invocation_Rate_in_ms="1000"/>
<?xml version='1.0' encoding='utf-8'?>
<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="0" Name="modbus_0"/>
<?xml version='1.0' encoding='utf-8'?>
<ModbusRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema"/>
<?xml version='1.0' encoding='utf-8'?>
<project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
<fileHeader companyName="Unknown" productName="Unnamed" productVersion="1" creationDateTime="2021-05-14T14:33:11"/>
<contentHeader name="Counter (OSIE)" modificationDateTime="2021-05-17T11:58:28">
<contentHeader name="Counter (OSIE)" modificationDateTime="2021-05-19T14:06:39">
<coordinateInfo>
<fbd>
<scaling x="0" y="0"/>
......@@ -32,26 +32,6 @@
<INT/>
</type>
</variable>
<variable name="Cnt1">
<type>
<INT/>
</type>
</variable>
<variable name="Cnt2">
<type>
<INT/>
</type>
</variable>
<variable name="Cnt3">
<type>
<INT/>
</type>
</variable>
<variable name="Cnt4">
<type>
<INT/>
</type>
</variable>
</outputVars>
<localVars>
<variable name="CounterST0">
......@@ -60,24 +40,34 @@
</type>
</variable>
</localVars>
<localVars>
<variable name="Relay0Value" address="%QX0.0.0.0">
<type>
<BOOL/>
</type>
<documentation>
<xhtml:p><![CDATA[Relay 0 Status]]></xhtml:p>
</documentation>
</variable>
</localVars>
</interface>
<body>
<FBD>
<comment localId="1" height="143" width="201">
<position x="378" y="162"/>
<position x="566" y="75"/>
<content>
<xhtml:p><![CDATA[Test Ivan: this PLC will run an increasing counter unless a Reset is switched On (then counter will be reset).]]></xhtml:p>
</content>
</comment>
<block localId="2" typeName="CounterST" instanceName="CounterST0" executionOrderId="0" height="109" width="99">
<position x="122" y="192"/>
<position x="288" y="192"/>
<inputVariables>
<variable formalParameter="Reset">
<connectionPointIn>
<relPosition x="0" y="64"/>
<connection refLocalId="3">
<position x="122" y="256"/>
<position x="51" y="256"/>
<position x="288" y="256"/>
<position x="229" y="256"/>
</connection>
</connectionPointIn>
</variable>
......@@ -92,19 +82,19 @@
</outputVariables>
</block>
<inVariable localId="3" executionOrderId="0" height="24" width="50" negated="false">
<position x="1" y="244"/>
<position x="179" y="244"/>
<connectionPointOut>
<relPosition x="50" y="12"/>
</connectionPointOut>
<expression>Reset</expression>
</inVariable>
<outVariable localId="4" executionOrderId="0" height="24" width="42" negated="false">
<position x="284" y="244"/>
<position x="450" y="244"/>
<connectionPointIn>
<relPosition x="0" y="12"/>
<connection refLocalId="2" formalParameter="Out">
<position x="284" y="256"/>
<position x="221" y="256"/>
<position x="450" y="256"/>
<position x="387" y="256"/>
</connection>
</connectionPointIn>
<expression>Cnt0</expression>
......@@ -141,6 +131,11 @@
<INT/>
</type>
</variable>
<variable name="Relay0Value">
<type>
<BOOL/>
</type>
</variable>
</externalVars>
</interface>
<body>
......@@ -171,8 +166,52 @@ Out := Cnt;]]></xhtml:p>
<INT/>
</type>
<initialValue>
<simpleValue value="1000"/>
<simpleValue value="0"/>
</initialValue>
</variable>
<variable name="Relay0Value" address="%QX0.0.0.0">
<type>
<BOOL/>
</type>
<initialValue>
<simpleValue value="0"/>
</initialValue>
<documentation>
<xhtml:p><![CDATA[The status of the Lime's relay0 (read over modbus)]]></xhtml:p>
</documentation>
</variable>
<variable name="Relay1Value" address="%QX0.0.1.1">
<type>
<BOOL/>
</type>
<initialValue>
<simpleValue value="0"/>
</initialValue>
<documentation>
<xhtml:p><![CDATA[The status of the Lime's relay1 (read over modbus)]]></xhtml:p>
</documentation>
</variable>
<variable name="Relay2Value" address="%QX0.0.2.2">
<type>
<BOOL/>
</type>
<initialValue>
<simpleValue value="0"/>
</initialValue>
<documentation>
<xhtml:p><![CDATA[The status of the Lime's relay2 (read over modbus)]]></xhtml:p>
</documentation>
</variable>
<variable name="Relay3Value" address="%QX0.0.3.3">
<type>
<BOOL/>
</type>
<initialValue>
<simpleValue value="0"/>
</initialValue>
<documentation>
<xhtml:p><![CDATA[The status of the Lime's relay3 (read over modbus)]]></xhtml:p>
</documentation>
</variable>
</globalVars>
</configuration>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment