From 11e9d9ac65aac1df4f1f080cb981d671ab6fb770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Thu, 11 Sep 2025 14:42:17 +0200 Subject: [PATCH 01/44] Add plugin driver system with config parsing Introduces a plugin driver framework supporting both native and Python plugins, including configuration parsing, driver management, and integration into the main PLC application. Updates CMake to link Python libraries and adds example configuration files for plugins. (WIP) --- .gitignore | 2 + core/src/CMakeLists.txt | 18 +++++- core/src/drivers/driver_api.c | 0 core/src/drivers/driver_api.h | 6 ++ core/src/drivers/plugin_config.c | 53 ++++++++++++++++ core/src/drivers/plugin_config.h | 17 ++++++ core/src/drivers/plugin_config_example.txt | 6 ++ core/src/drivers/plugin_driver.h | 70 ++++++++++++++++++++++ core/src/drivers/python_plugin_bridge.h | 21 +++++++ core/src/plc_app/plc_main.c | 23 ++++++- plugins.conf | 5 ++ 11 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 core/src/drivers/driver_api.c create mode 100644 core/src/drivers/driver_api.h create mode 100644 core/src/drivers/plugin_config.c create mode 100644 core/src/drivers/plugin_config.h create mode 100644 core/src/drivers/plugin_config_example.txt create mode 100644 core/src/drivers/plugin_driver.h create mode 100644 core/src/drivers/python_plugin_bridge.h create mode 100644 plugins.conf diff --git a/.gitignore b/.gitignore index 1179d56b..a15bf2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ # Ignore all files in the build output directory /build*/ /core/generated/ +/memory-bank # .vscode/ .*/ venv/ __pycache__/ +.clinerules install_log.txt diff --git a/core/src/CMakeLists.txt b/core/src/CMakeLists.txt index 5fb5ffd9..1692a155 100644 --- a/core/src/CMakeLists.txt +++ b/core/src/CMakeLists.txt @@ -3,17 +3,25 @@ project(plc_application C) set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# Find Python development libraries +find_package(PkgConfig REQUIRED) +pkg_check_modules(PYTHON REQUIRED python3-embed) + # Include directories include_directories(${CMAKE_SOURCE_DIR}/core/src/plc_app ${CMAKE_SOURCE_DIR}/core/src/plc_app/utils ${CMAKE_SOURCE_DIR}/core/generated/plc_lib - ${CMAKE_SOURCE_DIR}/core/generated/plc_lib/lib) + ${CMAKE_SOURCE_DIR}/core/generated/plc_lib/lib + ${PYTHON_INCLUDE_DIRS}) # Compiler options add_compile_options(-Wall -Werror -Wextra -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -Wformat -Werror=format-security -fPIC -fPIE) +# Add drivers subdirectory +add_subdirectory(drivers) + # Step 3: Build the executable and link against the shared library add_executable(plc_main ${CMAKE_SOURCE_DIR}/core/src/plc_app/plc_main.c @@ -23,10 +31,16 @@ add_executable(plc_main ${CMAKE_SOURCE_DIR}/core/src/plc_app/image_tables.c ${CMAKE_SOURCE_DIR}/core/src/plc_app/plcapp_manager.c ${CMAKE_SOURCE_DIR}/core/src/plc_app/scan_cycle_manager.c + ${CMAKE_SOURCE_DIR}/core/src/drivers/plugin_driver.c + ${CMAKE_SOURCE_DIR}/core/src/drivers/plugin_config.c ) # Link against shared library -target_link_libraries(plc_main dl pthread) +target_link_libraries(plc_main + dl + pthread + ${PYTHON_LIBRARIES} +) # Ensure executable can find shared library at runtime set_target_properties(plc_main PROPERTIES diff --git a/core/src/drivers/driver_api.c b/core/src/drivers/driver_api.c new file mode 100644 index 00000000..e69de29b diff --git a/core/src/drivers/driver_api.h b/core/src/drivers/driver_api.h new file mode 100644 index 00000000..9d74185b --- /dev/null +++ b/core/src/drivers/driver_api.h @@ -0,0 +1,6 @@ +#ifndef __DRIVER_API_H__ +#define __DRIVER_API_H__ + + + +#endif /*__DRIVER_API_H__*/ \ No newline at end of file diff --git a/core/src/drivers/plugin_config.c b/core/src/drivers/plugin_config.c new file mode 100644 index 00000000..396d76a9 --- /dev/null +++ b/core/src/drivers/plugin_config.c @@ -0,0 +1,53 @@ +#include "plugin_config.h" +#include +#include +#include + +int parse_plugin_config(const char *config_file, plugin_config_t *configs, int max_configs) { + FILE *file = fopen(config_file, "r"); + if (!file) { + return -1; + } + + char line[512]; + int config_count = 0; + + while (fgets(line, sizeof(line), file) && config_count < max_configs) { + // Skip comments and empty lines + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') { + continue; + } + + // Parse plugin configuration: name,path,enabled,type,plugin_related_config_path + // Parsing name + char *token = strtok(line, ","); + if (!token) continue; + strncpy(configs[config_count].name, token, sizeof(configs[config_count].name) - 1); + + // Parsing path + token = strtok(NULL, ","); + if (!token) continue; + strncpy(configs[config_count].path, token, sizeof(configs[config_count].path) - 1); + + // Parsing enabled + token = strtok(NULL, ","); + if (!token) continue; + configs[config_count].enabled = atoi(token); + + // Parsing type + token = strtok(NULL, ","); + if (!token) continue; + configs[config_count].type = atoi(token); + + // parsing plugin_related_config_path + token = strtok(NULL, ","); + if (!token) continue; + strncpy(configs[config_count].plugin_related_config_path, token, sizeof(configs[config_count].plugin_related_config_path) - 1); + + // Incrementing index to target next config + config_count++; + } + + fclose(file); + return config_count; +} diff --git a/core/src/drivers/plugin_config.h b/core/src/drivers/plugin_config.h new file mode 100644 index 00000000..e0e58030 --- /dev/null +++ b/core/src/drivers/plugin_config.h @@ -0,0 +1,17 @@ +#ifndef PLUGIN_CONFIG_H +#define PLUGIN_CONFIG_H + +#define MAX_PLUGIN_NAME_LEN 64 +#define MAX_PLUGIN_PATH_LEN 256 + +typedef struct { + char name[MAX_PLUGIN_NAME_LEN]; + char path[MAX_PLUGIN_PATH_LEN]; + int enabled; + int type; // 0 = native, 1 = python + char plugin_related_config_path[MAX_PLUGIN_PATH_LEN]; +} plugin_config_t; + +int parse_plugin_config(const char *config_file, plugin_config_t *configs, int max_configs); + +#endif // PLUGIN_CONFIG_H diff --git a/core/src/drivers/plugin_config_example.txt b/core/src/drivers/plugin_config_example.txt new file mode 100644 index 00000000..36e45679 --- /dev/null +++ b/core/src/drivers/plugin_config_example.txt @@ -0,0 +1,6 @@ +# Plugin configuration file +# Format: name,path,enabled,type,plugin_related_config_path +# Example plugins +example_plugin1,./plugins/example1.so,1 +example_plugin2,./plugins/example2.so,0 +python_plugin,./plugins/python_bridge.so,1 diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h new file mode 100644 index 00000000..3d4842fa --- /dev/null +++ b/core/src/drivers/plugin_driver.h @@ -0,0 +1,70 @@ +#ifndef PLUGIN_DRIVER_H +#define PLUGIN_DRIVER_H + +#include +#include "../lib/iec_types.h" +#include "../plc_app/plcapp_manager.h" +#include "plugin_config.h" +// #include "python_plugin_bridge.h" + +// Maximum number of plugins +#define MAX_PLUGINS 16 + +typedef enum { + PLUGIN_TYPE_PYTHON, + PLUGIN_TYPE_NATIVE +} plugin_type_t; + +// Plugin configuration structure +// typedef struct { +// char name[64]; +// char path[256]; +// int enabled; +// plugin_type_t type; +// } plugin_config_t; + +// Plugin instance structure +typedef struct { + union { + PluginManager *manager; + // python_plugin_t *python_plugin; + }; + pthread_t thread; + int running; + plugin_config_t config; +} plugin_instance_t; + +// Driver structure +typedef struct { + plugin_instance_t plugins[MAX_PLUGINS]; + int plugin_count; + pthread_mutex_t buffer_mutex; +} plugin_driver_t; + +// Buffer access functions for plugins +IEC_BOOL plugin_get_bool_input(int index, int bit); +void plugin_set_bool_output(int index, int bit, IEC_BOOL value); +IEC_BYTE plugin_get_byte_input(int index); +void plugin_set_byte_output(int index, IEC_BYTE value); +IEC_UINT plugin_get_int_input(int index); +void plugin_set_int_output(int index, IEC_UINT value); +IEC_UDINT plugin_get_dint_input(int index); +void plugin_set_dint_output(int index, IEC_UDINT value); +IEC_ULINT plugin_get_lint_input(int index); +void plugin_set_lint_output(int index, IEC_ULINT value); +IEC_UINT plugin_get_int_memory(int index); +void plugin_set_int_memory(int index, IEC_UINT value); +IEC_UDINT plugin_get_dint_memory(int index); +void plugin_set_dint_memory(int index, IEC_UDINT value); +IEC_ULINT plugin_get_lint_memory(int index); +void plugin_set_lint_memory(int index, IEC_ULINT value); + +// Driver management functions +plugin_driver_t* plugin_driver_create(void); +int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file); +int plugin_driver_init(plugin_driver_t *driver); +int plugin_driver_start(plugin_driver_t *driver); +int plugin_driver_stop(plugin_driver_t *driver); +void plugin_driver_destroy(plugin_driver_t *driver); + +#endif // PLUGIN_DRIVER_H diff --git a/core/src/drivers/python_plugin_bridge.h b/core/src/drivers/python_plugin_bridge.h new file mode 100644 index 00000000..adc3500a --- /dev/null +++ b/core/src/drivers/python_plugin_bridge.h @@ -0,0 +1,21 @@ +#ifndef __PYTHON_PLUGIN_BRIDGE_H +#define __PYTHON_PLUGIN_BRIDGE_H + +#include + +// Python plugin bridge structure +typedef struct { + PyObject *pModule; + PyObject *pFuncInit; // Driver Init function + PyObject *pFuncStartLoop; + PyObject *pFuncStopLoop; + PyObject *pFuncCycleRun; + PyObject *pFuncCleanup; +} python_plugin_t; + +int python_plugin_init(plugin_instance_t *plugin); +void python_plugin_cycle(plugin_instance_t *plugin); +void python_plugin_cleanup(plugin_instance_t *plugin); + + +#endif // __PYTHON_PLUGIN_BRIDGE_H \ No newline at end of file diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index d0b4e1bc..ebf7c5c5 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -15,6 +15,7 @@ #include "utils/utils.h" #include "utils/watchdog.h" #include "scan_cycle_manager.h" +#include "../drivers/plugin_driver.h" extern atomic_long plc_heartbeat; extern PLCState plc_state; @@ -154,6 +155,19 @@ int main() return -1; } + // Initialize plugin driver system + plugin_driver_t *plugin_driver = plugin_driver_create(); + if (plugin_driver) { + // Load plugin configuration + if (plugin_driver_load_config(plugin_driver, "../plugins.conf") == 0) { + // Start plugins + plugin_driver_start(plugin_driver); + log_info("[PLUGIN]: Plugin driver system initialized"); + } else { + log_error("[PLUGIN]: Failed to load plugin configuration"); + } + } + // Load user application code plc_program = plugin_manager_create("./libplc.so"); load_plc_program(plc_program); @@ -179,5 +193,12 @@ int main() pthread_join(stats_thread, NULL); pthread_join(plc_thread, NULL); plugin_manager_destroy(plc_program); + + // Cleanup plugin driver system + if (plugin_driver) { + plugin_driver_stop(plugin_driver); + plugin_driver_destroy(plugin_driver); + } + return 0; -} \ No newline at end of file +} diff --git a/plugins.conf b/plugins.conf new file mode 100644 index 00000000..7de8e647 --- /dev/null +++ b/plugins.conf @@ -0,0 +1,5 @@ +# Plugin configuration file +# Format: name,path,enabled +example_plugin1,./plugins/example1.so,1,1,./config.txt +example_plugin2,./plugins/example2.so,0,1,./config.txt +python_plugin,./plugins/python_bridge.so,1,0,./config.txt From fb3b5d09e565734c99e717d466fbb93fe2ff9d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Thu, 11 Sep 2025 21:15:33 +0200 Subject: [PATCH 02/44] Refactor plugin driver for Python Modbus support Removed legacy driver_api files and introduced new plugin driver structures to support Python-based plugins. Added a simple_modbus.py driver and configuration for Modbus slave integration. Updated plugin_driver.h and python_plugin_bridge.h to support Python plugin bindings and runtime argument passing. Modified plc_main.c to call plugin_driver_init instead of plugin_driver_start. Updated plugins.conf to use the new Python Modbus plugin. --- core/src/drivers/driver_api.c | 0 core/src/drivers/driver_api.h | 6 - core/src/drivers/modbus_slave_config.ini | 23 ++ core/src/drivers/plugin_driver.h | 83 ++++--- core/src/drivers/python_plugin_bridge.h | 10 +- core/src/drivers/simple_modbus.py | 275 +++++++++++++++++++++++ core/src/plc_app/plc_main.c | 2 +- plugins.conf | 7 +- 8 files changed, 358 insertions(+), 48 deletions(-) delete mode 100644 core/src/drivers/driver_api.c delete mode 100644 core/src/drivers/driver_api.h create mode 100644 core/src/drivers/modbus_slave_config.ini create mode 100644 core/src/drivers/simple_modbus.py diff --git a/core/src/drivers/driver_api.c b/core/src/drivers/driver_api.c deleted file mode 100644 index e69de29b..00000000 diff --git a/core/src/drivers/driver_api.h b/core/src/drivers/driver_api.h deleted file mode 100644 index 9d74185b..00000000 --- a/core/src/drivers/driver_api.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef __DRIVER_API_H__ -#define __DRIVER_API_H__ - - - -#endif /*__DRIVER_API_H__*/ \ No newline at end of file diff --git a/core/src/drivers/modbus_slave_config.ini b/core/src/drivers/modbus_slave_config.ini new file mode 100644 index 00000000..70e9ff40 --- /dev/null +++ b/core/src/drivers/modbus_slave_config.ini @@ -0,0 +1,23 @@ +# Modbus Slave Plugin Configuration Example +# This file shows how to configure the Modbus slave plugin + +[plugin_modbus_slave] +type = PLUGIN_TYPE_PYTHON +path = /home/marcone/Documents/Github/openplc-runtime/core/src/drivers/modbus_slave.py +enabled = true +description = "Modbus TCP Slave server that exposes OpenPLC buffers" + +# Network configuration +host = 172.29.65.104 +port = 5020 + +# Buffer mapping configuration +# Coils (Read/Write) -> bool_output[0-7999] (1000 buffers * 8 bits) +# Discrete Inputs (Read Only) -> bool_input[0-7999] (1000 buffers * 8 bits) +# Holding Registers -> int_output[0-999] (future implementation) +# Input Registers -> int_input[0-999] (future implementation) + +max_coils = 8000 +max_discrete_inputs = 8000 +max_holding_registers = 1000 +max_input_registers = 1000 diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index 3d4842fa..844b01a0 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -5,7 +5,8 @@ #include "../lib/iec_types.h" #include "../plc_app/plcapp_manager.h" #include "plugin_config.h" -// #include "python_plugin_bridge.h" +#include "python_plugin_bridge.h" +#include // Maximum number of plugins #define MAX_PLUGINS 16 @@ -15,20 +16,51 @@ typedef enum { PLUGIN_TYPE_NATIVE } plugin_type_t; -// Plugin configuration structure -// typedef struct { -// char name[64]; -// char path[256]; -// int enabled; -// plugin_type_t type; -// } plugin_config_t; +typedef int (*plugin_init_func_t)(void *); +typedef void (*plugin_start_loop_func_t)(); +typedef void (*plugin_stop_loop_func_t)(); +typedef void (*plugin_run_cycle_func_t)(); +typedef void (*plugin_cleanup_func_t)(); -// Plugin instance structure typedef struct { - union { - PluginManager *manager; - // python_plugin_t *python_plugin; - }; + plugin_init_func_t init; + plugin_start_loop_func_t start; + plugin_stop_loop_func_t stop; + plugin_run_cycle_func_t run_cycle; + plugin_cleanup_func_t cleanup; +} plugin_funct_bundle_t; + +// Runtime buffer access structure for plugins +typedef struct { + // Buffer pointers + IEC_BOOL *(*bool_input)[8]; + IEC_BOOL *(*bool_output)[8]; + IEC_BYTE **byte_input; + IEC_BYTE **byte_output; + IEC_UINT **int_input; + IEC_UINT **int_output; + IEC_UDINT **dint_input; + IEC_UDINT **dint_output; + IEC_ULINT **lint_input; + IEC_ULINT **lint_output; + IEC_UINT **int_memory; + IEC_UDINT **dint_memory; + IEC_ULINT **lint_memory; + + // Mutex functions + int (*mutex_take)(pthread_mutex_t *mutex); + int (*mutex_give)(pthread_mutex_t *mutex); + pthread_mutex_t *buffer_mutex; + + // Buffer size information + int buffer_size; + int bits_per_buffer; +} plugin_runtime_args_t; + +// Plugin instance structure +typedef struct plugin_instance_s { + PluginManager *manager; + python_binds_t *python_plugin; pthread_t thread; int running; plugin_config_t config; @@ -41,24 +73,6 @@ typedef struct { pthread_mutex_t buffer_mutex; } plugin_driver_t; -// Buffer access functions for plugins -IEC_BOOL plugin_get_bool_input(int index, int bit); -void plugin_set_bool_output(int index, int bit, IEC_BOOL value); -IEC_BYTE plugin_get_byte_input(int index); -void plugin_set_byte_output(int index, IEC_BYTE value); -IEC_UINT plugin_get_int_input(int index); -void plugin_set_int_output(int index, IEC_UINT value); -IEC_UDINT plugin_get_dint_input(int index); -void plugin_set_dint_output(int index, IEC_UDINT value); -IEC_ULINT plugin_get_lint_input(int index); -void plugin_set_lint_output(int index, IEC_ULINT value); -IEC_UINT plugin_get_int_memory(int index); -void plugin_set_int_memory(int index, IEC_UINT value); -IEC_UDINT plugin_get_dint_memory(int index); -void plugin_set_dint_memory(int index, IEC_UDINT value); -IEC_ULINT plugin_get_lint_memory(int index); -void plugin_set_lint_memory(int index, IEC_ULINT value); - // Driver management functions plugin_driver_t* plugin_driver_create(void); int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file); @@ -67,4 +81,11 @@ int plugin_driver_start(plugin_driver_t *driver); int plugin_driver_stop(plugin_driver_t *driver); void plugin_driver_destroy(plugin_driver_t *driver); +// Python plugin functions +int python_plugin_get_symbols(plugin_instance_t *plugin); + +// Runtime arguments generation +void* generate_structured_args(plugin_type_t type); +void free_structured_args(plugin_runtime_args_t *args); + #endif // PLUGIN_DRIVER_H diff --git a/core/src/drivers/python_plugin_bridge.h b/core/src/drivers/python_plugin_bridge.h index adc3500a..62a93fe3 100644 --- a/core/src/drivers/python_plugin_bridge.h +++ b/core/src/drivers/python_plugin_bridge.h @@ -3,6 +3,9 @@ #include +// Forward declaration +struct plugin_instance_s; + // Python plugin bridge structure typedef struct { PyObject *pModule; @@ -11,11 +14,6 @@ typedef struct { PyObject *pFuncStopLoop; PyObject *pFuncCycleRun; PyObject *pFuncCleanup; -} python_plugin_t; - -int python_plugin_init(plugin_instance_t *plugin); -void python_plugin_cycle(plugin_instance_t *plugin); -void python_plugin_cleanup(plugin_instance_t *plugin); - +} python_binds_t; #endif // __PYTHON_PLUGIN_BRIDGE_H \ No newline at end of file diff --git a/core/src/drivers/simple_modbus.py b/core/src/drivers/simple_modbus.py new file mode 100644 index 00000000..02916b45 --- /dev/null +++ b/core/src/drivers/simple_modbus.py @@ -0,0 +1,275 @@ +import asyncio +import ctypes +from ctypes import POINTER, c_bool, c_ubyte, c_uint16, c_uint32, c_uint64, c_int, c_void_p, CFUNCTYPE +import threading +import time +from pymodbus.server import StartAsyncTcpServer, ServerStop +from pymodbus.datastore import ( + ModbusSparseDataBlock, + ModbusDeviceContext, + ModbusServerContext, +) + +class PluginRuntimeArgs(ctypes.Structure): + """Python ctypes structure matching plugin_runtime_args_t""" + _fields_ = [ + # Buffer arrays (using POINTER type for arrays) + ("bool_input", POINTER(POINTER(c_bool * 8))), # bool_input[BUFFER_SIZE][8] + ("bool_output", POINTER(POINTER(c_bool * 8))), # bool_output[BUFFER_SIZE][8] + ("byte_input", POINTER(POINTER(c_ubyte))), # byte_input[BUFFER_SIZE] + ("byte_output", POINTER(POINTER(c_ubyte))), # byte_output[BUFFER_SIZE] + ("int_input", POINTER(POINTER(c_uint16))), # int_input[BUFFER_SIZE] + ("int_output", POINTER(POINTER(c_uint16))), # int_output[BUFFER_SIZE] + ("dint_input", POINTER(POINTER(c_uint32))), # dint_input[BUFFER_SIZE] + ("dint_output", POINTER(POINTER(c_uint32))), # dint_output[BUFFER_SIZE] + ("lint_input", POINTER(POINTER(c_uint64))), # lint_input[BUFFER_SIZE] + ("lint_output", POINTER(POINTER(c_uint64))), # lint_output[BUFFER_SIZE] + ("int_memory", POINTER(POINTER(c_uint16))), # int_memory[BUFFER_SIZE] + ("dint_memory", POINTER(POINTER(c_uint32))), # dint_memory[BUFFER_SIZE] + ("lint_memory", POINTER(POINTER(c_uint64))), # lint_memory[BUFFER_SIZE] + + # Mutex function pointers + ("mutex_take", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_take)(pthread_mutex_t*) + ("mutex_give", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_give)(pthread_mutex_t*) + ("buffer_mutex", c_void_p), # pthread_mutex_t *buffer_mutex + + # Buffer size information + ("buffer_size", c_int), # int buffer_size + ("bits_per_buffer", c_int), # int bits_per_buffer + ] + +class OpenPLCModbusDataBlock(ModbusSparseDataBlock): + """Custom Modbus data block that mirrors OpenPLC bool_output""" + + def __init__(self, runtime_args, buffer_index=0, num_coils=64): + self.runtime_args = runtime_args + self.buffer_index = buffer_index + self.num_coils = num_coils + + # Initialize with zeros + super().__init__([0] * num_coils) + + def getValues(self, address, count=1): + """Get coil values from OpenPLC bool_output""" + try: + # Take mutex before reading + if (hasattr(self.runtime_args, 'mutex_take') and + hasattr(self.runtime_args, 'buffer_mutex') and + self.runtime_args.mutex_take and + self.runtime_args.buffer_mutex): + self.runtime_args.mutex_take(self.runtime_args.buffer_mutex) + + values = [] + for i in range(count): + coil_addr = address + i + + # Check if we have valid runtime args and buffer + if (hasattr(self.runtime_args, 'bool_output') and + self.runtime_args.bool_output and + coil_addr < self.num_coils and + self.buffer_index < getattr(self.runtime_args, 'buffer_size', 1)): + + try: + # Extract bit from bool_output[buffer_index][byte_index] + byte_index = coil_addr // 8 + bit_index = coil_addr % 8 + + if byte_index < 8: # 8 bytes per buffer + bool_array = self.runtime_args.bool_output[self.buffer_index] + # Get the boolean value directly + if bool_array and len(bool_array) > byte_index: + bit_value = bool(bool_array[byte_index].value if hasattr(bool_array[byte_index], 'value') else bool_array[byte_index]) + values.append(1 if bit_value else 0) + else: + values.append(0) + else: + values.append(0) + except (IndexError, AttributeError, OSError): + values.append(0) + else: + values.append(0) + + return values + + except Exception as e: + # In case of any error, return zeros + return [0] * count + finally: + # Release mutex + if (hasattr(self.runtime_args, 'mutex_give') and + hasattr(self.runtime_args, 'buffer_mutex') and + self.runtime_args.mutex_give and + self.runtime_args.buffer_mutex): + try: + self.runtime_args.mutex_give(self.runtime_args.buffer_mutex) + except: + pass + + def setValues(self, address, values): + """Set coil values to OpenPLC bool_output""" + try: + # Take mutex before writing + if (hasattr(self.runtime_args, 'mutex_take') and + hasattr(self.runtime_args, 'buffer_mutex') and + self.runtime_args.mutex_take and + self.runtime_args.buffer_mutex): + self.runtime_args.mutex_take(self.runtime_args.buffer_mutex) + + for i, value in enumerate(values): + coil_addr = address + i + + # Check if we have valid runtime args and buffer + if (hasattr(self.runtime_args, 'bool_output') and + self.runtime_args.bool_output and + coil_addr < self.num_coils and + self.buffer_index < getattr(self.runtime_args, 'buffer_size', 1)): + + try: + # Set bit in bool_output[buffer_index][byte_index] + byte_index = coil_addr // 8 + + if byte_index < 8: # 8 bytes per buffer + bool_array = self.runtime_args.bool_output[self.buffer_index] + if bool_array and len(bool_array) > byte_index: + # Set the boolean value directly + if hasattr(bool_array[byte_index], 'value'): + bool_array[byte_index].value = bool(value) + else: + bool_array[byte_index] = bool(value) + except (IndexError, AttributeError, OSError): + pass # Ignore errors in setting values + + except Exception: + pass # Ignore any errors + finally: + # Release mutex + if (hasattr(self.runtime_args, 'mutex_give') and + hasattr(self.runtime_args, 'buffer_mutex') and + self.runtime_args.mutex_give and + self.runtime_args.buffer_mutex): + try: + self.runtime_args.mutex_give(self.runtime_args.buffer_mutex) + except: + pass + +# Global variables for plugin lifecycle +server_task = None +server_context = None +runtime_args = None +update_thread = None +running = False + +def init(args, host="127.0.0.1", port=5020): + """Initialize the Modbus plugin""" + global runtime_args, server_context + + runtime_args = args + + # Create OpenPLC-connected coils data block + coils_block = OpenPLCModbusDataBlock(runtime_args, buffer_index=0, num_coils=64) + + # Standard data blocks for other Modbus types + di = ModbusSparseDataBlock([0] * 64) # Discrete Inputs + ir = ModbusSparseDataBlock([0] * 32) # Input Registers (16-bit) + hr = ModbusSparseDataBlock([0] * 32) # Holding Registers (16-bit) + + # Create device context with OpenPLC-connected coils + device = ModbusDeviceContext(di=di, co=coils_block, ir=ir, hr=hr) + server_context = ModbusServerContext(devices={1: device}, single=False) + + print(f"[MODBUS] Plugin initialized - Host: {host}, Port: {port}") + return True + +def start_loop(): + """Start the Modbus server""" + global server_task, running, update_thread + + if server_context is None: + print("[MODBUS] Error: Plugin not initialized") + return False + + running = True + + # Start server in separate thread + def run_server(): + asyncio.run(StartAsyncTcpServer( + context=server_context, + address=("172.29.65.104", 5020) + )) + + server_task = threading.Thread(target=run_server, daemon=True) + server_task.start() + + print("[MODBUS] Server started on 172.29.65.104:5020") + return True + +def stop_loop(): + """Stop the Modbus server""" + global server_task, running, update_thread + + running = False + + if update_thread: + update_thread.join(timeout=1.0) + update_thread = None + + if server_task: + # Stop the asyncio server + try: + asyncio.run(ServerStop()) + except: + pass + + server_task.join(timeout=2.0) + server_task = None + + print("[MODBUS] Server stopped") + return True + +def cleanup(): + """Cleanup plugin resources""" + global server_context, runtime_args + + server_context = None + runtime_args = None + + print("[MODBUS] Plugin cleaned up") + return True + +async def main(): + """Standalone server for testing""" + # Mock runtime args for testing + class MockArgs: + def __init__(self): + self.buffer_size = 1 + self.bits_per_buffer = 64 + # Create simple boolean list for testing + self.bool_data = [[False] * 8] # 1 buffer, 8 booleans + self.bool_output = self.bool_data # Simple reference + self.mutex_take = None + self.mutex_give = None + self.buffer_mutex = None + + mock_args = MockArgs() + + # Initialize and start + if init(mock_args): + if start(): + print("Modbus server running on 172.29.65.104:5020") + print("Press Ctrl+C to stop...") + + try: + # Keep server running + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + print("\nStopping server...") + stop() + cleanup() + else: + print("Failed to start server") + else: + print("Failed to initialize plugin") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index ebf7c5c5..49815486 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -161,7 +161,7 @@ int main() // Load plugin configuration if (plugin_driver_load_config(plugin_driver, "../plugins.conf") == 0) { // Start plugins - plugin_driver_start(plugin_driver); + plugin_driver_init(plugin_driver); log_info("[PLUGIN]: Plugin driver system initialized"); } else { log_error("[PLUGIN]: Failed to load plugin configuration"); diff --git a/plugins.conf b/plugins.conf index 7de8e647..36272ec4 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,5 +1,4 @@ # Plugin configuration file -# Format: name,path,enabled -example_plugin1,./plugins/example1.so,1,1,./config.txt -example_plugin2,./plugins/example2.so,0,1,./config.txt -python_plugin,./plugins/python_bridge.so,1,0,./config.txt +# Format: name,path,enabled,type,config_path +#python_plugin,../core/src/drivers/modbus_slave.py,1,0,./modbus_slave_config.ini +python_plugin,../core/src/drivers/simple_modbus.py,1,0,./modbus_slave_config.ini From cd10661a019de5cf4538e67f506c95b0bcfbc75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Thu, 11 Sep 2025 21:20:10 +0200 Subject: [PATCH 03/44] sync plugin driver --- core/src/drivers/plugin_driver.c | 410 +++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 core/src/drivers/plugin_driver.c diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c new file mode 100644 index 00000000..7ab9e4a4 --- /dev/null +++ b/core/src/drivers/plugin_driver.c @@ -0,0 +1,410 @@ +#include "plugin_driver.h" +#include +#include +#include +#include +#include +#include "../plc_app/image_tables.h" +#include "plugin_config.h" + +// External buffer declarations from image_tables.c +extern IEC_BOOL *bool_input[BUFFER_SIZE][8]; +extern IEC_BOOL *bool_output[BUFFER_SIZE][8]; +extern IEC_BYTE *byte_input[BUFFER_SIZE]; +extern IEC_BYTE *byte_output[BUFFER_SIZE]; +extern IEC_UINT *int_input[BUFFER_SIZE]; +extern IEC_UINT *int_output[BUFFER_SIZE]; +extern IEC_UDINT *dint_input[BUFFER_SIZE]; +extern IEC_UDINT *dint_output[BUFFER_SIZE]; +extern IEC_ULINT *lint_input[BUFFER_SIZE]; +extern IEC_ULINT *lint_output[BUFFER_SIZE]; +extern IEC_UINT *int_memory[BUFFER_SIZE]; +extern IEC_UDINT *dint_memory[BUFFER_SIZE]; +extern IEC_ULINT *lint_memory[BUFFER_SIZE]; + +// Plugin thread function +void* plugin_thread_function(void *arg) { + plugin_instance_t *plugin = (plugin_instance_t *)arg; + + // Load the plugin + if (!plugin_manager_load(plugin->manager)) { + fprintf(stderr, "Failed to load plugin: %s\n", plugin->config.path); + return NULL; + } + + // TODO: Here you would generate structured args and call plugin init + // Example: + // plugin_runtime_args_t *args = generate_structured_args(driver, plugin->config.type); + // if (args) { + // // Get init symbol from plugin + // typedef int (*plugin_init_func_t)(plugin_runtime_args_t *args); + // plugin_init_func_t init_func = dlsym(plugin_handle, "init"); + // if (init_func) { + // init_func(args); + // } + // free_structured_args(args); + // } + + plugin->running = 1; + + // Main plugin loop + while (plugin->running) { + // Here plugins can do their work + // They can call the buffer access functions + usleep(10000); // 10ms sleep to prevent busy waiting + } + + return NULL; +} + +// Driver management functions +plugin_driver_t* plugin_driver_create(void) { + plugin_driver_t *driver = calloc(1, sizeof(plugin_driver_t)); + if (!driver) { + return NULL; + } + + // Initialize mutex + if (pthread_mutex_init(&driver->buffer_mutex, NULL) != 0) { + free(driver); + return NULL; + } + + return driver; +} + +// Mutex helper functions for plugins +static int plugin_mutex_take(pthread_mutex_t *mutex) { + return pthread_mutex_lock(mutex); +} + +static int plugin_mutex_give(pthread_mutex_t *mutex) { + return pthread_mutex_unlock(mutex); +} + +// Python capsule destructor for runtime args +static void plugin_runtime_args_capsule_destructor(PyObject *capsule) { + plugin_runtime_args_t *args = (plugin_runtime_args_t *)PyCapsule_GetPointer(capsule, "openplc_runtime_args"); + if (args) { + free_structured_args(args); + } +} + +// Create Python capsule with runtime arguments +static PyObject* create_python_runtime_args_capsule(plugin_runtime_args_t *args) { + if (!args) { + return NULL; + } + + // Create a capsule containing the runtime args pointer + PyObject *capsule = PyCapsule_New(args, "openplc_runtime_args", plugin_runtime_args_capsule_destructor); + if (!capsule) { + // If capsule creation fails, we need to free the args manually + free_structured_args(args); + return NULL; + } + + return capsule; +} + +int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file) { + if (!driver || !config_file) { + return -1; + } + + plugin_config_t configs[MAX_PLUGINS]; + int config_count = parse_plugin_config(config_file, configs, MAX_PLUGINS); + if (config_count < 0) { + return -1; + } + + driver->plugin_count = config_count; + for (int w = 0; w < config_count; w++) { + memcpy(&driver->plugins[w].config, &configs[w], sizeof(plugin_config_t)); + } + + // Agora leio todos os simbolos que preciso (init, start, stop, cycle, cleanup) e adiciono na struct plugin_instance_t + // para cada plugin. + for (int i = 0; i < driver->plugin_count; i++) { + plugin_instance_t *plugin = &driver->plugins[i]; + + if (plugin->config.type == PLUGIN_TYPE_PYTHON) { + if (python_plugin_get_symbols(plugin) != 0) { + fprintf(stderr, "Failed to get Python plugin symbols for: %s\n", plugin->config.path); + plugin_manager_destroy(plugin->manager); + return -1; + } + } + } + + return 0; +} + +// Send to plugin init function all args +int plugin_driver_init(plugin_driver_t *driver) { + if (!driver) { + return -1; + } + + return 0; +} + + +// Call the thread function for each plugin +int plugin_driver_start(plugin_driver_t *driver) { + if (!driver) { + return -1; + } + + for (int i = 0; i < driver->plugin_count; i++) { + plugin_instance_t *plugin = &driver->plugins[i]; + + if (pthread_create(&plugin->thread, NULL, plugin_thread_function, plugin) != 0) { // TODO: Here should be called the start_loop function from plugin + fprintf(stderr, "Failed to create thread for plugin: %s\n", plugin->config.name); + return -1; + } + } + + return 0; +} + +int plugin_driver_stop(plugin_driver_t *driver) { + if (!driver) { + return -1; + } + + // Signal all plugins to stop + for (int i = 0; i < driver->plugin_count; i++) { + driver->plugins[i].running = 0; + } + + // Wait for all threads to finish + for (int i = 0; i < driver->plugin_count; i++) { + if (driver->plugins[i].thread) { + pthread_join(driver->plugins[i].thread, NULL); + } + if (driver->plugins[i].manager) { + plugin_manager_destroy(driver->plugins[i].manager); + driver->plugins[i].manager = NULL; + } + } + + return 0; +} + +void plugin_driver_destroy(plugin_driver_t *driver) { + if (!driver) { + return; + } + + plugin_driver_stop(driver); + pthread_mutex_destroy(&driver->buffer_mutex); + free(driver); +} + +// Runtime arguments generation functions + +/** + * @brief Generate structured arguments for plugin initialization + * + * This function creates a structured argument containing all runtime buffers, + * mutex functions, and metadata needed by external plugins. + * + * @param type Type of plugin (PLUGIN_TYPE_PYTHON or PLUGIN_TYPE_NATIVE) + * @return Pointer to allocated structure/capsule, or NULL on error + * + * For PLUGIN_TYPE_NATIVE: Returns plugin_runtime_args_t* + * For PLUGIN_TYPE_PYTHON: Returns PyObject* (PyCapsule containing plugin_runtime_args_t*) + */ +void* generate_structured_args(plugin_type_t type) { + plugin_runtime_args_t *args = malloc(sizeof(plugin_runtime_args_t)); + if (!args) { + return NULL; + } + + // Initialize all buffer pointers + args->bool_input = bool_input; + args->bool_output = bool_output; + args->byte_input = byte_input; + args->byte_output = byte_output; + args->int_input = int_input; + args->int_output = int_output; + args->dint_input = dint_input; + args->dint_output = dint_output; + args->lint_input = lint_input; + args->lint_output = lint_output; + args->int_memory = int_memory; + args->dint_memory = dint_memory; + args->lint_memory = lint_memory; + + // Initialize mutex functions + args->mutex_take = plugin_mutex_take; + args->mutex_give = plugin_mutex_give; + // Note: buffer_mutex should be passed as parameter in future versions + args->buffer_mutex = NULL; // Will be set by caller + + // Initialize buffer size info + args->buffer_size = BUFFER_SIZE; + args->bits_per_buffer = 8; + + switch (type) { + case PLUGIN_TYPE_NATIVE: + // For native plugins, return the structure directly + return args; + + case PLUGIN_TYPE_PYTHON: + // For Python plugins, wrap in a PyCapsule + return create_python_runtime_args_capsule(args); + + default: + // Unknown type, clean up and return NULL + free(args); + return NULL; + } +} + +// Free structured arguments +void free_structured_args(plugin_runtime_args_t *args) { + if (args) { + // No dynamic allocations inside the structure to free + // Just free the main structure + free(args); + } +} + +int python_plugin_get_symbols(plugin_instance_t *plugin) { + if (!plugin || !plugin->config.path) { + return -1; + } + + // Allocate python binds structure + python_binds_t *py_binds = calloc(1, sizeof(python_binds_t)); + if (!py_binds) { + return -1; + } + + // Initialize Python if not already initialized + if (!Py_IsInitialized()) { + Py_Initialize(); + } + + // Extract module name from plugin path + // Remove .py extension and directory path if present + char module_name[256]; + const char *filename = strrchr(plugin->config.path, '/'); + if (filename) { + filename++; // Skip the '/' + } else { + filename = plugin->config.path; + } + + // Copy filename without .py extension + strncpy(module_name, filename, sizeof(module_name) - 1); + module_name[sizeof(module_name) - 1] = '\0'; + char *dot = strrchr(module_name, '.'); + if (dot && strcmp(dot, ".py") == 0) { + *dot = '\0'; + } + + // Add plugin directory to Python path + char python_path_cmd[512]; + const char *plugin_dir = strrchr(plugin->config.path, '/'); + if (plugin_dir) { + int dir_len = plugin_dir - plugin->config.path; + char dir_path[256]; + strncpy(dir_path, plugin->config.path, dir_len); + dir_path[dir_len] = '\0'; + snprintf(python_path_cmd, sizeof(python_path_cmd), + "import sys; sys.path.insert(0, '%s')", dir_path); + } else { + snprintf(python_path_cmd, sizeof(python_path_cmd), + "import sys; sys.path.insert(0, '.')"); + } + + PyRun_SimpleString("import sys"); + PyRun_SimpleString(python_path_cmd); + + // Load the Python module + py_binds->pModule = PyImport_ImportModule(module_name); + if (!py_binds->pModule) { + fprintf(stderr, "Failed to load Python module '%s' from path '%s'\n", + module_name, plugin->config.path); + PyErr_Print(); + free(py_binds); + return -1; + } + + // Get function references based on python_binds_t structure + py_binds->pFuncInit = PyObject_GetAttrString(py_binds->pModule, "init"); + if (!py_binds->pFuncInit || !PyCallable_Check(py_binds->pFuncInit)) { + fprintf(stderr, "Error: 'init' function not found or not callable in module '%s' - this function is required\n", module_name); + Py_XDECREF(py_binds->pModule); + free(py_binds); + return -1; + } + + py_binds->pFuncStartLoop = PyObject_GetAttrString(py_binds->pModule, "start_loop"); + if (!py_binds->pFuncStartLoop || !PyCallable_Check(py_binds->pFuncStartLoop)) { + // start_loop is optional + Py_XDECREF(py_binds->pFuncStartLoop); + py_binds->pFuncStartLoop = NULL; + } + + py_binds->pFuncStopLoop = PyObject_GetAttrString(py_binds->pModule, "stop_loop"); + if (!py_binds->pFuncStopLoop || !PyCallable_Check(py_binds->pFuncStopLoop)) { + // stop_loop is optional + Py_XDECREF(py_binds->pFuncStopLoop); + py_binds->pFuncStopLoop = NULL; + } + + py_binds->pFuncCycleRun = PyObject_GetAttrString(py_binds->pModule, "run_cycle"); + if (!py_binds->pFuncCycleRun || !PyCallable_Check(py_binds->pFuncCycleRun)) { + // run_cycle is optional + Py_XDECREF(py_binds->pFuncCycleRun); + py_binds->pFuncCycleRun = NULL; + } + + py_binds->pFuncCleanup = PyObject_GetAttrString(py_binds->pModule, "cleanup"); + if (!py_binds->pFuncCleanup || !PyCallable_Check(py_binds->pFuncCleanup)) { + // cleanup is optional + Py_XDECREF(py_binds->pFuncCleanup); + py_binds->pFuncCleanup = NULL; + } + + // Store the python binds in the plugin instance + plugin->python_plugin = py_binds; + + printf("Python plugin '%s' symbols loaded successfully\n", module_name); + printf(" - init: %s\n", py_binds->pFuncInit ? "✓" : "✗"); + printf(" - start_loop: %s\n", py_binds->pFuncStartLoop ? "✓" : "✗"); + printf(" - stop_loop: %s\n", py_binds->pFuncStopLoop ? "✓" : "✗"); + printf(" - run_cycle: %s\n", py_binds->pFuncCycleRun ? "✓" : "✗"); + printf(" - cleanup: %s\n", py_binds->pFuncCleanup ? "✓" : "✗"); + + return 0; +} + +// Python plugin cycle function +void python_plugin_cycle(plugin_instance_t *plugin) { + (void)plugin; // Suppress unused parameter warning + // In a real implementation, you'd retrieve the python_plugin_t structure + // and call the cycle function +} + +// Cleanup Python plugin +void python_plugin_cleanup(plugin_instance_t *plugin) { + (void)plugin; // Suppress unused parameter warning + // Cleanup Python resources + if (plugin && plugin->python_plugin) { + // Clean up Python objects + Py_XDECREF(plugin->python_plugin->pFuncInit); + Py_XDECREF(plugin->python_plugin->pFuncStartLoop); + Py_XDECREF(plugin->python_plugin->pFuncStopLoop); + Py_XDECREF(plugin->python_plugin->pFuncCycleRun); + Py_XDECREF(plugin->python_plugin->pFuncCleanup); + Py_XDECREF(plugin->python_plugin->pModule); + + free(plugin->python_plugin); + plugin->python_plugin = NULL; + } +} From 158b390de070700c7a95e2fbfe53ea1ea4612534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Fri, 12 Sep 2025 09:57:41 +0200 Subject: [PATCH 04/44] Adjusting python.h include Accordingly with the documentation, python header should be the first called and for security, we have to define PY_SSIZE_T_CLEAN --- core/src/drivers/plugin_driver.c | 49 ++++++++++++++++++++++--- core/src/drivers/plugin_driver.h | 4 +- core/src/drivers/python_plugin_bridge.h | 1 + plugins.conf | 2 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 7ab9e4a4..4a934348 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -1,9 +1,11 @@ +#define PY_SSIZE_T_CLEAN +#include + #include "plugin_driver.h" #include #include #include #include -#include #include "../plc_app/image_tables.h" #include "plugin_config.h" @@ -145,6 +147,39 @@ int plugin_driver_init(plugin_driver_t *driver) { if (!driver) { return -1; } + + // #chamdo a função init de cada plugin aqui + for (int i = 0; i < driver->plugin_count; i++) { + plugin_instance_t *plugin = &driver->plugins[i]; + if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && plugin->python_plugin->pFuncInit) { + // Generate structured args for Python plugin + plugin_runtime_args_t *args = generate_structured_args(PLUGIN_TYPE_PYTHON); + if (!args) { + fprintf(stderr, "Failed to generate runtime args for plugin: %s\n", plugin->config.name); + return -1; + } + // Set the buffer mutex + args->buffer_mutex = &driver->buffer_mutex; + + // Call the Python init function + PyObject *py_args = create_python_runtime_args_capsule(args); + if (!py_args) { + fprintf(stderr, "Failed to create Python capsule for plugin: %s\n", plugin->config.name); + free_structured_args(args); + return -1; + } + + PyObject *result = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, py_args, NULL); + Py_DECREF(py_args); + + if (!result) { + PyErr_Print(); + fprintf(stderr, "Python init function failed for plugin: %s\n", plugin->config.name); + return -1; + } + Py_DECREF(result); + } + } return 0; } @@ -158,10 +193,14 @@ int plugin_driver_start(plugin_driver_t *driver) { for (int i = 0; i < driver->plugin_count; i++) { plugin_instance_t *plugin = &driver->plugins[i]; - - if (pthread_create(&plugin->thread, NULL, plugin_thread_function, plugin) != 0) { // TODO: Here should be called the start_loop function from plugin - fprintf(stderr, "Failed to create thread for plugin: %s\n", plugin->config.name); - return -1; + if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && plugin->python_plugin->pFuncStartLoop) { + PyObject *res = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncStartLoop, NULL); + if (!res) { + PyErr_Print(); + fprintf(stderr, "Python start loop function failed for plugin: %s\n", plugin->config.name); + return -1; + } + Py_DECREF(res); } } diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index 844b01a0..73178adb 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -1,12 +1,14 @@ #ifndef PLUGIN_DRIVER_H #define PLUGIN_DRIVER_H +#define PY_SSIZE_T_CLEAN +#include + #include #include "../lib/iec_types.h" #include "../plc_app/plcapp_manager.h" #include "plugin_config.h" #include "python_plugin_bridge.h" -#include // Maximum number of plugins #define MAX_PLUGINS 16 diff --git a/core/src/drivers/python_plugin_bridge.h b/core/src/drivers/python_plugin_bridge.h index 62a93fe3..d0ebb510 100644 --- a/core/src/drivers/python_plugin_bridge.h +++ b/core/src/drivers/python_plugin_bridge.h @@ -1,6 +1,7 @@ #ifndef __PYTHON_PLUGIN_BRIDGE_H #define __PYTHON_PLUGIN_BRIDGE_H +#define PY_SSIZE_T_CLEAN #include // Forward declaration diff --git a/plugins.conf b/plugins.conf index 36272ec4..7a154e81 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,4 +1,4 @@ # Plugin configuration file # Format: name,path,enabled,type,config_path #python_plugin,../core/src/drivers/modbus_slave.py,1,0,./modbus_slave_config.ini -python_plugin,../core/src/drivers/simple_modbus.py,1,0,./modbus_slave_config.ini +python_plugin,../core/src/drivers/simple_modbus.py,1,0,./modbus_slave_config.ini \ No newline at end of file From 77dbf445a641758ce4050ed1e981791ec33f94f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Fri, 12 Sep 2025 12:57:08 +0200 Subject: [PATCH 05/44] fix init driver's args encapsulation --- core/src/drivers/example_python_plugin.py | 152 ++++++++++++++++++++++ core/src/drivers/plugin_driver.c | 25 ++-- core/src/drivers/plugin_driver.h | 2 +- core/src/plc_app/plc_main.c | 1 + plugins.conf | 4 +- 5 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 core/src/drivers/example_python_plugin.py diff --git a/core/src/drivers/example_python_plugin.py b/core/src/drivers/example_python_plugin.py new file mode 100644 index 00000000..fd39b6e5 --- /dev/null +++ b/core/src/drivers/example_python_plugin.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Example plugin for testing the updated python_plugin_get_symbols function +This demonstrates the expected functions that should be present in a Python plugin +""" + +import time +import ctypes +from ctypes import * + +# Global variable to track initialization +_initialized = False +_runtime_args = None + +# Define the runtime arguments structure matching the C structure +class PluginRuntimeArgs(ctypes.Structure): + """Python ctypes structure matching plugin_runtime_args_t""" + _fields_ = [ + # Buffer arrays (using POINTER type for arrays) + ("bool_input", POINTER(POINTER(ctypes.c_bool * 8))), # bool_input[BUFFER_SIZE][8] + ("bool_output", POINTER(POINTER(ctypes.c_bool * 8))), # bool_output[BUFFER_SIZE][8] + ("byte_input", POINTER(POINTER(ctypes.c_ubyte))), # byte_input[BUFFER_SIZE] + ("byte_output", POINTER(POINTER(ctypes.c_ubyte))), # byte_output[BUFFER_SIZE] + ("int_input", POINTER(POINTER(ctypes.c_uint16))), # int_input[BUFFER_SIZE] + ("int_output", POINTER(POINTER(ctypes.c_uint16))), # int_output[BUFFER_SIZE] + ("dint_input", POINTER(POINTER(ctypes.c_uint32))), # dint_input[BUFFER_SIZE] + ("dint_output", POINTER(POINTER(ctypes.c_uint32))), # dint_output[BUFFER_SIZE] + ("lint_input", POINTER(POINTER(ctypes.c_uint64))), # lint_input[BUFFER_SIZE] + ("lint_output", POINTER(POINTER(ctypes.c_uint64))), # lint_output[BUFFER_SIZE] + ("int_memory", POINTER(POINTER(ctypes.c_uint16))), # int_memory[BUFFER_SIZE] + ("dint_memory", POINTER(POINTER(ctypes.c_uint32))), # dint_memory[BUFFER_SIZE] + ("lint_memory", POINTER(POINTER(ctypes.c_uint64))), # lint_memory[BUFFER_SIZE] + + # Mutex function pointers + ("mutex_take", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_take)(pthread_mutex_t*) + ("mutex_give", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_give)(pthread_mutex_t*) + ("buffer_mutex", c_void_p), # pthread_mutex_t *buffer_mutex + + # Buffer size information + ("buffer_size", c_int), # int buffer_size + ("bits_per_buffer", c_int), # int bits_per_buffer + ] + +def extract_runtime_args_from_capsule(capsule): + """Extract runtime arguments from PyCapsule""" + if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': + raise TypeError("Expected PyCapsule object") + + # Get the pointer from the capsule + ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") + if not ptr: + raise ValueError("Failed to extract pointer from capsule") + + # Cast the pointer to our structure type + args_ptr = ctypes.cast(ptr, POINTER(PluginRuntimeArgs)) + return args_ptr.contents + +def init(runtime_args_capsule): + """ + Plugin initialization function + Called once when the plugin is loaded + + Args: + runtime_args_capsule: PyCapsule containing plugin_runtime_args_t structure + """ + global _initialized, _runtime_args + + print("Python plugin 'example_plugin' initializing...") + + try: + # Extract runtime args from capsule + # runtime_args = extract_runtime_args_from_capsule(runtime_args_capsule) + # print(f"✓ Runtime arguments extracted successfully") + # print(f" Buffer size: {runtime_args.buffer_size}") + # print(f" Bits per buffer: {runtime_args.bits_per_buffer}") + + # # Store runtime args for later use + # _runtime_args = runtime_args + # _initialized = True + + print("✓ Plugin initialized successfully") + return True + + except Exception as e: + print(f"✗ Plugin initialization failed: {e}") + return False + +def start_loop(): + """ + Called when the plugin loop should start + Optional function - not all plugins need this + """ + print("Plugin start_loop called") + pass + +def stop_loop(): + """ + Called when the plugin loop should stop + Optional function - not all plugins need this + """ + print("Plugin stop_loop called") + pass + +def run_cycle(): + """ + Main plugin cycle function + Called periodically by the plugin system + Optional function - some plugins may only need init + """ + global _initialized, _runtime_args + + if not _initialized or not _runtime_args: + return + + # Example: Toggle a digital output every cycle + try: + if _runtime_args.mutex_take(_runtime_args.buffer_mutex) == 0: + # Toggle bool_output[0][0] + current_value = _runtime_args.bool_output[0][0] + _runtime_args.bool_output[0][0] = not current_value + print(f"Toggled output 0.0 to {not current_value}") + except Exception as e: + print(f"Error in run_cycle: {e}") + finally: + if _runtime_args.buffer_mutex: + _runtime_args.mutex_give(_runtime_args.buffer_mutex) + +def cleanup(): + """ + Plugin cleanup function + Called when the plugin is being unloaded + Optional function - use for cleanup tasks + """ + global _initialized, _runtime_args + + print("Plugin cleanup called") + + _initialized = False + _runtime_args = None + + print("✓ Plugin cleaned up successfully") + +if __name__ == "__main__": + print("This is an example Python plugin for OpenPLC Runtime") + print("Expected functions:") + print(" - init(runtime_args_capsule) -> bool") + print(" - start_loop() -> None (optional)") + print(" - stop_loop() -> None (optional)") + print(" - run_cycle() -> None (optional)") + print(" - cleanup() -> None (optional)") + print() + print("This file should be loaded by the OpenPLC plugin system") diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 4a934348..a34f87e9 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -153,24 +153,14 @@ int plugin_driver_init(plugin_driver_t *driver) { plugin_instance_t *plugin = &driver->plugins[i]; if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && plugin->python_plugin->pFuncInit) { // Generate structured args for Python plugin - plugin_runtime_args_t *args = generate_structured_args(PLUGIN_TYPE_PYTHON); + PyObject *args = generate_structured_args_with_driver(PLUGIN_TYPE_PYTHON, driver); if (!args) { fprintf(stderr, "Failed to generate runtime args for plugin: %s\n", plugin->config.name); return -1; } - // Set the buffer mutex - args->buffer_mutex = &driver->buffer_mutex; - - // Call the Python init function - PyObject *py_args = create_python_runtime_args_capsule(args); - if (!py_args) { - fprintf(stderr, "Failed to create Python capsule for plugin: %s\n", plugin->config.name); - free_structured_args(args); - return -1; - } - - PyObject *result = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, py_args, NULL); - Py_DECREF(py_args); + // Call the Python init function with proper capsule + PyObject *result = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, args, NULL); + Py_DECREF(args); if (!result) { PyErr_Print(); @@ -250,12 +240,13 @@ void plugin_driver_destroy(plugin_driver_t *driver) { * mutex functions, and metadata needed by external plugins. * * @param type Type of plugin (PLUGIN_TYPE_PYTHON or PLUGIN_TYPE_NATIVE) + * @param driver Pointer to plugin driver (for buffer mutex) * @return Pointer to allocated structure/capsule, or NULL on error * * For PLUGIN_TYPE_NATIVE: Returns plugin_runtime_args_t* * For PLUGIN_TYPE_PYTHON: Returns PyObject* (PyCapsule containing plugin_runtime_args_t*) */ -void* generate_structured_args(plugin_type_t type) { +void* generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver) { plugin_runtime_args_t *args = malloc(sizeof(plugin_runtime_args_t)); if (!args) { return NULL; @@ -279,8 +270,8 @@ void* generate_structured_args(plugin_type_t type) { // Initialize mutex functions args->mutex_take = plugin_mutex_take; args->mutex_give = plugin_mutex_give; - // Note: buffer_mutex should be passed as parameter in future versions - args->buffer_mutex = NULL; // Will be set by caller + // Set buffer mutex from driver + args->buffer_mutex = driver ? &driver->buffer_mutex : NULL; // Initialize buffer size info args->buffer_size = BUFFER_SIZE; diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index 73178adb..a04999ec 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -87,7 +87,7 @@ void plugin_driver_destroy(plugin_driver_t *driver); int python_plugin_get_symbols(plugin_instance_t *plugin); // Runtime arguments generation -void* generate_structured_args(plugin_type_t type); +void* generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver); void free_structured_args(plugin_runtime_args_t *args); #endif // PLUGIN_DRIVER_H diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index 49815486..140f4ab4 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -162,6 +162,7 @@ int main() if (plugin_driver_load_config(plugin_driver, "../plugins.conf") == 0) { // Start plugins plugin_driver_init(plugin_driver); + plugin_driver_start(plugin_driver); log_info("[PLUGIN]: Plugin driver system initialized"); } else { log_error("[PLUGIN]: Failed to load plugin configuration"); diff --git a/plugins.conf b/plugins.conf index 7a154e81..d84d0809 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,4 +1,6 @@ # Plugin configuration file # Format: name,path,enabled,type,config_path #python_plugin,../core/src/drivers/modbus_slave.py,1,0,./modbus_slave_config.ini -python_plugin,../core/src/drivers/simple_modbus.py,1,0,./modbus_slave_config.ini \ No newline at end of file +#python_plugin,../core/src/drivers/simple_modbus.py,1,0,./modbus_slave_config.ini +#python_plugin,../core/src/drivers/simple_modbus_sync.py,1,0,./modbus_slave_config.ini +python_plugin,../core/src/drivers/example_python_plugin.py,1,0,./modbus_slave_config.ini \ No newline at end of file From b10e7b85d49a41e9e01c21f29d706fc3ba49ff83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Fri, 12 Sep 2025 13:13:51 +0200 Subject: [PATCH 06/44] adjusting brackets position and function identation Everything accordingly BARR Standard --- .clang-format | 8 + core/src/drivers/plugin_config.c | 48 +++-- core/src/drivers/plugin_config.h | 3 +- core/src/drivers/plugin_driver.c | 359 +++++++++++++++++++------------ core/src/drivers/plugin_driver.h | 29 +-- core/src/plc_app/plc_main.c | 62 +++--- 6 files changed, 304 insertions(+), 205 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..4573264d --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +UseTab: Never +ColumnLimit: 100 +BreakBeforeBraces: Allman # (ou Attach, Linux, etc.) +AllowShortFunctionsOnASingleLine: Empty +SpaceBeforeParens: ControlStatements +AlignConsecutiveAssignments: true \ No newline at end of file diff --git a/core/src/drivers/plugin_config.c b/core/src/drivers/plugin_config.c index 396d76a9..74dfd3b3 100644 --- a/core/src/drivers/plugin_config.c +++ b/core/src/drivers/plugin_config.c @@ -3,51 +3,61 @@ #include #include -int parse_plugin_config(const char *config_file, plugin_config_t *configs, int max_configs) { +int parse_plugin_config(const char *config_file, plugin_config_t *configs, int max_configs) +{ FILE *file = fopen(config_file, "r"); - if (!file) { + if (!file) + { return -1; } - + char line[512]; int config_count = 0; - - while (fgets(line, sizeof(line), file) && config_count < max_configs) { + + while (fgets(line, sizeof(line), file) && config_count < max_configs) + { // Skip comments and empty lines - if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') { + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') + { continue; } - + // Parse plugin configuration: name,path,enabled,type,plugin_related_config_path // Parsing name char *token = strtok(line, ","); - if (!token) continue; + if (!token) + continue; strncpy(configs[config_count].name, token, sizeof(configs[config_count].name) - 1); - + // Parsing path token = strtok(NULL, ","); - if (!token) continue; + if (!token) + continue; strncpy(configs[config_count].path, token, sizeof(configs[config_count].path) - 1); - + // Parsing enabled token = strtok(NULL, ","); - if (!token) continue; + if (!token) + continue; configs[config_count].enabled = atoi(token); - + // Parsing type token = strtok(NULL, ","); - if (!token) continue; + if (!token) + continue; configs[config_count].type = atoi(token); - + // parsing plugin_related_config_path token = strtok(NULL, ","); - if (!token) continue; - strncpy(configs[config_count].plugin_related_config_path, token, sizeof(configs[config_count].plugin_related_config_path) - 1); - + if (!token) + continue; + strncpy(configs[config_count].plugin_related_config_path, token, + sizeof(configs[config_count].plugin_related_config_path) - 1); + // Incrementing index to target next config config_count++; } - + fclose(file); return config_count; } diff --git a/core/src/drivers/plugin_config.h b/core/src/drivers/plugin_config.h index e0e58030..ab287f7f 100644 --- a/core/src/drivers/plugin_config.h +++ b/core/src/drivers/plugin_config.h @@ -4,7 +4,8 @@ #define MAX_PLUGIN_NAME_LEN 64 #define MAX_PLUGIN_PATH_LEN 256 -typedef struct { +typedef struct +{ char name[MAX_PLUGIN_NAME_LEN]; char path[MAX_PLUGIN_PATH_LEN]; int enabled; diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index a34f87e9..dfb4e332 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -1,13 +1,13 @@ -#define PY_SSIZE_T_CLEAN +#define PY_SSIZE_T_CLEAN #include +#include "../plc_app/image_tables.h" +#include "plugin_config.h" #include "plugin_driver.h" #include #include #include #include -#include "../plc_app/image_tables.h" -#include "plugin_config.h" // External buffer declarations from image_tables.c extern IEC_BOOL *bool_input[BUFFER_SIZE][8]; @@ -25,15 +25,17 @@ extern IEC_UDINT *dint_memory[BUFFER_SIZE]; extern IEC_ULINT *lint_memory[BUFFER_SIZE]; // Plugin thread function -void* plugin_thread_function(void *arg) { +void *plugin_thread_function(void *arg) +{ plugin_instance_t *plugin = (plugin_instance_t *)arg; - + // Load the plugin - if (!plugin_manager_load(plugin->manager)) { + if (!plugin_manager_load(plugin->manager)) + { fprintf(stderr, "Failed to load plugin: %s\n", plugin->config.path); return NULL; } - + // TODO: Here you would generate structured args and call plugin init // Example: // plugin_runtime_args_t *args = generate_structured_args(driver, plugin->config.type); @@ -46,93 +48,114 @@ void* plugin_thread_function(void *arg) { // } // free_structured_args(args); // } - + plugin->running = 1; - + // Main plugin loop - while (plugin->running) { + while (plugin->running) + { // Here plugins can do their work // They can call the buffer access functions usleep(10000); // 10ms sleep to prevent busy waiting } - + return NULL; } // Driver management functions -plugin_driver_t* plugin_driver_create(void) { +plugin_driver_t *plugin_driver_create(void) +{ plugin_driver_t *driver = calloc(1, sizeof(plugin_driver_t)); - if (!driver) { + if (!driver) + { return NULL; } - + // Initialize mutex - if (pthread_mutex_init(&driver->buffer_mutex, NULL) != 0) { + if (pthread_mutex_init(&driver->buffer_mutex, NULL) != 0) + { free(driver); return NULL; } - + return driver; } // Mutex helper functions for plugins -static int plugin_mutex_take(pthread_mutex_t *mutex) { +static int plugin_mutex_take(pthread_mutex_t *mutex) +{ return pthread_mutex_lock(mutex); } -static int plugin_mutex_give(pthread_mutex_t *mutex) { +static int plugin_mutex_give(pthread_mutex_t *mutex) +{ return pthread_mutex_unlock(mutex); } // Python capsule destructor for runtime args -static void plugin_runtime_args_capsule_destructor(PyObject *capsule) { - plugin_runtime_args_t *args = (plugin_runtime_args_t *)PyCapsule_GetPointer(capsule, "openplc_runtime_args"); - if (args) { +static void plugin_runtime_args_capsule_destructor(PyObject *capsule) +{ + plugin_runtime_args_t *args = + (plugin_runtime_args_t *)PyCapsule_GetPointer(capsule, "openplc_runtime_args"); + if (args) + { free_structured_args(args); } } // Create Python capsule with runtime arguments -static PyObject* create_python_runtime_args_capsule(plugin_runtime_args_t *args) { - if (!args) { +static PyObject *create_python_runtime_args_capsule(plugin_runtime_args_t *args) +{ + if (!args) + { return NULL; } - + // Create a capsule containing the runtime args pointer - PyObject *capsule = PyCapsule_New(args, "openplc_runtime_args", plugin_runtime_args_capsule_destructor); - if (!capsule) { + PyObject *capsule = + PyCapsule_New(args, "openplc_runtime_args", plugin_runtime_args_capsule_destructor); + if (!capsule) + { // If capsule creation fails, we need to free the args manually free_structured_args(args); return NULL; } - + return capsule; } -int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file) { - if (!driver || !config_file) { +int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file) +{ + if (!driver || !config_file) + { return -1; } - + plugin_config_t configs[MAX_PLUGINS]; int config_count = parse_plugin_config(config_file, configs, MAX_PLUGINS); - if (config_count < 0) { + if (config_count < 0) + { return -1; } driver->plugin_count = config_count; - for (int w = 0; w < config_count; w++) { + for (int w = 0; w < config_count; w++) + { memcpy(&driver->plugins[w].config, &configs[w], sizeof(plugin_config_t)); } - // Agora leio todos os simbolos que preciso (init, start, stop, cycle, cleanup) e adiciono na struct plugin_instance_t - // para cada plugin. - for (int i = 0; i < driver->plugin_count; i++) { + // Agora leio todos os simbolos que preciso (init, start, stop, cycle, cleanup) e adiciono na + // struct plugin_instance_t para cada plugin. + for (int i = 0; i < driver->plugin_count; i++) + { plugin_instance_t *plugin = &driver->plugins[i]; - - if (plugin->config.type == PLUGIN_TYPE_PYTHON) { - if (python_plugin_get_symbols(plugin) != 0) { - fprintf(stderr, "Failed to get Python plugin symbols for: %s\n", plugin->config.path); + + if (plugin->config.type == PLUGIN_TYPE_PYTHON) + { + if (python_plugin_get_symbols(plugin) != 0) + { + fprintf(stderr, "Failed to get Python plugin symbols for: %s\n", + plugin->config.path); plugin_manager_destroy(plugin->manager); return -1; } @@ -143,89 +166,114 @@ int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file) } // Send to plugin init function all args -int plugin_driver_init(plugin_driver_t *driver) { - if (!driver) { +int plugin_driver_init(plugin_driver_t *driver) +{ + if (!driver) + { return -1; } // #chamdo a função init de cada plugin aqui - for (int i = 0; i < driver->plugin_count; i++) { + for (int i = 0; i < driver->plugin_count; i++) + { plugin_instance_t *plugin = &driver->plugins[i]; - if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && plugin->python_plugin->pFuncInit) { + if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && + plugin->python_plugin->pFuncInit) + { // Generate structured args for Python plugin PyObject *args = generate_structured_args_with_driver(PLUGIN_TYPE_PYTHON, driver); - if (!args) { - fprintf(stderr, "Failed to generate runtime args for plugin: %s\n", plugin->config.name); + if (!args) + { + fprintf(stderr, "Failed to generate runtime args for plugin: %s\n", + plugin->config.name); return -1; } // Call the Python init function with proper capsule - PyObject *result = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, args, NULL); + PyObject *result = + PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, args, NULL); Py_DECREF(args); - if (!result) { + if (!result) + { PyErr_Print(); - fprintf(stderr, "Python init function failed for plugin: %s\n", plugin->config.name); + fprintf(stderr, "Python init function failed for plugin: %s\n", + plugin->config.name); return -1; } Py_DECREF(result); } } - + return 0; } - // Call the thread function for each plugin -int plugin_driver_start(plugin_driver_t *driver) { - if (!driver) { +int plugin_driver_start(plugin_driver_t *driver) +{ + if (!driver) + { return -1; } - - for (int i = 0; i < driver->plugin_count; i++) { + + for (int i = 0; i < driver->plugin_count; i++) + { plugin_instance_t *plugin = &driver->plugins[i]; - if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && plugin->python_plugin->pFuncStartLoop) { - PyObject *res = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncStartLoop, NULL); - if (!res) { + if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && + plugin->python_plugin->pFuncStartLoop) + { + PyObject *res = + PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncStartLoop, NULL); + if (!res) + { PyErr_Print(); - fprintf(stderr, "Python start loop function failed for plugin: %s\n", plugin->config.name); + fprintf(stderr, "Python start loop function failed for plugin: %s\n", + plugin->config.name); return -1; } Py_DECREF(res); } } - + return 0; } -int plugin_driver_stop(plugin_driver_t *driver) { - if (!driver) { +int plugin_driver_stop(plugin_driver_t *driver) +{ + if (!driver) + { return -1; } - + // Signal all plugins to stop - for (int i = 0; i < driver->plugin_count; i++) { + for (int i = 0; i < driver->plugin_count; i++) + { driver->plugins[i].running = 0; } - + // Wait for all threads to finish - for (int i = 0; i < driver->plugin_count; i++) { - if (driver->plugins[i].thread) { + for (int i = 0; i < driver->plugin_count; i++) + { + if (driver->plugins[i].thread) + { pthread_join(driver->plugins[i].thread, NULL); } - if (driver->plugins[i].manager) { + if (driver->plugins[i].manager) + { plugin_manager_destroy(driver->plugins[i].manager); driver->plugins[i].manager = NULL; } } - + return 0; } -void plugin_driver_destroy(plugin_driver_t *driver) { - if (!driver) { +void plugin_driver_destroy(plugin_driver_t *driver) +{ + if (!driver) + { return; } - + plugin_driver_stop(driver); pthread_mutex_destroy(&driver->buffer_mutex); free(driver); @@ -235,35 +283,37 @@ void plugin_driver_destroy(plugin_driver_t *driver) { /** * @brief Generate structured arguments for plugin initialization - * + * * This function creates a structured argument containing all runtime buffers, * mutex functions, and metadata needed by external plugins. - * + * * @param type Type of plugin (PLUGIN_TYPE_PYTHON or PLUGIN_TYPE_NATIVE) * @param driver Pointer to plugin driver (for buffer mutex) * @return Pointer to allocated structure/capsule, or NULL on error - * + * * For PLUGIN_TYPE_NATIVE: Returns plugin_runtime_args_t* * For PLUGIN_TYPE_PYTHON: Returns PyObject* (PyCapsule containing plugin_runtime_args_t*) */ -void* generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver) { +void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver) +{ plugin_runtime_args_t *args = malloc(sizeof(plugin_runtime_args_t)); - if (!args) { + if (!args) + { return NULL; } // Initialize all buffer pointers - args->bool_input = bool_input; + args->bool_input = bool_input; args->bool_output = bool_output; - args->byte_input = byte_input; + args->byte_input = byte_input; args->byte_output = byte_output; - args->int_input = int_input; - args->int_output = int_output; - args->dint_input = dint_input; + args->int_input = int_input; + args->int_output = int_output; + args->dint_input = dint_input; args->dint_output = dint_output; - args->lint_input = lint_input; + args->lint_input = lint_input; args->lint_output = lint_output; - args->int_memory = int_memory; + args->int_memory = int_memory; args->dint_memory = dint_memory; args->lint_memory = lint_memory; @@ -272,160 +322,185 @@ void* generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t * args->mutex_give = plugin_mutex_give; // Set buffer mutex from driver args->buffer_mutex = driver ? &driver->buffer_mutex : NULL; - + // Initialize buffer size info - args->buffer_size = BUFFER_SIZE; + args->buffer_size = BUFFER_SIZE; args->bits_per_buffer = 8; - switch (type) { - case PLUGIN_TYPE_NATIVE: - // For native plugins, return the structure directly - return args; - - case PLUGIN_TYPE_PYTHON: - // For Python plugins, wrap in a PyCapsule - return create_python_runtime_args_capsule(args); - - default: - // Unknown type, clean up and return NULL - free(args); - return NULL; + switch (type) + { + case PLUGIN_TYPE_NATIVE: + // For native plugins, return the structure directly + return args; + + case PLUGIN_TYPE_PYTHON: + // For Python plugins, wrap in a PyCapsule + return create_python_runtime_args_capsule(args); + + default: + // Unknown type, clean up and return NULL + free(args); + return NULL; } } // Free structured arguments -void free_structured_args(plugin_runtime_args_t *args) { - if (args) { +void free_structured_args(plugin_runtime_args_t *args) +{ + if (args) + { // No dynamic allocations inside the structure to free // Just free the main structure free(args); } } -int python_plugin_get_symbols(plugin_instance_t *plugin) { - if (!plugin || !plugin->config.path) { +int python_plugin_get_symbols(plugin_instance_t *plugin) +{ + if (!plugin || !plugin->config.path) + { return -1; } - + // Allocate python binds structure python_binds_t *py_binds = calloc(1, sizeof(python_binds_t)); - if (!py_binds) { + if (!py_binds) + { return -1; } - + // Initialize Python if not already initialized - if (!Py_IsInitialized()) { + if (!Py_IsInitialized()) + { Py_Initialize(); } - + // Extract module name from plugin path // Remove .py extension and directory path if present char module_name[256]; const char *filename = strrchr(plugin->config.path, '/'); - if (filename) { + if (filename) + { filename++; // Skip the '/' - } else { + } + else + { filename = plugin->config.path; } - + // Copy filename without .py extension strncpy(module_name, filename, sizeof(module_name) - 1); module_name[sizeof(module_name) - 1] = '\0'; - char *dot = strrchr(module_name, '.'); - if (dot && strcmp(dot, ".py") == 0) { + char *dot = strrchr(module_name, '.'); + if (dot && strcmp(dot, ".py") == 0) + { *dot = '\0'; } - + // Add plugin directory to Python path char python_path_cmd[512]; const char *plugin_dir = strrchr(plugin->config.path, '/'); - if (plugin_dir) { + if (plugin_dir) + { int dir_len = plugin_dir - plugin->config.path; char dir_path[256]; strncpy(dir_path, plugin->config.path, dir_len); dir_path[dir_len] = '\0'; - snprintf(python_path_cmd, sizeof(python_path_cmd), - "import sys; sys.path.insert(0, '%s')", dir_path); - } else { - snprintf(python_path_cmd, sizeof(python_path_cmd), - "import sys; sys.path.insert(0, '.')"); + snprintf(python_path_cmd, sizeof(python_path_cmd), "import sys; sys.path.insert(0, '%s')", + dir_path); + } + else + { + snprintf(python_path_cmd, sizeof(python_path_cmd), "import sys; sys.path.insert(0, '.')"); } - + PyRun_SimpleString("import sys"); PyRun_SimpleString(python_path_cmd); - + // Load the Python module py_binds->pModule = PyImport_ImportModule(module_name); - if (!py_binds->pModule) { - fprintf(stderr, "Failed to load Python module '%s' from path '%s'\n", - module_name, plugin->config.path); + if (!py_binds->pModule) + { + fprintf(stderr, "Failed to load Python module '%s' from path '%s'\n", module_name, + plugin->config.path); PyErr_Print(); free(py_binds); return -1; } - + // Get function references based on python_binds_t structure py_binds->pFuncInit = PyObject_GetAttrString(py_binds->pModule, "init"); - if (!py_binds->pFuncInit || !PyCallable_Check(py_binds->pFuncInit)) { - fprintf(stderr, "Error: 'init' function not found or not callable in module '%s' - this function is required\n", module_name); + if (!py_binds->pFuncInit || !PyCallable_Check(py_binds->pFuncInit)) + { + fprintf(stderr, + "Error: 'init' function not found or not callable in module '%s' - this function " + "is required\n", + module_name); Py_XDECREF(py_binds->pModule); free(py_binds); return -1; } - + py_binds->pFuncStartLoop = PyObject_GetAttrString(py_binds->pModule, "start_loop"); - if (!py_binds->pFuncStartLoop || !PyCallable_Check(py_binds->pFuncStartLoop)) { + if (!py_binds->pFuncStartLoop || !PyCallable_Check(py_binds->pFuncStartLoop)) + { // start_loop is optional Py_XDECREF(py_binds->pFuncStartLoop); py_binds->pFuncStartLoop = NULL; } - + py_binds->pFuncStopLoop = PyObject_GetAttrString(py_binds->pModule, "stop_loop"); - if (!py_binds->pFuncStopLoop || !PyCallable_Check(py_binds->pFuncStopLoop)) { + if (!py_binds->pFuncStopLoop || !PyCallable_Check(py_binds->pFuncStopLoop)) + { // stop_loop is optional Py_XDECREF(py_binds->pFuncStopLoop); py_binds->pFuncStopLoop = NULL; } - + py_binds->pFuncCycleRun = PyObject_GetAttrString(py_binds->pModule, "run_cycle"); - if (!py_binds->pFuncCycleRun || !PyCallable_Check(py_binds->pFuncCycleRun)) { + if (!py_binds->pFuncCycleRun || !PyCallable_Check(py_binds->pFuncCycleRun)) + { // run_cycle is optional Py_XDECREF(py_binds->pFuncCycleRun); py_binds->pFuncCycleRun = NULL; } - + py_binds->pFuncCleanup = PyObject_GetAttrString(py_binds->pModule, "cleanup"); - if (!py_binds->pFuncCleanup || !PyCallable_Check(py_binds->pFuncCleanup)) { + if (!py_binds->pFuncCleanup || !PyCallable_Check(py_binds->pFuncCleanup)) + { // cleanup is optional Py_XDECREF(py_binds->pFuncCleanup); py_binds->pFuncCleanup = NULL; } - + // Store the python binds in the plugin instance plugin->python_plugin = py_binds; - + printf("Python plugin '%s' symbols loaded successfully\n", module_name); printf(" - init: %s\n", py_binds->pFuncInit ? "✓" : "✗"); printf(" - start_loop: %s\n", py_binds->pFuncStartLoop ? "✓" : "✗"); printf(" - stop_loop: %s\n", py_binds->pFuncStopLoop ? "✓" : "✗"); printf(" - run_cycle: %s\n", py_binds->pFuncCycleRun ? "✓" : "✗"); printf(" - cleanup: %s\n", py_binds->pFuncCleanup ? "✓" : "✗"); - + return 0; } // Python plugin cycle function -void python_plugin_cycle(plugin_instance_t *plugin) { +void python_plugin_cycle(plugin_instance_t *plugin) +{ (void)plugin; // Suppress unused parameter warning // In a real implementation, you'd retrieve the python_plugin_t structure // and call the cycle function } // Cleanup Python plugin -void python_plugin_cleanup(plugin_instance_t *plugin) { +void python_plugin_cleanup(plugin_instance_t *plugin) +{ (void)plugin; // Suppress unused parameter warning // Cleanup Python resources - if (plugin && plugin->python_plugin) { + if (plugin && plugin->python_plugin) + { // Clean up Python objects Py_XDECREF(plugin->python_plugin->pFuncInit); Py_XDECREF(plugin->python_plugin->pFuncStartLoop); @@ -433,7 +508,7 @@ void python_plugin_cleanup(plugin_instance_t *plugin) { Py_XDECREF(plugin->python_plugin->pFuncCycleRun); Py_XDECREF(plugin->python_plugin->pFuncCleanup); Py_XDECREF(plugin->python_plugin->pModule); - + free(plugin->python_plugin); plugin->python_plugin = NULL; } diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index a04999ec..966c02cc 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -1,21 +1,22 @@ #ifndef PLUGIN_DRIVER_H #define PLUGIN_DRIVER_H -#define PY_SSIZE_T_CLEAN +#define PY_SSIZE_T_CLEAN #include -#include #include "../lib/iec_types.h" #include "../plc_app/plcapp_manager.h" #include "plugin_config.h" #include "python_plugin_bridge.h" +#include // Maximum number of plugins #define MAX_PLUGINS 16 -typedef enum { +typedef enum +{ PLUGIN_TYPE_PYTHON, - PLUGIN_TYPE_NATIVE + PLUGIN_TYPE_NATIVE } plugin_type_t; typedef int (*plugin_init_func_t)(void *); @@ -24,7 +25,8 @@ typedef void (*plugin_stop_loop_func_t)(); typedef void (*plugin_run_cycle_func_t)(); typedef void (*plugin_cleanup_func_t)(); -typedef struct { +typedef struct +{ plugin_init_func_t init; plugin_start_loop_func_t start; plugin_stop_loop_func_t stop; @@ -33,7 +35,8 @@ typedef struct { } plugin_funct_bundle_t; // Runtime buffer access structure for plugins -typedef struct { +typedef struct +{ // Buffer pointers IEC_BOOL *(*bool_input)[8]; IEC_BOOL *(*bool_output)[8]; @@ -48,19 +51,20 @@ typedef struct { IEC_UINT **int_memory; IEC_UDINT **dint_memory; IEC_ULINT **lint_memory; - + // Mutex functions int (*mutex_take)(pthread_mutex_t *mutex); int (*mutex_give)(pthread_mutex_t *mutex); pthread_mutex_t *buffer_mutex; - + // Buffer size information int buffer_size; int bits_per_buffer; } plugin_runtime_args_t; // Plugin instance structure -typedef struct plugin_instance_s { +typedef struct plugin_instance_s +{ PluginManager *manager; python_binds_t *python_plugin; pthread_t thread; @@ -69,14 +73,15 @@ typedef struct plugin_instance_s { } plugin_instance_t; // Driver structure -typedef struct { +typedef struct +{ plugin_instance_t plugins[MAX_PLUGINS]; int plugin_count; pthread_mutex_t buffer_mutex; } plugin_driver_t; // Driver management functions -plugin_driver_t* plugin_driver_create(void); +plugin_driver_t *plugin_driver_create(void); int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file); int plugin_driver_init(plugin_driver_t *driver); int plugin_driver_start(plugin_driver_t *driver); @@ -87,7 +92,7 @@ void plugin_driver_destroy(plugin_driver_t *driver); int python_plugin_get_symbols(plugin_instance_t *plugin); // Runtime arguments generation -void* generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver); +void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver); void free_structured_args(plugin_runtime_args_t *args); #endif // PLUGIN_DRIVER_H diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index 140f4ab4..57102ce5 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -9,13 +9,13 @@ #include #include +#include "../drivers/plugin_driver.h" #include "image_tables.h" -#include "utils/log.h" #include "plcapp_manager.h" +#include "scan_cycle_manager.h" +#include "utils/log.h" #include "utils/utils.h" #include "utils/watchdog.h" -#include "scan_cycle_manager.h" -#include "../drivers/plugin_driver.h" extern atomic_long plc_heartbeat; extern PLCState plc_state; @@ -25,39 +25,35 @@ struct timespec timer_start; pthread_t plc_thread; PluginManager *plc_program = NULL; - -void handle_sigint(int sig) +void handle_sigint(int sig) { (void)sig; keep_running = 0; } -void *print_stats_thread(void *arg) +void *print_stats_thread(void *arg) { (void)arg; - while (keep_running) + while (keep_running) { - if (bool_output[0][0]) + if (bool_output[0][0]) { log_debug("bool_output[0][0]: %d", *bool_output[0][0]); - } - else + } + else { log_debug("bool_output[0][0] is NULL"); } log_info("Scan Count: %lu", plc_timing_stats.scan_count); log_info("Scan Time - Min: %ld us, Max: %ld us, Avg: %ld us", - plc_timing_stats.scan_time_min, - plc_timing_stats.scan_time_max, + plc_timing_stats.scan_time_min, plc_timing_stats.scan_time_max, plc_timing_stats.scan_time_avg); log_info("Cycle Time - Min: %lu us, Max: %lu us, Avg: %ld us", - plc_timing_stats.cycle_time_min, - plc_timing_stats.cycle_time_max, + plc_timing_stats.cycle_time_min, plc_timing_stats.cycle_time_max, plc_timing_stats.cycle_time_avg); log_info("Cycle Latency - Min: %ld us, Max: %ld us, Avg: %ld us", - plc_timing_stats.cycle_latency_min, - plc_timing_stats.cycle_latency_max, + plc_timing_stats.cycle_latency_min, plc_timing_stats.cycle_latency_max, plc_timing_stats.cycle_latency_avg); log_info("Overruns: %lu", plc_timing_stats.overruns); @@ -67,7 +63,7 @@ void *print_stats_thread(void *arg) return NULL; } -void *plc_cycle_thread(void *arg) +void *plc_cycle_thread(void *arg) { PluginManager *pm = (PluginManager *)arg; @@ -112,13 +108,13 @@ void *plc_cycle_thread(void *arg) int load_plc_program(PluginManager *pm) { - if (plugin_manager_load(pm)) + if (plugin_manager_load(pm)) { log_info("Loading PLC application"); plc_state = PLC_STATE_INIT; log_info("PLC State: INIT"); - if (pthread_create(&plc_thread, NULL, plc_cycle_thread, pm) != 0) + if (pthread_create(&plc_thread, NULL, plc_cycle_thread, pm) != 0) { log_error("Failed to create PLC cycle thread"); plc_state = PLC_STATE_ERROR; @@ -126,8 +122,8 @@ int load_plc_program(PluginManager *pm) return -1; } return 0; - } - else + } + else { log_error("Failed to load PLC application"); plc_state = PLC_STATE_ERROR; @@ -136,8 +132,7 @@ int load_plc_program(PluginManager *pm) } } - -int main() +int main() { log_set_level(LOG_LEVEL_DEBUG); @@ -157,14 +152,18 @@ int main() // Initialize plugin driver system plugin_driver_t *plugin_driver = plugin_driver_create(); - if (plugin_driver) { + if (plugin_driver) + { // Load plugin configuration - if (plugin_driver_load_config(plugin_driver, "../plugins.conf") == 0) { + if (plugin_driver_load_config(plugin_driver, "../plugins.conf") == 0) + { // Start plugins plugin_driver_init(plugin_driver); plugin_driver_start(plugin_driver); log_info("[PLUGIN]: Plugin driver system initialized"); - } else { + } + else + { log_error("[PLUGIN]: Failed to load plugin configuration"); } } @@ -175,13 +174,13 @@ int main() // Launch status printing thread pthread_t stats_thread; - if (pthread_create(&stats_thread, NULL, print_stats_thread, NULL) != 0) + if (pthread_create(&stats_thread, NULL, print_stats_thread, NULL) != 0) { log_error("Failed to create stats thread"); return -1; } - while (keep_running) + while (keep_running) { // Handle UNIX socket here in the future sleep(1); @@ -194,12 +193,13 @@ int main() pthread_join(stats_thread, NULL); pthread_join(plc_thread, NULL); plugin_manager_destroy(plc_program); - + // Cleanup plugin driver system - if (plugin_driver) { + if (plugin_driver) + { plugin_driver_stop(plugin_driver); plugin_driver_destroy(plugin_driver); } - + return 0; } From 75dfe2f893a71a03072b80fb9aa0e19839e88ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Fri, 12 Sep 2025 14:28:01 +0200 Subject: [PATCH 07/44] fix cmakelist --- core/src/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/CMakeLists.txt b/core/src/CMakeLists.txt index 1692a155..3a54ce12 100644 --- a/core/src/CMakeLists.txt +++ b/core/src/CMakeLists.txt @@ -19,8 +19,6 @@ add_compile_options(-Wall -Werror -Wextra -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -Wformat -Werror=format-security -fPIC -fPIE) -# Add drivers subdirectory -add_subdirectory(drivers) # Step 3: Build the executable and link against the shared library add_executable(plc_main From e89ff10b3e7dfb2b214c23008f8346e6940671c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Mon, 15 Sep 2025 10:03:54 +0200 Subject: [PATCH 08/44] adjust python_plugin_bridge.h identation everything accordingly BARR standard --- core/src/drivers/python_plugin_bridge.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/drivers/python_plugin_bridge.h b/core/src/drivers/python_plugin_bridge.h index d0ebb510..6b01ca8b 100644 --- a/core/src/drivers/python_plugin_bridge.h +++ b/core/src/drivers/python_plugin_bridge.h @@ -1,16 +1,17 @@ #ifndef __PYTHON_PLUGIN_BRIDGE_H #define __PYTHON_PLUGIN_BRIDGE_H -#define PY_SSIZE_T_CLEAN +#define PY_SSIZE_T_CLEAN #include // Forward declaration struct plugin_instance_s; // Python plugin bridge structure -typedef struct { +typedef struct +{ PyObject *pModule; - PyObject *pFuncInit; // Driver Init function + PyObject *pFuncInit; // Driver Init function PyObject *pFuncStartLoop; PyObject *pFuncStopLoop; PyObject *pFuncCycleRun; From ccef7493b3f3111de00e778a5baac56ef0e5c392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Mon, 15 Sep 2025 10:09:40 +0200 Subject: [PATCH 09/44] python start funct running within a thread deleting python cycle function since it will be running async --- core/src/drivers/plugin_driver.c | 84 ++++++++++++++----------- core/src/drivers/python_plugin_bridge.h | 1 - 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index dfb4e332..3576a75e 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -29,25 +29,18 @@ void *plugin_thread_function(void *arg) { plugin_instance_t *plugin = (plugin_instance_t *)arg; - // Load the plugin - if (!plugin_manager_load(plugin->manager)) + PyObject *res = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncStartLoop, NULL); + if (!res) { - fprintf(stderr, "Failed to load plugin: %s\n", plugin->config.path); - return NULL; + PyErr_Print(); + fprintf(stderr, "Python start loop function failed for plugin: %s\n", plugin->config.name); + // return -1; } - - // TODO: Here you would generate structured args and call plugin init - // Example: - // plugin_runtime_args_t *args = generate_structured_args(driver, plugin->config.type); - // if (args) { - // // Get init symbol from plugin - // typedef int (*plugin_init_func_t)(plugin_runtime_args_t *args); - // plugin_init_func_t init_func = dlsym(plugin_handle, "init"); - // if (init_func) { - // init_func(args); - // } - // free_structured_args(args); - // } + else + { + printf("[PLUGIN]: Plugin %s started successfully.\n", plugin->config.name); + } + Py_DECREF(res); plugin->running = 1; @@ -202,6 +195,10 @@ int plugin_driver_init(plugin_driver_t *driver) } Py_DECREF(result); } + else if (plugin->config.type == PLUGIN_TYPE_NATIVE && plugin->manager) + { + // TODO: Implement native plugin initialization + } } return 0; @@ -218,19 +215,37 @@ int plugin_driver_start(plugin_driver_t *driver) for (int i = 0; i < driver->plugin_count; i++) { plugin_instance_t *plugin = &driver->plugins[i]; - if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && - plugin->python_plugin->pFuncStartLoop) + switch (plugin->config.type) { - PyObject *res = - PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncStartLoop, NULL); - if (!res) + case PLUGIN_TYPE_PYTHON: + { + // Python plugins run asynchronously in their own threads + if (plugin->python_plugin && plugin->python_plugin->pFuncStartLoop) { - PyErr_Print(); - fprintf(stderr, "Python start loop function failed for plugin: %s\n", + // Create a thread to run the plugin thread function + if (pthread_create(&plugin->thread, NULL, plugin_thread_function, plugin) != 0) + { + fprintf(stderr, "Failed to create thread for plugin: %s\n", + plugin->config.name); + return -1; + } + } + else + { + fprintf(stderr, "Python plugin %s does not have a start_loop function.\n", plugin->config.name); - return -1; } - Py_DECREF(res); + } + break; + + case PLUGIN_TYPE_NATIVE: + { + // TODO: Implement native plugin start logic + } + break; + + default: + break; } } @@ -239,6 +254,7 @@ int plugin_driver_start(plugin_driver_t *driver) int plugin_driver_stop(plugin_driver_t *driver) { + printf("[PLUGIN]: Stopping all plugins...\n"); if (!driver) { return -1; @@ -255,7 +271,11 @@ int plugin_driver_stop(plugin_driver_t *driver) { if (driver->plugins[i].thread) { - pthread_join(driver->plugins[i].thread, NULL); + printf("[PLUGIN]: Plugin %s thread canceling.\n", driver->plugins[i].config.name); + pthread_cancel(driver->plugins[i].thread); + driver->plugins[i].thread = 0; + printf("[PLUGIN]: Plugin %s thread canceled.\n", driver->plugins[i].config.name); + // pthread_join(driver->plugins[i].thread, NULL); } if (driver->plugins[i].manager) { @@ -457,14 +477,6 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) py_binds->pFuncStopLoop = NULL; } - py_binds->pFuncCycleRun = PyObject_GetAttrString(py_binds->pModule, "run_cycle"); - if (!py_binds->pFuncCycleRun || !PyCallable_Check(py_binds->pFuncCycleRun)) - { - // run_cycle is optional - Py_XDECREF(py_binds->pFuncCycleRun); - py_binds->pFuncCycleRun = NULL; - } - py_binds->pFuncCleanup = PyObject_GetAttrString(py_binds->pModule, "cleanup"); if (!py_binds->pFuncCleanup || !PyCallable_Check(py_binds->pFuncCleanup)) { @@ -480,7 +492,6 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) printf(" - init: %s\n", py_binds->pFuncInit ? "✓" : "✗"); printf(" - start_loop: %s\n", py_binds->pFuncStartLoop ? "✓" : "✗"); printf(" - stop_loop: %s\n", py_binds->pFuncStopLoop ? "✓" : "✗"); - printf(" - run_cycle: %s\n", py_binds->pFuncCycleRun ? "✓" : "✗"); printf(" - cleanup: %s\n", py_binds->pFuncCleanup ? "✓" : "✗"); return 0; @@ -505,7 +516,6 @@ void python_plugin_cleanup(plugin_instance_t *plugin) Py_XDECREF(plugin->python_plugin->pFuncInit); Py_XDECREF(plugin->python_plugin->pFuncStartLoop); Py_XDECREF(plugin->python_plugin->pFuncStopLoop); - Py_XDECREF(plugin->python_plugin->pFuncCycleRun); Py_XDECREF(plugin->python_plugin->pFuncCleanup); Py_XDECREF(plugin->python_plugin->pModule); diff --git a/core/src/drivers/python_plugin_bridge.h b/core/src/drivers/python_plugin_bridge.h index 6b01ca8b..76964407 100644 --- a/core/src/drivers/python_plugin_bridge.h +++ b/core/src/drivers/python_plugin_bridge.h @@ -14,7 +14,6 @@ typedef struct PyObject *pFuncInit; // Driver Init function PyObject *pFuncStartLoop; PyObject *pFuncStopLoop; - PyObject *pFuncCycleRun; PyObject *pFuncCleanup; } python_binds_t; From ce90100ca9e8f26660915a1a8e1cdca2098c7ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Mon, 15 Sep 2025 14:53:15 +0200 Subject: [PATCH 10/44] fixing pointer dereferencing for some reason, when init is called it can successfully link buffers and function address between runtime and plugin, but when the same previous parsed "struct" is called within start function, the buffer pointer was no longer pointing to the right address. --- core/src/drivers/README.md | 419 ++++++++++++++++++++++ core/src/drivers/example_python_plugin.py | 129 +++---- core/src/drivers/plugin_driver.c | 50 ++- core/src/drivers/python_plugin_types.py | 318 ++++++++++++++++ 4 files changed, 838 insertions(+), 78 deletions(-) create mode 100644 core/src/drivers/README.md create mode 100644 core/src/drivers/python_plugin_types.py diff --git a/core/src/drivers/README.md b/core/src/drivers/README.md new file mode 100644 index 00000000..a4ab678a --- /dev/null +++ b/core/src/drivers/README.md @@ -0,0 +1,419 @@ +# OpenPLC Runtime Plugin System + +This directory contains the OpenPLC Runtime plugin system, which allows extending the runtime with custom drivers and communication protocols through both native C and Python plugins. + +## Overview + +The plugin system provides a flexible architecture for integrating external hardware drivers, communication protocols, and custom logic into the OpenPLC Runtime. It offers thread-safe access to OpenPLC I/O buffers and supports both native C plugins and Python plugins. + +## Architecture + +### Core Components + +``` +core/src/drivers/ +├── plugin_driver.c/h # Main plugin driver system +├── plugin_config.c/h # Configuration file parsing +├── python_plugin_bridge.c/h # Python plugin integration +├── CMakeLists.txt # Build configuration +├── examples/ # Plugin examples +└── *.py # Python plugin implementations +``` + +### Plugin Types + +1. **Native C Plugins** (`PLUGIN_TYPE_NATIVE`) + - Compiled shared libraries (.so files) + - Direct C function calls + - Maximum performance + +2. **Python Plugins** (`PLUGIN_TYPE_PYTHON`) + - Python scripts (.py files) + - Embedded Python interpreter + - Easier development and debugging + +## Plugin Interface + +### Required Functions + +All plugins must implement these core functions: + +#### Python Plugins +```python +def init(runtime_args_capsule): + """ + Initialize plugin with runtime arguments + Args: + runtime_args_capsule: PyCapsule containing plugin_runtime_args_t + Returns: + bool: True if initialization successful + """ + pass + +# Optional functions +def start_loop(): + """Called when plugin should start operations""" + pass + +def stop_loop(): + """Called when plugin should stop operations""" + pass + +def run_cycle(): + """Called periodically during runtime""" + pass + +def cleanup(): + """Called when plugin is being unloaded""" + pass +``` + +#### Native C Plugins +```c +int init(plugin_runtime_args_t *args); +void start_loop(void); +void stop_loop(void); +void run_cycle(void); +void cleanup(void); +``` + +### Runtime Arguments Structure + +Plugins receive access to OpenPLC buffers through the `plugin_runtime_args_t` structure: + +```c +typedef struct { + // I/O Buffer pointers + IEC_BOOL *(*bool_input)[8]; // Digital inputs + IEC_BOOL *(*bool_output)[8]; // Digital outputs + IEC_BYTE **byte_input; // Byte inputs + IEC_BYTE **byte_output; // Byte outputs + IEC_UINT **int_input; // 16-bit integer inputs + IEC_UINT **int_output; // 16-bit integer outputs + IEC_UDINT **dint_input; // 32-bit integer inputs + IEC_UDINT **dint_output; // 32-bit integer outputs + IEC_ULINT **lint_input; // 64-bit integer inputs + IEC_ULINT **lint_output; // 64-bit integer outputs + IEC_UINT **int_memory; // Internal memory + IEC_UDINT **dint_memory; // Internal memory + IEC_ULINT **lint_memory; // Internal memory + + // Thread synchronization + int (*mutex_take)(pthread_mutex_t *mutex); + int (*mutex_give)(pthread_mutex_t *mutex); + pthread_mutex_t *buffer_mutex; + + // Buffer metadata + int buffer_size; // Number of buffers + int bits_per_buffer; // Bits per boolean buffer +} plugin_runtime_args_t; +``` + +## Thread-Safe Buffer Access + +### Python Example +```python +def safe_read_output(runtime_args, buffer_idx, bit_pos): + """Safely read a boolean output""" + try: + if runtime_args.mutex_take(runtime_args.buffer_mutex) == 0: + value = runtime_args.bool_output[buffer_idx][bit_pos] + return bool(value) + finally: + runtime_args.mutex_give(runtime_args.buffer_mutex) + return False + +def safe_write_output(runtime_args, buffer_idx, bit_pos, value): + """Safely write a boolean output""" + try: + if runtime_args.mutex_take(runtime_args.buffer_mutex) == 0: + runtime_args.bool_output[buffer_idx][bit_pos] = bool(value) + return True + finally: + runtime_args.mutex_give(runtime_args.buffer_mutex) + return False +``` + +## Configuration + +### Plugin Configuration File Format + +``` +# Format: name,path,enabled,type,plugin_related_config_path +example_plugin1,./plugins/example1.so,1,0,./config/example1.conf +python_plugin,./plugins/modbus_slave.py,1,1,./config/modbus.ini +``` + +**Fields:** +- `name`: Plugin identifier +- `path`: Path to plugin file (.so for native, .py for Python) +- `enabled`: 1 = enabled, 0 = disabled +- `type`: 0 = native C, 1 = Python +- `plugin_related_config_path`: Path to plugin-specific configuration + +### Loading Configuration +```c +plugin_driver_t *driver = plugin_driver_create(); +plugin_driver_load_config(driver, "plugins.conf"); +plugin_driver_init(driver); +plugin_driver_start(driver); +``` + +## Examples + +### 1. Simple Python Plugin + +See `example_python_plugin.py` for a basic template that demonstrates: +- Plugin initialization +- Runtime arguments extraction +- Buffer access patterns +- Lifecycle management + +### 2. Modbus TCP Slave + +The `modbus_slave.py` provides a complete implementation of a Modbus TCP slave server: + +**Features:** +- Maps OpenPLC bool_input/bool_output to Modbus coils and discrete inputs +- Maps OpenPLC int_input/int_output to Modbus registers +- Supports standard Modbus function codes (01, 02, 03, 04, 05, 06, 0F, 10) +- Thread-safe buffer access +- Configurable host/port +- Enhanced functionality with pymodbus (optional) + +**Configuration:** +```ini +[plugin_modbus_slave] +type = PLUGIN_TYPE_PYTHON +path = /path/to/modbus_slave.py +enabled = true +host = 172.29.65.104 +port = 5020 +max_coils = 8000 +max_discrete_inputs = 8000 +``` + +**Usage:** +```python +# Initialize and start Modbus slave +if init(runtime_args_capsule): + start_loop() # Starts server on configured port +``` + +### 3. Synchronous Modbus Implementation + +The `simple_modbus_sync.py` provides a lightweight synchronous Modbus TCP server using Python's built-in `socketserver` module. + +## Development Guide + +### Creating a Python Plugin + +1. **Create plugin file:** +```python +#!/usr/bin/env python3 +import ctypes +from ctypes import * + +# Define runtime args structure (copy from examples) +class PluginRuntimeArgs(ctypes.Structure): + # ... (see example files for complete structure) + +def init(runtime_args_capsule): + """Initialize your plugin""" + # Extract runtime args from capsule + runtime_args = extract_runtime_args_from_capsule(runtime_args_capsule) + + # Initialize your plugin logic + print("Plugin initialized") + return True + +def start_loop(): + """Start plugin operations""" + pass + +def cleanup(): + """Cleanup resources""" + pass +``` + +2. **Add to configuration:** +``` +my_plugin,/path/to/my_plugin.py,1,1,/path/to/config.ini +``` + +3. **Test plugin:** +```bash +# Load and test plugin +python3 my_plugin.py +``` + +### Creating a Native C Plugin + +1. **Implement required functions:** +```c +#include "plugin_driver.h" + +int init(plugin_runtime_args_t *args) { + // Initialize plugin + return 0; // Success +} + +void start_loop(void) { + // Start operations +} + +void cleanup(void) { + // Cleanup resources +} +``` + +2. **Compile as shared library:** +```bash +gcc -shared -fPIC -o my_plugin.so my_plugin.c +``` + +3. **Add to configuration:** +``` +my_plugin,./plugins/my_plugin.so,1,0,./config/my_plugin.conf +``` + +## Buffer Mapping + +### Boolean Buffers +- `bool_input[BUFFER_SIZE][8]` - Digital inputs (read-only for plugins) +- `bool_output[BUFFER_SIZE][8]` - Digital outputs (read/write) +- Each buffer contains 8 boolean values +- Total capacity: BUFFER_SIZE × 8 boolean I/O points + +### Integer Buffers +- `int_input/int_output` - 16-bit integers +- `dint_input/dint_output` - 32-bit integers +- `lint_input/lint_output` - 64-bit integers +- `*_memory` - Internal memory buffers + +### Modbus Mapping Example +``` +Modbus Coils (0x01) -> bool_output[0-999][0-7] +Modbus Discrete Inputs (0x02) -> bool_input[0-999][0-7] +Modbus Holding Registers (0x03) -> int_output[0-999] +Modbus Input Registers (0x04) -> int_input[0-999] +``` + +## API Reference + +### Plugin Driver Functions + +```c +// Driver lifecycle +plugin_driver_t *plugin_driver_create(void); +int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file); +int plugin_driver_init(plugin_driver_t *driver); +int plugin_driver_start(plugin_driver_t *driver); +int plugin_driver_stop(plugin_driver_t *driver); +void plugin_driver_destroy(plugin_driver_t *driver); + +// Runtime arguments +void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver); +void free_structured_args(plugin_runtime_args_t *args); + +// Python plugin support +int python_plugin_get_symbols(plugin_instance_t *plugin); +``` + +### Configuration Functions + +```c +// Parse plugin configuration file +int parse_plugin_config(const char *config_file, plugin_config_t *configs, int max_configs); +``` + +## Error Handling + +### Common Issues + +1. **Plugin initialization fails:** + - Check plugin file path and permissions + - Verify Python syntax for Python plugins + - Check runtime arguments extraction + +2. **Buffer access errors:** + - Always use mutex protection + - Check buffer bounds before access + - Handle null pointer cases + +3. **Python import errors:** + - Ensure Python path includes plugin directory + - Check for missing dependencies + - Verify Python interpreter initialization + +### Debugging + +1. **Enable debug output:** +```python +import sys +print(f"Plugin loaded from: {__file__}", file=sys.stderr) +``` + +2. **Check plugin symbols:** +```bash +# For native plugins +nm -D plugin.so | grep init + +# For Python plugins +python3 -c "import plugin; print(dir(plugin))" +``` + +3. **Monitor buffer access:** +```python +def debug_buffer_access(runtime_args, operation, buffer_name, index): + print(f"[DEBUG] {operation} {buffer_name}[{index}]") +``` + +## Performance Considerations + +1. **Minimize mutex lock time:** + - Read/write buffers quickly + - Avoid complex operations while holding mutex + - Use local variables for processing + +2. **Plugin lifecycle:** + - Initialize resources in `init()` + - Start threads/servers in `start_loop()` + - Clean up in `cleanup()` + +3. **Memory management:** + - Python plugins: Let Python GC handle memory + - Native plugins: Free allocated memory in `cleanup()` + +## Dependencies + +### Required +- OpenPLC Runtime core +- pthread library +- Python 3.x (for Python plugins) + +### Optional +- pymodbus (for enhanced Modbus functionality) +- Additional Python packages as needed by specific plugins + +## License + +This plugin system is part of the OpenPLC Runtime project and follows the same licensing terms. + +## Contributing + +When contributing new plugins: + +1. Follow the established plugin interface +2. Include comprehensive error handling +3. Document configuration options +4. Provide usage examples +5. Test with multiple buffer configurations +6. Ensure thread safety for buffer access + +## See Also + +- `example_python_plugin.py` - Basic plugin template +- `modbus_slave.py` - Complete Modbus TCP slave implementation +- `plugin_config_example.txt` - Configuration file format +- OpenPLC Runtime documentation diff --git a/core/src/drivers/example_python_plugin.py b/core/src/drivers/example_python_plugin.py index fd39b6e5..561c4580 100644 --- a/core/src/drivers/example_python_plugin.py +++ b/core/src/drivers/example_python_plugin.py @@ -8,52 +8,19 @@ import ctypes from ctypes import * +# Import the correct type definitions +from python_plugin_types import ( + PluginRuntimeArgs, + safe_extract_runtime_args_from_capsule, + SafeBufferAccess, + PluginStructureValidator +) + # Global variable to track initialization _initialized = False _runtime_args = None - -# Define the runtime arguments structure matching the C structure -class PluginRuntimeArgs(ctypes.Structure): - """Python ctypes structure matching plugin_runtime_args_t""" - _fields_ = [ - # Buffer arrays (using POINTER type for arrays) - ("bool_input", POINTER(POINTER(ctypes.c_bool * 8))), # bool_input[BUFFER_SIZE][8] - ("bool_output", POINTER(POINTER(ctypes.c_bool * 8))), # bool_output[BUFFER_SIZE][8] - ("byte_input", POINTER(POINTER(ctypes.c_ubyte))), # byte_input[BUFFER_SIZE] - ("byte_output", POINTER(POINTER(ctypes.c_ubyte))), # byte_output[BUFFER_SIZE] - ("int_input", POINTER(POINTER(ctypes.c_uint16))), # int_input[BUFFER_SIZE] - ("int_output", POINTER(POINTER(ctypes.c_uint16))), # int_output[BUFFER_SIZE] - ("dint_input", POINTER(POINTER(ctypes.c_uint32))), # dint_input[BUFFER_SIZE] - ("dint_output", POINTER(POINTER(ctypes.c_uint32))), # dint_output[BUFFER_SIZE] - ("lint_input", POINTER(POINTER(ctypes.c_uint64))), # lint_input[BUFFER_SIZE] - ("lint_output", POINTER(POINTER(ctypes.c_uint64))), # lint_output[BUFFER_SIZE] - ("int_memory", POINTER(POINTER(ctypes.c_uint16))), # int_memory[BUFFER_SIZE] - ("dint_memory", POINTER(POINTER(ctypes.c_uint32))), # dint_memory[BUFFER_SIZE] - ("lint_memory", POINTER(POINTER(ctypes.c_uint64))), # lint_memory[BUFFER_SIZE] - - # Mutex function pointers - ("mutex_take", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_take)(pthread_mutex_t*) - ("mutex_give", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_give)(pthread_mutex_t*) - ("buffer_mutex", c_void_p), # pthread_mutex_t *buffer_mutex - - # Buffer size information - ("buffer_size", c_int), # int buffer_size - ("bits_per_buffer", c_int), # int bits_per_buffer - ] - -def extract_runtime_args_from_capsule(capsule): - """Extract runtime arguments from PyCapsule""" - if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': - raise TypeError("Expected PyCapsule object") - - # Get the pointer from the capsule - ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") - if not ptr: - raise ValueError("Failed to extract pointer from capsule") - - # Cast the pointer to our structure type - args_ptr = ctypes.cast(ptr, POINTER(PluginRuntimeArgs)) - return args_ptr.contents +_safe_buffer_access = None +_runtime_args_capsule = None def init(runtime_args_capsule): """ @@ -63,26 +30,50 @@ def init(runtime_args_capsule): Args: runtime_args_capsule: PyCapsule containing plugin_runtime_args_t structure """ - global _initialized, _runtime_args + global _initialized, _runtime_args, _safe_buffer_access, _runtime_args_capsule + _runtime_args_capsule = runtime_args_capsule print("Python plugin 'example_plugin' initializing...") try: - # Extract runtime args from capsule - # runtime_args = extract_runtime_args_from_capsule(runtime_args_capsule) - # print(f"✓ Runtime arguments extracted successfully") - # print(f" Buffer size: {runtime_args.buffer_size}") - # print(f" Bits per buffer: {runtime_args.bits_per_buffer}") + # Print structure validation info for debugging + print("Validating plugin structure alignment...") + PluginStructureValidator.print_structure_info() + + # Extract runtime args from capsule using safe method + runtime_args, error_msg = safe_extract_runtime_args_from_capsule(runtime_args_capsule) + if runtime_args is None: + print(f"✗ Failed to extract runtime args: {error_msg}") + return False + + print(f"✓ Runtime arguments extracted successfully") + + # Safely access buffer size using validation + buffer_size, size_error = runtime_args.safe_access_buffer_size() + if buffer_size == -1: + print(f"✗ Failed to access buffer size: {size_error}") + return False + + print(f" Buffer size: {buffer_size}") + print(f" Bits per buffer: {runtime_args.bits_per_buffer}") + print(f" Structure details: {runtime_args}") - # # Store runtime args for later use - # _runtime_args = runtime_args - # _initialized = True + # Create safe buffer access wrapper + _safe_buffer_access = SafeBufferAccess(runtime_args) + if not _safe_buffer_access.is_valid: + print(f"✗ Failed to create safe buffer access: {_safe_buffer_access.error_msg}") + return False + + # Store runtime args for later use + _runtime_args = runtime_args print("✓ Plugin initialized successfully") return True except Exception as e: print(f"✗ Plugin initialization failed: {e}") + import traceback + traceback.print_exc() return False def start_loop(): @@ -90,7 +81,17 @@ def start_loop(): Called when the plugin loop should start Optional function - not all plugins need this """ + + current_args, error_msg = safe_extract_runtime_args_from_capsule(_runtime_args_capsule) + + global _runtime_args print("Plugin start_loop called") + while True: + time.sleep(0.1) + print("Plugin running...") + print(f"Output[0][0]: {(bool(current_args.bool_output[0][0])) if current_args else 'N/A'}") + # print(f"safe read output[0][0]: {_safe_buffer_access.safe_read_bool_output(0,0)}" if _safe_buffer_access else "N/A") + print(f"Output buffer address: 0x{ctypes.addressof(current_args.bool_output.contents) if current_args else 0:x}") pass def stop_loop(): @@ -101,30 +102,6 @@ def stop_loop(): print("Plugin stop_loop called") pass -def run_cycle(): - """ - Main plugin cycle function - Called periodically by the plugin system - Optional function - some plugins may only need init - """ - global _initialized, _runtime_args - - if not _initialized or not _runtime_args: - return - - # Example: Toggle a digital output every cycle - try: - if _runtime_args.mutex_take(_runtime_args.buffer_mutex) == 0: - # Toggle bool_output[0][0] - current_value = _runtime_args.bool_output[0][0] - _runtime_args.bool_output[0][0] = not current_value - print(f"Toggled output 0.0 to {not current_value}") - except Exception as e: - print(f"Error in run_cycle: {e}") - finally: - if _runtime_args.buffer_mutex: - _runtime_args.mutex_give(_runtime_args.buffer_mutex) - def cleanup(): """ Plugin cleanup function diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 3576a75e..6417def7 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -316,12 +316,24 @@ void plugin_driver_destroy(plugin_driver_t *driver) */ void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver) { + printf("[PLUGIN]: Generating structured args for plugin type %d\n", type); + + if (!driver) + { + fprintf(stderr, "[PLUGIN]: Error - driver is NULL\n"); + return NULL; + } + plugin_runtime_args_t *args = malloc(sizeof(plugin_runtime_args_t)); if (!args) { + fprintf(stderr, "[PLUGIN]: Error - failed to allocate memory for runtime args\n"); return NULL; } + printf("[PLUGIN]: Allocated runtime args structure (size: %zu bytes)\n", + sizeof(plugin_runtime_args_t)); + // Initialize all buffer pointers args->bool_input = bool_input; args->bool_output = bool_output; @@ -341,23 +353,57 @@ void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t * args->mutex_take = plugin_mutex_take; args->mutex_give = plugin_mutex_give; // Set buffer mutex from driver - args->buffer_mutex = driver ? &driver->buffer_mutex : NULL; + args->buffer_mutex = &driver->buffer_mutex; // Initialize buffer size info args->buffer_size = BUFFER_SIZE; args->bits_per_buffer = 8; + printf("[PLUGIN]: Runtime args initialized:\n"); + printf("[PLUGIN]: buffer_size = %d\n", args->buffer_size); + printf("[PLUGIN]: bits_per_buffer = %d\n", args->bits_per_buffer); + printf("[PLUGIN]: buffer_mutex = %p\n", (void *)args->buffer_mutex); + printf("[PLUGIN]: bool_input = %p\n", (void *)args->bool_input); + printf("[PLUGIN]: mutex_take = %p\n", (void *)args->mutex_take); + printf("[PLUGIN]: mutex_give = %p\n", (void *)args->mutex_give); + + // Validate critical pointers + if (!args->buffer_mutex) + { + fprintf(stderr, "[PLUGIN]: Error - buffer_mutex is NULL\n"); + free(args); + return NULL; + } + + if (!args->mutex_take || !args->mutex_give) + { + fprintf(stderr, "[PLUGIN]: Error - mutex function pointers are NULL\n"); + free(args); + return NULL; + } + switch (type) { case PLUGIN_TYPE_NATIVE: + printf("[PLUGIN]: Returning native plugin args\n"); // For native plugins, return the structure directly return args; case PLUGIN_TYPE_PYTHON: + printf("[PLUGIN]: Creating Python capsule for args\n"); // For Python plugins, wrap in a PyCapsule - return create_python_runtime_args_capsule(args); + PyObject *capsule = create_python_runtime_args_capsule(args); + if (!capsule) + { + fprintf(stderr, "[PLUGIN]: Error - failed to create Python capsule\n"); + free(args); + return NULL; + } + printf("[PLUGIN]: Python capsule created successfully\n"); + return capsule; default: + fprintf(stderr, "[PLUGIN]: Error - unknown plugin type: %d\n", type); // Unknown type, clean up and return NULL free(args); return NULL; diff --git a/core/src/drivers/python_plugin_types.py b/core/src/drivers/python_plugin_types.py new file mode 100644 index 00000000..1ef19bf8 --- /dev/null +++ b/core/src/drivers/python_plugin_types.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Shared type definitions for OpenPLC Python plugins +This module provides correct ctypes mappings for the plugin_runtime_args_t structure +""" + +import ctypes +from ctypes import * +import sys + +# IEC type mappings based on iec_types.h +# These must match exactly with the C definitions +IEC_BOOL = ctypes.c_uint8 # typedef uint8_t IEC_BOOL; +IEC_BYTE = ctypes.c_uint8 # typedef uint8_t IEC_BYTE; +IEC_UINT = ctypes.c_uint16 # typedef uint16_t IEC_UINT; +IEC_UDINT = ctypes.c_uint32 # typedef uint32_t IEC_UDINT; +IEC_ULINT = ctypes.c_uint64 # typedef uint64_t IEC_ULINT; + +class PluginRuntimeArgs(ctypes.Structure): + """ + Python ctypes structure matching plugin_runtime_args_t from plugin_driver.h + + CRITICAL: This structure must match the C definition exactly to prevent + segmentation faults and memory corruption. + """ + _fields_ = [ + # Buffer arrays - these are pointers to arrays of pointers + # C: IEC_BOOL *(*bool_input)[8] means pointer to array of 8 pointers + ("bool_input", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), + ("bool_output", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), + ("byte_input", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), + ("byte_output", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), + ("int_input", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("int_output", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("dint_input", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("dint_output", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("lint_input", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + ("lint_output", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + ("int_memory", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("dint_memory", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("lint_memory", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + + # Mutex function pointers + ("mutex_take", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), + ("mutex_give", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), + ("buffer_mutex", ctypes.c_void_p), + + # Buffer size information + ("buffer_size", ctypes.c_int), + ("bits_per_buffer", ctypes.c_int), + ] + + def validate_pointers(self): + """ + Validate that critical pointers are not NULL + Returns: (bool, str) - (is_valid, error_message) + """ + try: + # Check buffer mutex + if not self.buffer_mutex: + return False, "buffer_mutex is NULL" + + # Check mutex functions + if not self.mutex_take: + return False, "mutex_take function pointer is NULL" + if not self.mutex_give: + return False, "mutex_give function pointer is NULL" + + # Check buffer size is reasonable + if self.buffer_size <= 0 or self.buffer_size > 10000: + return False, f"buffer_size is invalid: {self.buffer_size}" + + if self.bits_per_buffer <= 0 or self.bits_per_buffer > 64: + return False, f"bits_per_buffer is invalid: {self.bits_per_buffer}" + + return True, "All pointers valid" + + except Exception as e: + return False, f"Exception during validation: {e}" + + def safe_access_buffer_size(self): + """ + Safely access buffer_size with validation + Returns: (int, str) - (buffer_size, error_message) + """ + try: + is_valid, msg = self.validate_pointers() + if not is_valid: + return -1, f"Validation failed: {msg}" + + size = self.buffer_size + if size <= 0 or size > 10000: + return -1, f"Invalid buffer size: {size}" + + return size, "Success" + + except Exception as e: + return -1, f"Exception accessing buffer_size: {e}" + + def __str__(self): + """Debug representation of the structure""" + try: + return (f"PluginRuntimeArgs(\n" + f" bool_input=0x{ctypes.addressof(self.bool_input.contents) if self.bool_input else 0:x},\n" + f" bool_output=0x{ctypes.addressof(self.bool_output.contents) if self.bool_output else 0:x},\n" + f" byte_input=0x{ctypes.addressof(self.byte_input.contents) if self.byte_input else 0:x},\n" + f" byte_output=0x{ctypes.addressof(self.byte_output.contents) if self.byte_output else 0:x},\n" + f" int_input=0x{ctypes.addressof(self.int_input.contents) if self.int_input else 0:x},\n" + f" int_output=0x{ctypes.addressof(self.int_output.contents) if self.int_output else 0:x},\n" + f" dint_input=0x{ctypes.addressof(self.dint_input.contents) if self.dint_input else 0:x},\n" + f" dint_output=0x{ctypes.addressof(self.dint_output.contents) if self.dint_output else 0:x},\n" + f" lint_input=0x{ctypes.addressof(self.lint_input.contents) if self.lint_input else 0:x},\n" + f" lint_output=0x{ctypes.addressof(self.lint_output.contents) if self.lint_output else 0:x},\n" + f" int_memory=0x{ctypes.addressof(self.int_memory.contents) if self.int_memory else 0:x},\n" + f" buffer_size={self.buffer_size},\n" + f" bits_per_buffer={self.bits_per_buffer},\n" + f" buffer_mutex=0x{self.buffer_mutex or 0:x},\n" + f" mutex_take={'valid' if self.mutex_take else 'NULL'},\n" + f" mutex_give={'valid' if self.mutex_give else 'NULL'}\n" + f")") + except: + return "PluginRuntimeArgs(corrupted or invalid)" + +class PluginStructureValidator: + """Validates structure alignment and provides debugging tools""" + + @staticmethod + def validate_structure_alignment(): + """ + Validates that the Python ctypes structure has the expected size and alignment + Returns: (bool, str, dict) - (is_valid, message, debug_info) + """ + try: + # Calculate expected structure size + # This is platform-dependent but we can do basic checks + struct_size = ctypes.sizeof(PluginRuntimeArgs) + + debug_info = { + 'structure_size': struct_size, + 'pointer_size': ctypes.sizeof(ctypes.c_void_p), + 'int_size': ctypes.sizeof(ctypes.c_int), + 'platform': sys.platform, + 'architecture': sys.maxsize > 2**32 and '64-bit' or '32-bit' + } + + # Basic sanity checks + expected_min_size = ( + 13 * ctypes.sizeof(ctypes.c_void_p) + # 13 buffer pointers + 2 * ctypes.sizeof(ctypes.c_void_p) + # 2 function pointers + 1 * ctypes.sizeof(ctypes.c_void_p) + # 1 mutex pointer + 2 * ctypes.sizeof(ctypes.c_int) # 2 integers + ) + + if struct_size < expected_min_size: + return False, f"Structure too small: {struct_size} < {expected_min_size}", debug_info + + # Check field offsets make sense + buffer_size_offset = PluginRuntimeArgs.buffer_size.offset + bits_per_buffer_offset = PluginRuntimeArgs.bits_per_buffer.offset + + if bits_per_buffer_offset <= buffer_size_offset: + return False, "Field offsets are incorrect", debug_info + + debug_info['buffer_size_offset'] = buffer_size_offset + debug_info['bits_per_buffer_offset'] = bits_per_buffer_offset + + return True, "Structure validation passed", debug_info + + except Exception as e: + return False, f"Exception during validation: {e}", {} + + @staticmethod + def print_structure_info(): + """Print detailed structure information for debugging""" + is_valid, msg, debug_info = PluginStructureValidator.validate_structure_alignment() + + print("=== Plugin Structure Validation ===") + print(f"Status: {'VALID' if is_valid else 'INVALID'}") + print(f"Message: {msg}") + print("\nStructure Details:") + for key, value in debug_info.items(): + print(f" {key}: {value}") + + print(f"\nField Offsets:") + try: + for field_name, field_type in PluginRuntimeArgs._fields_: + offset = getattr(PluginRuntimeArgs, field_name).offset + print(f" {field_name}: offset {offset}") + except Exception as e: + print(f" Error getting field offsets: {e}") + +class SafeBufferAccess: + """Wrapper class for safe buffer operations with mutex handling""" + + def __init__(self, runtime_args): + """ + Initialize with validated runtime args + Args: + runtime_args: PluginRuntimeArgs instance + """ + self.args = runtime_args + self.is_valid, self.error_msg = runtime_args.validate_pointers() + + def safe_read_bool_output(self, buffer_idx, bit_idx): + """ + Safely read a boolean output with proper mutex handling + Returns: (bool, str) - (value, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Access the value + value = bool(self.args.bool_output[buffer_idx][bit_idx]) + return value, "Success" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except Exception as e: + return False, f"Exception during buffer access: {e}" + + def safe_write_bool_output(self, buffer_idx, bit_idx, value): + """ + Safely write a boolean output with proper mutex handling + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Set the value + self.args.bool_output[buffer_idx][bit_idx] = 1 if value else 0 + return True, "Success" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except Exception as e: + return False, f"Exception during buffer access: {e}" + +def safe_extract_runtime_args_from_capsule(capsule): + """ + Enhanced capsule extraction with comprehensive validation + Args: + capsule: PyCapsule containing plugin_runtime_args_t structure + Returns: + (PluginRuntimeArgs, str) - (runtime_args, error_message) + """ + try: + # Validate capsule type + if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': + return None, f"Expected PyCapsule object, got {type(capsule)}" + + # Set up the Python API function signatures + ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] + ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p + + # Get the pointer from the capsule + ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") + if not ptr: + return None, "Failed to extract pointer from capsule - invalid capsule name or corrupted data" + + # Cast the pointer to our structure type + args_ptr = ctypes.cast(ptr, ctypes.POINTER(PluginRuntimeArgs)) + if not args_ptr: + return None, "Failed to cast pointer to PluginRuntimeArgs structure" + + runtime_args = args_ptr.contents + + # Validate the extracted structure + is_valid, validation_msg = runtime_args.validate_pointers() + if not is_valid: + return None, f"Structure validation failed: {validation_msg}" + + return runtime_args, "Success" + + except Exception as e: + return None, f"Exception during capsule extraction: {e}" + +if __name__ == "__main__": + # Self-test when run directly + print("OpenPLC Python Plugin Types - Self Test") + print("=" * 50) + + # Test structure validation + PluginStructureValidator.print_structure_info() + + print(f"\nIEC Type Sizes:") + print(f" IEC_BOOL: {ctypes.sizeof(IEC_BOOL)} bytes") + print(f" IEC_BYTE: {ctypes.sizeof(IEC_BYTE)} bytes") + print(f" IEC_UINT: {ctypes.sizeof(IEC_UINT)} bytes") + print(f" IEC_UDINT: {ctypes.sizeof(IEC_UDINT)} bytes") + print(f" IEC_ULINT: {ctypes.sizeof(IEC_ULINT)} bytes") From 306311676f8c73075e373f9d2e4f5cb07fea4719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Tue, 16 Sep 2025 13:26:46 +0200 Subject: [PATCH 11/44] Fix buffer access in Python plugin driver Corrects how bool_output buffer values are read and written by accessing the actual value via .contents.value instead of the pointer. Updates example plugin to use SafeBufferAccess for safer buffer operations and improves output logging. --- core/src/drivers/example_python_plugin.py | 8 ++++---- core/src/drivers/python_plugin_types.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/drivers/example_python_plugin.py b/core/src/drivers/example_python_plugin.py index 561c4580..a42308df 100644 --- a/core/src/drivers/example_python_plugin.py +++ b/core/src/drivers/example_python_plugin.py @@ -88,10 +88,10 @@ def start_loop(): print("Plugin start_loop called") while True: time.sleep(0.1) - print("Plugin running...") - print(f"Output[0][0]: {(bool(current_args.bool_output[0][0])) if current_args else 'N/A'}") - # print(f"safe read output[0][0]: {_safe_buffer_access.safe_read_bool_output(0,0)}" if _safe_buffer_access else "N/A") - print(f"Output buffer address: 0x{ctypes.addressof(current_args.bool_output.contents) if current_args else 0:x}") + addr = ctypes.addressof(current_args.bool_output[0][0]) + internal_safe_buffer_access = SafeBufferAccess(current_args) + value, msg = internal_safe_buffer_access.safe_read_bool_output(0,0) + print(f"Value at address 0x{addr:x}: {value} ({msg})") pass def stop_loop(): diff --git a/core/src/drivers/python_plugin_types.py b/core/src/drivers/python_plugin_types.py index 1ef19bf8..f9275bc6 100644 --- a/core/src/drivers/python_plugin_types.py +++ b/core/src/drivers/python_plugin_types.py @@ -221,8 +221,8 @@ def safe_read_bool_output(self, buffer_idx, bit_idx): if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: return False, f"Invalid bit index: {bit_idx}" - # Access the value - value = bool(self.args.bool_output[buffer_idx][bit_idx]) + # Access the value - read from the actual value, not the pointer + value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) return value, "Success" finally: @@ -252,8 +252,8 @@ def safe_write_bool_output(self, buffer_idx, bit_idx, value): if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: return False, f"Invalid bit index: {bit_idx}" - # Set the value - self.args.bool_output[buffer_idx][bit_idx] = 1 if value else 0 + # Set the value - access the actual value, not the pointer + self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 return True, "Success" finally: From ef9b1ae2c03407418c784b6167c254358f7856d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Tue, 16 Sep 2025 13:28:11 +0200 Subject: [PATCH 12/44] deleting stop call Stop is already being called within destroy function --- core/src/plc_app/plc_main.c | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index 57102ce5..5dea32d7 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -197,7 +197,6 @@ int main() // Cleanup plugin driver system if (plugin_driver) { - plugin_driver_stop(plugin_driver); plugin_driver_destroy(plugin_driver); } From 39a893d4dec691bc31048441b4f16815b8404f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Tue, 16 Sep 2025 15:39:27 +0200 Subject: [PATCH 13/44] Remove unused _runtime_args_capsule variable Eliminated the _runtime_args_capsule global variable and related code from example_python_plugin.py, simplifying state management and usage of runtime arguments. --- core/src/drivers/example_python_plugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/drivers/example_python_plugin.py b/core/src/drivers/example_python_plugin.py index a42308df..733c908e 100644 --- a/core/src/drivers/example_python_plugin.py +++ b/core/src/drivers/example_python_plugin.py @@ -20,7 +20,6 @@ _initialized = False _runtime_args = None _safe_buffer_access = None -_runtime_args_capsule = None def init(runtime_args_capsule): """ @@ -31,7 +30,6 @@ def init(runtime_args_capsule): runtime_args_capsule: PyCapsule containing plugin_runtime_args_t structure """ global _initialized, _runtime_args, _safe_buffer_access, _runtime_args_capsule - _runtime_args_capsule = runtime_args_capsule print("Python plugin 'example_plugin' initializing...") @@ -82,14 +80,12 @@ def start_loop(): Optional function - not all plugins need this """ - current_args, error_msg = safe_extract_runtime_args_from_capsule(_runtime_args_capsule) - global _runtime_args print("Plugin start_loop called") while True: time.sleep(0.1) - addr = ctypes.addressof(current_args.bool_output[0][0]) - internal_safe_buffer_access = SafeBufferAccess(current_args) + addr = ctypes.addressof(_runtime_args.bool_output[0][0]) + internal_safe_buffer_access = SafeBufferAccess(_runtime_args) value, msg = internal_safe_buffer_access.safe_read_bool_output(0,0) print(f"Value at address 0x{addr:x}: {value} ({msg})") pass From df1ac5d138af1d8d4a2f5a08ec975a33c4e190c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 10:24:50 +0200 Subject: [PATCH 14/44] Refactor Python plugin threading and lifecycle management Moved plugin thread creation to Python side and removed native thread management for Python plugins. Updated function names in python_binds_t for clarity. Improved plugin start, stop, and cleanup logic to use Python-side functions and ensured proper GIL handling. Cleaned up resource management and removed unused thread fields from plugin_instance_t. --- core/src/drivers/plugin_driver.c | 164 ++++++++++++++---------- core/src/drivers/plugin_driver.h | 2 +- core/src/drivers/python_plugin_bridge.h | 4 +- 3 files changed, 96 insertions(+), 74 deletions(-) diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 6417def7..2a2e7d2c 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -23,37 +23,10 @@ extern IEC_ULINT *lint_output[BUFFER_SIZE]; extern IEC_UINT *int_memory[BUFFER_SIZE]; extern IEC_UDINT *dint_memory[BUFFER_SIZE]; extern IEC_ULINT *lint_memory[BUFFER_SIZE]; +PyThreadState *main_tstate = NULL; -// Plugin thread function -void *plugin_thread_function(void *arg) -{ - plugin_instance_t *plugin = (plugin_instance_t *)arg; - - PyObject *res = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncStartLoop, NULL); - if (!res) - { - PyErr_Print(); - fprintf(stderr, "Python start loop function failed for plugin: %s\n", plugin->config.name); - // return -1; - } - else - { - printf("[PLUGIN]: Plugin %s started successfully.\n", plugin->config.name); - } - Py_DECREF(res); - - plugin->running = 1; - - // Main plugin loop - while (plugin->running) - { - // Here plugins can do their work - // They can call the buffer access functions - usleep(10000); // 10ms sleep to prevent busy waiting - } - - return NULL; -} +// Prototypes +static void python_plugin_cleanup(plugin_instance_t *plugin); // Driver management functions plugin_driver_t *plugin_driver_create(void) @@ -86,6 +59,7 @@ static int plugin_mutex_give(pthread_mutex_t *mutex) } // Python capsule destructor for runtime args +// Breakpoint here to debug capsule issues static void plugin_runtime_args_capsule_destructor(PyObject *capsule) { plugin_runtime_args_t *args = @@ -174,7 +148,8 @@ int plugin_driver_init(plugin_driver_t *driver) plugin->python_plugin->pFuncInit) { // Generate structured args for Python plugin - PyObject *args = generate_structured_args_with_driver(PLUGIN_TYPE_PYTHON, driver); + PyObject *args = + (PyObject *)generate_structured_args_with_driver(PLUGIN_TYPE_PYTHON, driver); if (!args) { fprintf(stderr, "Failed to generate runtime args for plugin: %s\n", @@ -184,7 +159,7 @@ int plugin_driver_init(plugin_driver_t *driver) // Call the Python init function with proper capsule PyObject *result = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, args, NULL); - Py_DECREF(args); + Py_SET_REFCNT(args, UINT64_MAX); if (!result) { @@ -212,6 +187,9 @@ int plugin_driver_start(plugin_driver_t *driver) return -1; } + main_tstate = PyEval_SaveThread(); + PyGILState_STATE g = PyGILState_Ensure(); + for (int i = 0; i < driver->plugin_count; i++) { plugin_instance_t *plugin = &driver->plugins[i]; @@ -219,16 +197,24 @@ int plugin_driver_start(plugin_driver_t *driver) { case PLUGIN_TYPE_PYTHON: { - // Python plugins run asynchronously in their own threads - if (plugin->python_plugin && plugin->python_plugin->pFuncStartLoop) + // Python plugins run asynchronously in their own threads. + // NOTE: The thread is created python-side + if (plugin->python_plugin && plugin->python_plugin->pFuncStart) { - // Create a thread to run the plugin thread function - if (pthread_create(&plugin->thread, NULL, plugin_thread_function, plugin) != 0) + PyObject *res = PyObject_CallNoArgs(plugin->python_plugin->pFuncStart); + if (!res) { - fprintf(stderr, "Failed to create thread for plugin: %s\n", + PyErr_Print(); + fprintf(stderr, "Python start call failed for plugin: %s\n", plugin->config.name); - return -1; } + else + { + printf("[PLUGIN]: Plugin %s started successfully.\n", plugin->config.name); + } + Py_DECREF(res); + + plugin->running = 1; } else { @@ -248,7 +234,7 @@ int plugin_driver_start(plugin_driver_t *driver) break; } } - + PyGILState_Release(g); return 0; } @@ -263,25 +249,29 @@ int plugin_driver_stop(plugin_driver_t *driver) // Signal all plugins to stop for (int i = 0; i < driver->plugin_count; i++) { - driver->plugins[i].running = 0; - } - - // Wait for all threads to finish - for (int i = 0; i < driver->plugin_count; i++) - { - if (driver->plugins[i].thread) - { - printf("[PLUGIN]: Plugin %s thread canceling.\n", driver->plugins[i].config.name); - pthread_cancel(driver->plugins[i].thread); - driver->plugins[i].thread = 0; - printf("[PLUGIN]: Plugin %s thread canceled.\n", driver->plugins[i].config.name); - // pthread_join(driver->plugins[i].thread, NULL); - } - if (driver->plugins[i].manager) + if (driver->plugins[i].python_plugin && driver->plugins[i].python_plugin->pFuncStop && + driver->plugins[i].running) { - plugin_manager_destroy(driver->plugins[i].manager); - driver->plugins[i].manager = NULL; + plugin_instance_t *plugin = &driver->plugins[i]; + + PyObject *res = + PyObject_CallFunctionObjArgs(driver->plugins[i].python_plugin->pFuncStop, NULL); + if (!res) + { + PyErr_Print(); + fprintf(stderr, "Python stop call failed for plugin: %s\n", plugin->config.name); + } + else + { + printf("[PLUGIN]: Plugin %s stopped successfully.\n", plugin->config.name); + } + Py_DECREF(res); + + plugin->running = 0; } + + // Plugin manager only handles destruction, not stopping + // TODO: Implement native plugin stop logic if needed } return 0; @@ -295,7 +285,24 @@ void plugin_driver_destroy(plugin_driver_t *driver) } plugin_driver_stop(driver); + + for (int i = 0; i < driver->plugin_count; i++) + { + plugin_instance_t *plugin = &driver->plugins[i]; + if (plugin->manager) + { + plugin_manager_destroy(plugin->manager); + plugin->manager = NULL; + } + if (plugin->python_plugin) + { + python_plugin_cleanup(plugin); + } + } + PyEval_RestoreThread(main_tstate); + Py_FinalizeEx(); pthread_mutex_destroy(&driver->buffer_mutex); + free(driver); } @@ -480,7 +487,6 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) snprintf(python_path_cmd, sizeof(python_path_cmd), "import sys; sys.path.insert(0, '.')"); } - PyRun_SimpleString("import sys"); PyRun_SimpleString(python_path_cmd); // Load the Python module @@ -507,20 +513,20 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) return -1; } - py_binds->pFuncStartLoop = PyObject_GetAttrString(py_binds->pModule, "start_loop"); - if (!py_binds->pFuncStartLoop || !PyCallable_Check(py_binds->pFuncStartLoop)) + py_binds->pFuncStart = PyObject_GetAttrString(py_binds->pModule, "start_loop"); + if (!py_binds->pFuncStart || !PyCallable_Check(py_binds->pFuncStart)) { // start_loop is optional - Py_XDECREF(py_binds->pFuncStartLoop); - py_binds->pFuncStartLoop = NULL; + Py_XDECREF(py_binds->pFuncStart); + py_binds->pFuncStart = NULL; } - py_binds->pFuncStopLoop = PyObject_GetAttrString(py_binds->pModule, "stop_loop"); - if (!py_binds->pFuncStopLoop || !PyCallable_Check(py_binds->pFuncStopLoop)) + py_binds->pFuncStop = PyObject_GetAttrString(py_binds->pModule, "stop_loop"); + if (!py_binds->pFuncStop || !PyCallable_Check(py_binds->pFuncStop)) { // stop_loop is optional - Py_XDECREF(py_binds->pFuncStopLoop); - py_binds->pFuncStopLoop = NULL; + Py_XDECREF(py_binds->pFuncStop); + py_binds->pFuncStop = NULL; } py_binds->pFuncCleanup = PyObject_GetAttrString(py_binds->pModule, "cleanup"); @@ -536,8 +542,8 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) printf("Python plugin '%s' symbols loaded successfully\n", module_name); printf(" - init: %s\n", py_binds->pFuncInit ? "✓" : "✗"); - printf(" - start_loop: %s\n", py_binds->pFuncStartLoop ? "✓" : "✗"); - printf(" - stop_loop: %s\n", py_binds->pFuncStopLoop ? "✓" : "✗"); + printf(" - start_loop: %s\n", py_binds->pFuncStart ? "✓" : "✗"); + printf(" - stop_loop: %s\n", py_binds->pFuncStop ? "✓" : "✗"); printf(" - cleanup: %s\n", py_binds->pFuncCleanup ? "✓" : "✗"); return 0; @@ -552,16 +558,32 @@ void python_plugin_cycle(plugin_instance_t *plugin) } // Cleanup Python plugin -void python_plugin_cleanup(plugin_instance_t *plugin) +static void python_plugin_cleanup(plugin_instance_t *plugin) { (void)plugin; // Suppress unused parameter warning // Cleanup Python resources if (plugin && plugin->python_plugin) { - // Clean up Python objects + // Call cleanup function if available + if (plugin->python_plugin->pFuncCleanup) + { + PyObject *res = PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncCleanup, NULL); + if (!res) + { + PyErr_Print(); + fprintf(stderr, "Python cleanup call failed for plugin: %s\n", plugin->config.name); + } + else + { + printf("[PLUGIN]: Plugin %s cleaned up successfully.\n", plugin->config.name); + } + Py_DECREF(res); + } + + // Decrement references to Python objects Py_XDECREF(plugin->python_plugin->pFuncInit); - Py_XDECREF(plugin->python_plugin->pFuncStartLoop); - Py_XDECREF(plugin->python_plugin->pFuncStopLoop); + Py_XDECREF(plugin->python_plugin->pFuncStart); + Py_XDECREF(plugin->python_plugin->pFuncStop); Py_XDECREF(plugin->python_plugin->pFuncCleanup); Py_XDECREF(plugin->python_plugin->pModule); diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index 966c02cc..76d93d26 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -67,7 +67,7 @@ typedef struct plugin_instance_s { PluginManager *manager; python_binds_t *python_plugin; - pthread_t thread; + // pthread_t thread; int running; plugin_config_t config; } plugin_instance_t; diff --git a/core/src/drivers/python_plugin_bridge.h b/core/src/drivers/python_plugin_bridge.h index 76964407..1c00060d 100644 --- a/core/src/drivers/python_plugin_bridge.h +++ b/core/src/drivers/python_plugin_bridge.h @@ -12,8 +12,8 @@ typedef struct { PyObject *pModule; PyObject *pFuncInit; // Driver Init function - PyObject *pFuncStartLoop; - PyObject *pFuncStopLoop; + PyObject *pFuncStart; + PyObject *pFuncStop; PyObject *pFuncCleanup; } python_binds_t; From 89e8a3e1c91d77b592b34ba6bc2f76379b95a425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 13:32:28 +0200 Subject: [PATCH 15/44] Refactor plugin driver cleanup and GIL management Improves Python GIL state management in plugin_driver by using a static variable and ensuring proper acquisition/release during plugin lifecycle. Moves plugin driver cleanup earlier in plc_main to avoid double destruction and adds more informative logging during plugin stop. --- core/src/drivers/plugin_driver.c | 19 +++++++++++++------ core/src/plc_app/plc_main.c | 12 ++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 2a2e7d2c..aa4138be 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -23,7 +23,8 @@ extern IEC_ULINT *lint_output[BUFFER_SIZE]; extern IEC_UINT *int_memory[BUFFER_SIZE]; extern IEC_UDINT *dint_memory[BUFFER_SIZE]; extern IEC_ULINT *lint_memory[BUFFER_SIZE]; -PyThreadState *main_tstate = NULL; +static PyThreadState *main_tstate = NULL; +static PyGILState_STATE gstate; // Prototypes static void python_plugin_cleanup(plugin_instance_t *plugin); @@ -187,8 +188,8 @@ int plugin_driver_start(plugin_driver_t *driver) return -1; } - main_tstate = PyEval_SaveThread(); - PyGILState_STATE g = PyGILState_Ensure(); + main_tstate = PyEval_SaveThread(); + gstate = PyGILState_Ensure(); for (int i = 0; i < driver->plugin_count; i++) { @@ -234,7 +235,7 @@ int plugin_driver_start(plugin_driver_t *driver) break; } } - PyGILState_Release(g); + PyGILState_Release(gstate); return 0; } @@ -249,13 +250,14 @@ int plugin_driver_stop(plugin_driver_t *driver) // Signal all plugins to stop for (int i = 0; i < driver->plugin_count; i++) { + printf("[PLUGIN]: Stopping plugin %d/%d: %s\n", i + 1, driver->plugin_count, + driver->plugins[i].config.name); if (driver->plugins[i].python_plugin && driver->plugins[i].python_plugin->pFuncStop && driver->plugins[i].running) { plugin_instance_t *plugin = &driver->plugins[i]; - PyObject *res = - PyObject_CallFunctionObjArgs(driver->plugins[i].python_plugin->pFuncStop, NULL); + PyObject *res = PyObject_CallNoArgs(driver->plugins[i].python_plugin->pFuncStop); if (!res) { PyErr_Print(); @@ -270,6 +272,7 @@ int plugin_driver_stop(plugin_driver_t *driver) plugin->running = 0; } + printf("[PLUGIN]: Plugin %s stopped...\n", driver->plugins[i].config.name); // Plugin manager only handles destruction, not stopping // TODO: Implement native plugin stop logic if needed } @@ -284,6 +287,8 @@ void plugin_driver_destroy(plugin_driver_t *driver) return; } + gstate = PyGILState_Ensure(); + plugin_driver_stop(driver); for (int i = 0; i < driver->plugin_count; i++) @@ -299,6 +304,8 @@ void plugin_driver_destroy(plugin_driver_t *driver) python_plugin_cleanup(plugin); } } + + PyGILState_Release(gstate); PyEval_RestoreThread(main_tstate); Py_FinalizeEx(); pthread_mutex_destroy(&driver->buffer_mutex); diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index 5dea32d7..001c9be9 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -186,6 +186,12 @@ int main() sleep(1); } + // Cleanup plugin driver system + if (plugin_driver) + { + plugin_driver_destroy(plugin_driver); + } + // Join threads and cleanup plc_state = PLC_STATE_STOPPED; log_info("PLC State: STOPPED"); @@ -194,11 +200,5 @@ int main() pthread_join(plc_thread, NULL); plugin_manager_destroy(plc_program); - // Cleanup plugin driver system - if (plugin_driver) - { - plugin_driver_destroy(plugin_driver); - } - return 0; } From 32fc0ec0f9e727d662133e19eb33f8f00328a403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 13:33:42 +0200 Subject: [PATCH 16/44] Refactor plugin loop to run in a separate thread The plugin's main loop now runs in a background thread using Python's threading module. Added a stop event to allow graceful termination of the loop in stop_loop, improving plugin lifecycle management and preventing blocking the main thread. --- core/src/drivers/example_python_plugin.py | 35 ++++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/core/src/drivers/example_python_plugin.py b/core/src/drivers/example_python_plugin.py index 733c908e..5794c58f 100644 --- a/core/src/drivers/example_python_plugin.py +++ b/core/src/drivers/example_python_plugin.py @@ -7,6 +7,7 @@ import time import ctypes from ctypes import * +import threading # Import the correct type definitions from python_plugin_types import ( @@ -20,6 +21,8 @@ _initialized = False _runtime_args = None _safe_buffer_access = None +_mainthread = None +_stop = threading.Event() def init(runtime_args_capsule): """ @@ -29,7 +32,7 @@ def init(runtime_args_capsule): Args: runtime_args_capsule: PyCapsule containing plugin_runtime_args_t structure """ - global _initialized, _runtime_args, _safe_buffer_access, _runtime_args_capsule + global _initialized, _runtime_args, _safe_buffer_access print("Python plugin 'example_plugin' initializing...") @@ -79,16 +82,19 @@ def start_loop(): Called when the plugin loop should start Optional function - not all plugins need this """ + def loop(): + global _runtime_args, _stop + print("Plugin start_loop called") + while not _stop.is_set(): + time.sleep(0.1) + addr = ctypes.addressof(_runtime_args.bool_output[0][0]) + value, msg = _safe_buffer_access.safe_read_bool_output(0,0) + print(f"Value at address 0x{addr:x}: {value} ({msg})") - global _runtime_args - print("Plugin start_loop called") - while True: - time.sleep(0.1) - addr = ctypes.addressof(_runtime_args.bool_output[0][0]) - internal_safe_buffer_access = SafeBufferAccess(_runtime_args) - value, msg = internal_safe_buffer_access.safe_read_bool_output(0,0) - print(f"Value at address 0x{addr:x}: {value} ({msg})") - pass + global _mainthread + _mainthread = threading.Thread(target=loop, daemon=True) + _mainthread.start() + return 0 def stop_loop(): """ @@ -96,7 +102,14 @@ def stop_loop(): Optional function - not all plugins need this """ print("Plugin stop_loop called") - pass + global _mainthread + if _mainthread is not None: + print("Stopping main thread...") + # In a real implementation, you would signal the thread to stop gracefully + _stop.set() + _mainthread.join() + _mainthread = None + print("✓ Main thread stopped") def cleanup(): """ From 993cfef0638c695cac311f410bb838d3d8e7af35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 13:34:49 +0200 Subject: [PATCH 17/44] Refactor Modbus plugin for safer buffer access Replaces manual ctypes structure and buffer access with type-safe wrappers from python_plugin_types. Updates OpenPLCModbusDataBlock to use SafeBufferAccess for reading and writing coil values, improving safety and error handling. Refactors plugin initialization and server startup for better diagnostics and reliability. --- core/src/drivers/simple_modbus.py | 357 +++++++++++++++++------------- 1 file changed, 202 insertions(+), 155 deletions(-) diff --git a/core/src/drivers/simple_modbus.py b/core/src/drivers/simple_modbus.py index 02916b45..71c77a96 100644 --- a/core/src/drivers/simple_modbus.py +++ b/core/src/drivers/simple_modbus.py @@ -1,6 +1,5 @@ import asyncio import ctypes -from ctypes import POINTER, c_bool, c_ubyte, c_uint16, c_uint32, c_uint64, c_int, c_void_p, CFUNCTYPE import threading import time from pymodbus.server import StartAsyncTcpServer, ServerStop @@ -10,147 +9,83 @@ ModbusServerContext, ) -class PluginRuntimeArgs(ctypes.Structure): - """Python ctypes structure matching plugin_runtime_args_t""" - _fields_ = [ - # Buffer arrays (using POINTER type for arrays) - ("bool_input", POINTER(POINTER(c_bool * 8))), # bool_input[BUFFER_SIZE][8] - ("bool_output", POINTER(POINTER(c_bool * 8))), # bool_output[BUFFER_SIZE][8] - ("byte_input", POINTER(POINTER(c_ubyte))), # byte_input[BUFFER_SIZE] - ("byte_output", POINTER(POINTER(c_ubyte))), # byte_output[BUFFER_SIZE] - ("int_input", POINTER(POINTER(c_uint16))), # int_input[BUFFER_SIZE] - ("int_output", POINTER(POINTER(c_uint16))), # int_output[BUFFER_SIZE] - ("dint_input", POINTER(POINTER(c_uint32))), # dint_input[BUFFER_SIZE] - ("dint_output", POINTER(POINTER(c_uint32))), # dint_output[BUFFER_SIZE] - ("lint_input", POINTER(POINTER(c_uint64))), # lint_input[BUFFER_SIZE] - ("lint_output", POINTER(POINTER(c_uint64))), # lint_output[BUFFER_SIZE] - ("int_memory", POINTER(POINTER(c_uint16))), # int_memory[BUFFER_SIZE] - ("dint_memory", POINTER(POINTER(c_uint32))), # dint_memory[BUFFER_SIZE] - ("lint_memory", POINTER(POINTER(c_uint64))), # lint_memory[BUFFER_SIZE] - - # Mutex function pointers - ("mutex_take", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_take)(pthread_mutex_t*) - ("mutex_give", CFUNCTYPE(c_int, c_void_p)), # int (*mutex_give)(pthread_mutex_t*) - ("buffer_mutex", c_void_p), # pthread_mutex_t *buffer_mutex - - # Buffer size information - ("buffer_size", c_int), # int buffer_size - ("bits_per_buffer", c_int), # int bits_per_buffer - ] +# Import the correct type definitions +from python_plugin_types import ( + PluginRuntimeArgs, + safe_extract_runtime_args_from_capsule, + SafeBufferAccess, + PluginStructureValidator +) class OpenPLCModbusDataBlock(ModbusSparseDataBlock): - """Custom Modbus data block that mirrors OpenPLC bool_output""" + """Custom Modbus data block that mirrors OpenPLC bool_output using SafeBufferAccess""" def __init__(self, runtime_args, buffer_index=0, num_coils=64): self.runtime_args = runtime_args self.buffer_index = buffer_index self.num_coils = num_coils + # Create safe buffer access wrapper + self.safe_buffer_access = SafeBufferAccess(runtime_args) + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Warning: Failed to create safe buffer access: {self.safe_buffer_access.error_msg}") + # Initialize with zeros super().__init__([0] * num_coils) def getValues(self, address, count=1): - """Get coil values from OpenPLC bool_output""" - try: - # Take mutex before reading - if (hasattr(self.runtime_args, 'mutex_take') and - hasattr(self.runtime_args, 'buffer_mutex') and - self.runtime_args.mutex_take and - self.runtime_args.buffer_mutex): - self.runtime_args.mutex_take(self.runtime_args.buffer_mutex) + """Get coil values from OpenPLC bool_output using SafeBufferAccess""" + print(f"[MODBUS] getValues called: address={address}, count={count}") + address = address - 1 + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return [0] * count + + values = [] + for i in range(count): + coil_addr = address + i - values = [] - for i in range(count): - coil_addr = address + i - - # Check if we have valid runtime args and buffer - if (hasattr(self.runtime_args, 'bool_output') and - self.runtime_args.bool_output and - coil_addr < self.num_coils and - self.buffer_index < getattr(self.runtime_args, 'buffer_size', 1)): - - try: - # Extract bit from bool_output[buffer_index][byte_index] - byte_index = coil_addr // 8 - bit_index = coil_addr % 8 - - if byte_index < 8: # 8 bytes per buffer - bool_array = self.runtime_args.bool_output[self.buffer_index] - # Get the boolean value directly - if bool_array and len(bool_array) > byte_index: - bit_value = bool(bool_array[byte_index].value if hasattr(bool_array[byte_index], 'value') else bool_array[byte_index]) - values.append(1 if bit_value else 0) - else: - values.append(0) - else: - values.append(0) - except (IndexError, AttributeError, OSError): + # Use SafeBufferAccess to safely read the boolean value + if coil_addr < self.num_coils: + # Map coil address to buffer and bit indices + # For now, use buffer_index and coil_addr as bit_idx + if coil_addr < 8: # 8 boolean values per buffer + value, error_msg = self.safe_buffer_access.safe_read_bool_output(self.buffer_index, coil_addr) + if error_msg == "Success": + values.append(1 if value else 0) + print(f"[MODBUS] Read coil {coil_addr}: {value}") + else: + print(f"[MODBUS] Error reading coil {coil_addr}: {error_msg}") values.append(0) else: values.append(0) - - return values - - except Exception as e: - # In case of any error, return zeros - return [0] * count - finally: - # Release mutex - if (hasattr(self.runtime_args, 'mutex_give') and - hasattr(self.runtime_args, 'buffer_mutex') and - self.runtime_args.mutex_give and - self.runtime_args.buffer_mutex): - try: - self.runtime_args.mutex_give(self.runtime_args.buffer_mutex) - except: - pass + else: + values.append(0) + + return values def setValues(self, address, values): - """Set coil values to OpenPLC bool_output""" - try: - # Take mutex before writing - if (hasattr(self.runtime_args, 'mutex_take') and - hasattr(self.runtime_args, 'buffer_mutex') and - self.runtime_args.mutex_take and - self.runtime_args.buffer_mutex): - self.runtime_args.mutex_take(self.runtime_args.buffer_mutex) - - for i, value in enumerate(values): - coil_addr = address + i - - # Check if we have valid runtime args and buffer - if (hasattr(self.runtime_args, 'bool_output') and - self.runtime_args.bool_output and - coil_addr < self.num_coils and - self.buffer_index < getattr(self.runtime_args, 'buffer_size', 1)): - - try: - # Set bit in bool_output[buffer_index][byte_index] - byte_index = coil_addr // 8 - - if byte_index < 8: # 8 bytes per buffer - bool_array = self.runtime_args.bool_output[self.buffer_index] - if bool_array and len(bool_array) > byte_index: - # Set the boolean value directly - if hasattr(bool_array[byte_index], 'value'): - bool_array[byte_index].value = bool(value) - else: - bool_array[byte_index] = bool(value) - except (IndexError, AttributeError, OSError): - pass # Ignore errors in setting values + """Set coil values to OpenPLC bool_output using SafeBufferAccess""" + print(f"[MODBUS] setValues called: address={address}, values={values}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return + + for i, value in enumerate(values): + coil_addr = address + i - except Exception: - pass # Ignore any errors - finally: - # Release mutex - if (hasattr(self.runtime_args, 'mutex_give') and - hasattr(self.runtime_args, 'buffer_mutex') and - self.runtime_args.mutex_give and - self.runtime_args.buffer_mutex): - try: - self.runtime_args.mutex_give(self.runtime_args.buffer_mutex) - except: - pass + # Use SafeBufferAccess to safely write the boolean value + if coil_addr < self.num_coils: + # Map coil address to buffer and bit indices + # For now, use buffer_index and coil_addr as bit_idx + if coil_addr < 8: # 8 boolean values per buffer + success, error_msg = self.safe_buffer_access.safe_write_bool_output(self.buffer_index, coil_addr, bool(value)) + if error_msg == "Success": + print(f"[MODBUS] Set coil {coil_addr}: {bool(value)}") + else: + print(f"[MODBUS] Error setting coil {coil_addr}: {error_msg}") # Global variables for plugin lifecycle server_task = None @@ -158,49 +93,147 @@ def setValues(self, address, values): runtime_args = None update_thread = None running = False +gIp = "172.29.65.104" +gPort = 5020 -def init(args, host="127.0.0.1", port=5020): +def init(args_capsule, host="172.29.65.104", port=5020): """Initialize the Modbus plugin""" - global runtime_args, server_context - - runtime_args = args - - # Create OpenPLC-connected coils data block - coils_block = OpenPLCModbusDataBlock(runtime_args, buffer_index=0, num_coils=64) - - # Standard data blocks for other Modbus types - di = ModbusSparseDataBlock([0] * 64) # Discrete Inputs - ir = ModbusSparseDataBlock([0] * 32) # Input Registers (16-bit) - hr = ModbusSparseDataBlock([0] * 32) # Holding Registers (16-bit) + global runtime_args, server_context, gIp, gPort + gIp = host + gPort = port - # Create device context with OpenPLC-connected coils - device = ModbusDeviceContext(di=di, co=coils_block, ir=ir, hr=hr) - server_context = ModbusServerContext(devices={1: device}, single=False) + print("[MODBUS] Python plugin 'simple_modbus' initializing...") - print(f"[MODBUS] Plugin initialized - Host: {host}, Port: {port}") - return True + try: + # Print structure validation info for debugging + print("[MODBUS] Validating plugin structure alignment...") + PluginStructureValidator.print_structure_info() + + # Extract runtime args from capsule using safe method + if hasattr(args_capsule, '__class__') and 'PyCapsule' in str(type(args_capsule)): + # This is a PyCapsule from C - use safe extraction + runtime_args, error_msg = safe_extract_runtime_args_from_capsule(args_capsule) + if runtime_args is None: + print(f"[MODBUS] ✗ Failed to extract runtime args: {error_msg}") + return False + + print(f"[MODBUS] ✓ Runtime arguments extracted successfully") + else: + # This is a direct object (for testing) + runtime_args = args_capsule + print(f"[MODBUS] ✓ Using direct runtime args for testing") + + # Safely access buffer size using validation + buffer_size, size_error = runtime_args.safe_access_buffer_size() + if buffer_size == -1: + print(f"[MODBUS] ✗ Failed to access buffer size: {size_error}") + return False + + print(f"[MODBUS] Buffer size: {buffer_size}") + print(f"[MODBUS] Bits per buffer: {runtime_args.bits_per_buffer}") + print(f"[MODBUS] Structure details: {runtime_args}") + + # Create OpenPLC-connected coils data block + coils_block = OpenPLCModbusDataBlock(runtime_args, buffer_index=0, num_coils=64) + + # Standard data blocks for other Modbus types + di = ModbusSparseDataBlock([0] * 64) # Discrete Inputs + ir = ModbusSparseDataBlock([0] * 32) # Input Registers (16-bit) + hr = ModbusSparseDataBlock([0] * 32) # Holding Registers (16-bit) + + # Create device context with OpenPLC-connected coils + print(f"[MODBUS] coils_block created with {coils_block} coils") + device = ModbusDeviceContext(di=di, co=coils_block, ir=ir, hr=hr) + server_context = ModbusServerContext(devices={1: device}, single=False) + + print(f"[MODBUS] ✓ Plugin initialized successfully - Host: {host}, Port: {port}") + return True + + except Exception as e: + print(f"[MODBUS] ✗ Plugin initialization failed: {e}") + import traceback + traceback.print_exc() + return False def start_loop(): """Start the Modbus server""" - global server_task, running, update_thread + global server_task, running, update_thread, gIp, gPort if server_context is None: print("[MODBUS] Error: Plugin not initialized") return False + print("[MODBUS] Server context is valid, proceeding with startup...") + print(f"[MODBUS] Server context created successfully") + running = True - # Start server in separate thread + # Start server in separate thread with proper asyncio handling def run_server(): - asyncio.run(StartAsyncTcpServer( - context=server_context, - address=("172.29.65.104", 5020) - )) + try: + print("[MODBUS] Creating new event loop for server thread...") + # Create new event loop for this thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + print("[MODBUS] Event loop created successfully") + + # Start the server and keep it running + async def start_server(): + try: + print(f"[MODBUS] Attempting to start TCP server on {gIp}:{gPort}...") + try: + server = await StartAsyncTcpServer( + context=server_context, + address=(gIp, gPort) + ) + print(f"[MODBUS] Server successfully bound to {gIp}:{gPort}") + except Exception as bind_error: + print(f"[MODBUS] Failed to bind to {gIp}:{gPort}: {bind_error}") + print(f"[MODBUS] Attempting to bind to 0.0.0.0:{gPort} as fallback...") + server = await StartAsyncTcpServer( + context=server_context, + address=("0.0.0.0", gPort) + ) + print(f"[MODBUS] Server successfully bound to 0.0.0.0:{gPort} (fallback)") + + # Keep the server running + try: + print("[MODBUS] Server is now running and accepting connections") + while running: + await asyncio.sleep(1) + except asyncio.CancelledError: + print("[MODBUS] Server cancelled") + finally: + print("[MODBUS] Shutting down server...") + if hasattr(server, 'close'): + server.close() + if hasattr(server, 'wait_closed'): + await server.wait_closed() + print("[MODBUS] Server shutdown complete") + + except Exception as server_error: + print(f"[MODBUS] Error in start_server async function: {server_error}") + import traceback + print(f"[MODBUS] Traceback: {traceback.format_exc()}") + raise + + # Run the server + print("[MODBUS] Running server event loop...") + loop.run_until_complete(start_server()) + + except Exception as e: + print(f"[MODBUS] Error in run_server thread: {e}") + import traceback + print(f"[MODBUS] Full traceback: {traceback.format_exc()}") + finally: + print("[MODBUS] Closing event loop...") + loop.close() + print("[MODBUS] Event loop closed") - server_task = threading.Thread(target=run_server, daemon=True) + server_task = threading.Thread(target=run_server, daemon=False) server_task.start() - - print("[MODBUS] Server started on 172.29.65.104:5020") + + print(f"[MODBUS] Server thread started on {gIp}:{gPort}") return True def stop_loop(): @@ -238,24 +271,38 @@ def cleanup(): async def main(): """Standalone server for testing""" - # Mock runtime args for testing + # Create a proper mock runtime args that inherits from PluginRuntimeArgs + import ctypes + + # Create a mock that has the required methods class MockArgs: def __init__(self): self.buffer_size = 1 - self.bits_per_buffer = 64 + self.bits_per_buffer = 8 # Create simple boolean list for testing self.bool_data = [[False] * 8] # 1 buffer, 8 booleans self.bool_output = self.bool_data # Simple reference self.mutex_take = None self.mutex_give = None self.buffer_mutex = None + + def safe_access_buffer_size(self): + """Mock implementation of safe_access_buffer_size""" + return self.buffer_size, "Success" + + def validate_pointers(self): + """Mock implementation of validate_pointers""" + return True, "Mock validation passed" + + def __str__(self): + return f"MockArgs(buffer_size={self.buffer_size}, bits_per_buffer={self.bits_per_buffer})" mock_args = MockArgs() # Initialize and start if init(mock_args): - if start(): - print("Modbus server running on 172.29.65.104:5020") + if start_loop(): + print(f"Modbus server running on {gIp}:{gPort}") print("Press Ctrl+C to stop...") try: @@ -264,7 +311,7 @@ def __init__(self): await asyncio.sleep(1) except KeyboardInterrupt: print("\nStopping server...") - stop() + stop_loop() cleanup() else: print("Failed to start server") @@ -272,4 +319,4 @@ def __init__(self): print("Failed to initialize plugin") if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 763b105304be332d3f4492ba00d61ea0bf3a322c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 14:11:18 +0200 Subject: [PATCH 18/44] Update Python plugin documentation and type safety Revised README to clarify plugin type values, enhance Python plugin type safety, and document usage of the new python_plugin_types.py module. Added examples for thread-safe buffer access, advanced Modbus implementation, and improved plugin initialization with structure validation and error handling. --- core/src/drivers/README.md | 188 +++++++++++++++++++++++++++++++------ 1 file changed, 161 insertions(+), 27 deletions(-) diff --git a/core/src/drivers/README.md b/core/src/drivers/README.md index a4ab678a..65f4b746 100644 --- a/core/src/drivers/README.md +++ b/core/src/drivers/README.md @@ -22,15 +22,16 @@ core/src/drivers/ ### Plugin Types -1. **Native C Plugins** (`PLUGIN_TYPE_NATIVE`) - - Compiled shared libraries (.so files) - - Direct C function calls - - Maximum performance - -2. **Python Plugins** (`PLUGIN_TYPE_PYTHON`) +1. **Python Plugins** (`PLUGIN_TYPE_PYTHON = 0`) - Python scripts (.py files) - Embedded Python interpreter - Easier development and debugging + - Enhanced type safety with `python_plugin_types.py` + +2. **Native C Plugins** (`PLUGIN_TYPE_NATIVE = 1`) + - Compiled shared libraries (.so files) + - Direct C function calls + - Maximum performance ## Plugin Interface @@ -111,13 +112,38 @@ typedef struct { ## Thread-Safe Buffer Access -### Python Example +### Enhanced Python SafeBufferAccess + +The `python_plugin_types.py` module provides a `SafeBufferAccess` wrapper class for safe buffer operations: + +```python +from python_plugin_types import SafeBufferAccess + +# Create safe buffer access wrapper +safe_buffer_access = SafeBufferAccess(runtime_args) + +# Safe read operation +value, error_msg = safe_buffer_access.safe_read_bool_output(buffer_idx, bit_idx) +if error_msg == "Success": + print(f"Read value: {value}") +else: + print(f"Read error: {error_msg}") + +# Safe write operation +success, error_msg = safe_buffer_access.safe_write_bool_output(buffer_idx, bit_idx, True) +if error_msg == "Success": + print("Write successful") +else: + print(f"Write error: {error_msg}") +``` + +### Manual Python Example (Legacy) ```python def safe_read_output(runtime_args, buffer_idx, bit_pos): """Safely read a boolean output""" try: if runtime_args.mutex_take(runtime_args.buffer_mutex) == 0: - value = runtime_args.bool_output[buffer_idx][bit_pos] + value = runtime_args.bool_output[buffer_idx][bit_pos].contents.value return bool(value) finally: runtime_args.mutex_give(runtime_args.buffer_mutex) @@ -127,7 +153,7 @@ def safe_write_output(runtime_args, buffer_idx, bit_pos, value): """Safely write a boolean output""" try: if runtime_args.mutex_take(runtime_args.buffer_mutex) == 0: - runtime_args.bool_output[buffer_idx][bit_pos] = bool(value) + runtime_args.bool_output[buffer_idx][bit_pos].contents.value = 1 if value else 0 return True finally: runtime_args.mutex_give(runtime_args.buffer_mutex) @@ -140,17 +166,19 @@ def safe_write_output(runtime_args, buffer_idx, bit_pos, value): ``` # Format: name,path,enabled,type,plugin_related_config_path -example_plugin1,./plugins/example1.so,1,0,./config/example1.conf -python_plugin,./plugins/modbus_slave.py,1,1,./config/modbus.ini +python_plugin,../core/src/drivers/example_python_plugin.py,1,0,./modbus_slave_config.ini +native_plugin,./plugins/example1.so,1,1,./config/example1.conf ``` **Fields:** - `name`: Plugin identifier -- `path`: Path to plugin file (.so for native, .py for Python) +- `path`: Path to plugin file (.py for Python, .so for native) - `enabled`: 1 = enabled, 0 = disabled -- `type`: 0 = native C, 1 = Python +- `type`: 0 = Python (`PLUGIN_TYPE_PYTHON`), 1 = Native C (`PLUGIN_TYPE_NATIVE`) - `plugin_related_config_path`: Path to plugin-specific configuration +**Note:** The type values are: 0 for Python plugins, 1 for Native C plugins (opposite of what might be expected). + ### Loading Configuration ```c plugin_driver_t *driver = plugin_driver_create(); @@ -200,9 +228,81 @@ if init(runtime_args_capsule): start_loop() # Starts server on configured port ``` -### 3. Synchronous Modbus Implementation +### 3. Advanced Modbus Implementation + +The `simple_modbus.py` provides an advanced asynchronous Modbus TCP server implementation: + +**Features:** +- Uses pymodbus for full Modbus protocol support +- Asynchronous operation with asyncio +- Custom OpenPLCModbusDataBlock that directly mirrors OpenPLC buffers +- Enhanced error handling and debugging +- Configurable host/port settings +- Thread-safe buffer access using SafeBufferAccess -The `simple_modbus_sync.py` provides a lightweight synchronous Modbus TCP server using Python's built-in `socketserver` module. +**Usage:** +```python +# Can be run standalone for testing +python3 simple_modbus.py + +# Or loaded as a plugin via configuration +``` + +## Python Plugin Type System + +### Enhanced Type Safety with `python_plugin_types.py` + +The `python_plugin_types.py` module provides comprehensive type definitions and safety utilities for Python plugins: + +#### Key Components + +1. **PluginRuntimeArgs Structure** + - Exact ctypes mapping of the C `plugin_runtime_args_t` structure + - Built-in validation methods + - Safe buffer size access + +2. **SafeBufferAccess Wrapper** + - Thread-safe buffer operations + - Automatic mutex handling + - Error reporting and validation + +3. **PluginStructureValidator** + - Structure alignment validation + - Debugging information + - Platform compatibility checks + +4. **Safe Capsule Extraction** + - Robust PyCapsule handling + - Comprehensive error checking + - Memory safety validation + +#### Usage Example +```python +from python_plugin_types import ( + PluginRuntimeArgs, + safe_extract_runtime_args_from_capsule, + SafeBufferAccess, + PluginStructureValidator +) + +def init(runtime_args_capsule): + # Validate structure for debugging + PluginStructureValidator.print_structure_info() + + # Safe extraction with error handling + runtime_args, error_msg = safe_extract_runtime_args_from_capsule(runtime_args_capsule) + if runtime_args is None: + print(f"Extraction failed: {error_msg}") + return False + + # Create safe buffer access + safe_access = SafeBufferAccess(runtime_args) + if not safe_access.is_valid: + print(f"Buffer access failed: {safe_access.error_msg}") + return False + + return True +``` ## Development Guide @@ -214,18 +314,49 @@ The `simple_modbus_sync.py` provides a lightweight synchronous Modbus TCP server import ctypes from ctypes import * -# Define runtime args structure (copy from examples) -class PluginRuntimeArgs(ctypes.Structure): - # ... (see example files for complete structure) +# Import the enhanced type definitions +from python_plugin_types import ( + PluginRuntimeArgs, + safe_extract_runtime_args_from_capsule, + SafeBufferAccess, + PluginStructureValidator +) def init(runtime_args_capsule): """Initialize your plugin""" - # Extract runtime args from capsule - runtime_args = extract_runtime_args_from_capsule(runtime_args_capsule) + global _runtime_args, _safe_buffer_access - # Initialize your plugin logic - print("Plugin initialized") - return True + print("Plugin initializing...") + + try: + # Validate structure alignment for debugging + PluginStructureValidator.print_structure_info() + + # Extract runtime args from capsule using safe method + runtime_args, error_msg = safe_extract_runtime_args_from_capsule(runtime_args_capsule) + if runtime_args is None: + print(f"Failed to extract runtime args: {error_msg}") + return False + + # Validate buffer size + buffer_size, size_error = runtime_args.safe_access_buffer_size() + if buffer_size == -1: + print(f"Failed to access buffer size: {size_error}") + return False + + # Create safe buffer access wrapper + _safe_buffer_access = SafeBufferAccess(runtime_args) + if not _safe_buffer_access.is_valid: + print(f"Failed to create safe buffer access: {_safe_buffer_access.error_msg}") + return False + + _runtime_args = runtime_args + print("Plugin initialized successfully") + return True + + except Exception as e: + print(f"Plugin initialization failed: {e}") + return False def start_loop(): """Start plugin operations""" @@ -238,7 +369,7 @@ def cleanup(): 2. **Add to configuration:** ``` -my_plugin,/path/to/my_plugin.py,1,1,/path/to/config.ini +my_plugin,/path/to/my_plugin.py,1,0,/path/to/config.ini ``` 3. **Test plugin:** @@ -413,7 +544,10 @@ When contributing new plugins: ## See Also -- `example_python_plugin.py` - Basic plugin template -- `modbus_slave.py` - Complete Modbus TCP slave implementation -- `plugin_config_example.txt` - Configuration file format +- `example_python_plugin.py` - Basic plugin template with enhanced type safety +- `simple_modbus.py` - Advanced asynchronous Modbus TCP slave implementation +- `python_plugin_types.py` - Enhanced type definitions and safety utilities +- `plugin_config_example.txt` - Configuration file format examples +- `plugins.conf` - Active plugin configuration file +- `modbus_slave_config.ini` - Modbus plugin configuration example - OpenPLC Runtime documentation From d447cc1e072c7cc73356722e574bd106fcf0908f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 14:35:18 +0200 Subject: [PATCH 19/44] moving examples and plugins to respective folder --- core/src/drivers/{ => examples}/example_python_plugin.py | 0 core/src/drivers/{ => plugins/python}/simple_modbus.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename core/src/drivers/{ => examples}/example_python_plugin.py (100%) rename core/src/drivers/{ => plugins/python}/simple_modbus.py (100%) diff --git a/core/src/drivers/example_python_plugin.py b/core/src/drivers/examples/example_python_plugin.py similarity index 100% rename from core/src/drivers/example_python_plugin.py rename to core/src/drivers/examples/example_python_plugin.py diff --git a/core/src/drivers/simple_modbus.py b/core/src/drivers/plugins/python/simple_modbus.py similarity index 100% rename from core/src/drivers/simple_modbus.py rename to core/src/drivers/plugins/python/simple_modbus.py From 85ac4cbcf1d87dffc53505635de815a1e199f149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 14:41:12 +0200 Subject: [PATCH 20/44] deleting unused plugins from config --- plugins.conf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins.conf b/plugins.conf index d84d0809..8dc81b41 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,6 +1,4 @@ # Plugin configuration file # Format: name,path,enabled,type,config_path -#python_plugin,../core/src/drivers/modbus_slave.py,1,0,./modbus_slave_config.ini -#python_plugin,../core/src/drivers/simple_modbus.py,1,0,./modbus_slave_config.ini -#python_plugin,../core/src/drivers/simple_modbus_sync.py,1,0,./modbus_slave_config.ini -python_plugin,../core/src/drivers/example_python_plugin.py,1,0,./modbus_slave_config.ini \ No newline at end of file +# modbus_slave,../core/src/drivers/plugins/python/simple_modbus.py,1,0,./modbus_slave_config.ini +example_plugin,../core/src/drivers/examples/example_python_plugin.py,1,0,./modbus_slave_config.ini From 79bb1b6ab68f17ab4a610bd0c45cba71d8f60efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Wed, 17 Sep 2025 14:54:17 +0200 Subject: [PATCH 21/44] Expose plugin mutex helpers and use in PLC cycle Made plugin_mutex_take and plugin_mutex_give functions public in plugin_driver.h and used them to protect buffer access in the PLC cycle thread. Also refactored plugin_driver variable to be global in plc_main.c for thread safety. --- core/src/drivers/plugin_driver.c | 4 ++-- core/src/drivers/plugin_driver.h | 2 ++ core/src/plc_app/plc_main.c | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index aa4138be..85915770 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -49,12 +49,12 @@ plugin_driver_t *plugin_driver_create(void) } // Mutex helper functions for plugins -static int plugin_mutex_take(pthread_mutex_t *mutex) +int plugin_mutex_take(pthread_mutex_t *mutex) { return pthread_mutex_lock(mutex); } -static int plugin_mutex_give(pthread_mutex_t *mutex) +int plugin_mutex_give(pthread_mutex_t *mutex) { return pthread_mutex_unlock(mutex); } diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index 76d93d26..6893f25c 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -87,6 +87,8 @@ int plugin_driver_init(plugin_driver_t *driver); int plugin_driver_start(plugin_driver_t *driver); int plugin_driver_stop(plugin_driver_t *driver); void plugin_driver_destroy(plugin_driver_t *driver); +int plugin_mutex_take(pthread_mutex_t *mutex); +int plugin_mutex_give(pthread_mutex_t *mutex); // Python plugin functions int python_plugin_get_symbols(plugin_instance_t *plugin); diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index 001c9be9..d963bccd 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -23,7 +23,8 @@ extern plc_timing_stats_t plc_timing_stats; volatile sig_atomic_t keep_running = 1; struct timespec timer_start; pthread_t plc_thread; -PluginManager *plc_program = NULL; +PluginManager *plc_program = NULL; +plugin_driver_t *plugin_driver = NULL; void handle_sigint(int sig) { @@ -85,6 +86,7 @@ void *plc_cycle_thread(void *arg) while (plc_state == PLC_STATE_RUNNING) { scan_cycle_time_start(); + plugin_mutex_take(&plugin_driver->buffer_mutex); // Execute the PLC cycle ext_config_run__(tick__++); @@ -93,6 +95,7 @@ void *plc_cycle_thread(void *arg) // Update Watchdog Heartbeat atomic_store(&plc_heartbeat, time(NULL)); + plugin_mutex_give(&plugin_driver->buffer_mutex); scan_cycle_time_end(); // Calculate next start time @@ -151,7 +154,7 @@ int main() } // Initialize plugin driver system - plugin_driver_t *plugin_driver = plugin_driver_create(); + plugin_driver = plugin_driver_create(); if (plugin_driver) { // Load plugin configuration From e5cfc77924a743846e516ed81c9364c1f99324f3 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Thu, 18 Sep 2025 14:03:37 +0200 Subject: [PATCH 22/44] Changing pluggins paths to meet exec.sh start path --- .../drivers/examples/python_plugin_types.py | 318 ++++++++++++++++++ core/src/drivers/plugin_config_example.txt | 6 +- .../python}/modbus_slave_config.ini | 0 core/src/plc_app/plc_main.c | 3 +- plugins.conf | 2 +- 5 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 core/src/drivers/examples/python_plugin_types.py rename core/src/drivers/{ => plugins/python}/modbus_slave_config.ini (100%) diff --git a/core/src/drivers/examples/python_plugin_types.py b/core/src/drivers/examples/python_plugin_types.py new file mode 100644 index 00000000..f9275bc6 --- /dev/null +++ b/core/src/drivers/examples/python_plugin_types.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Shared type definitions for OpenPLC Python plugins +This module provides correct ctypes mappings for the plugin_runtime_args_t structure +""" + +import ctypes +from ctypes import * +import sys + +# IEC type mappings based on iec_types.h +# These must match exactly with the C definitions +IEC_BOOL = ctypes.c_uint8 # typedef uint8_t IEC_BOOL; +IEC_BYTE = ctypes.c_uint8 # typedef uint8_t IEC_BYTE; +IEC_UINT = ctypes.c_uint16 # typedef uint16_t IEC_UINT; +IEC_UDINT = ctypes.c_uint32 # typedef uint32_t IEC_UDINT; +IEC_ULINT = ctypes.c_uint64 # typedef uint64_t IEC_ULINT; + +class PluginRuntimeArgs(ctypes.Structure): + """ + Python ctypes structure matching plugin_runtime_args_t from plugin_driver.h + + CRITICAL: This structure must match the C definition exactly to prevent + segmentation faults and memory corruption. + """ + _fields_ = [ + # Buffer arrays - these are pointers to arrays of pointers + # C: IEC_BOOL *(*bool_input)[8] means pointer to array of 8 pointers + ("bool_input", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), + ("bool_output", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), + ("byte_input", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), + ("byte_output", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), + ("int_input", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("int_output", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("dint_input", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("dint_output", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("lint_input", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + ("lint_output", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + ("int_memory", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("dint_memory", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("lint_memory", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + + # Mutex function pointers + ("mutex_take", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), + ("mutex_give", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), + ("buffer_mutex", ctypes.c_void_p), + + # Buffer size information + ("buffer_size", ctypes.c_int), + ("bits_per_buffer", ctypes.c_int), + ] + + def validate_pointers(self): + """ + Validate that critical pointers are not NULL + Returns: (bool, str) - (is_valid, error_message) + """ + try: + # Check buffer mutex + if not self.buffer_mutex: + return False, "buffer_mutex is NULL" + + # Check mutex functions + if not self.mutex_take: + return False, "mutex_take function pointer is NULL" + if not self.mutex_give: + return False, "mutex_give function pointer is NULL" + + # Check buffer size is reasonable + if self.buffer_size <= 0 or self.buffer_size > 10000: + return False, f"buffer_size is invalid: {self.buffer_size}" + + if self.bits_per_buffer <= 0 or self.bits_per_buffer > 64: + return False, f"bits_per_buffer is invalid: {self.bits_per_buffer}" + + return True, "All pointers valid" + + except Exception as e: + return False, f"Exception during validation: {e}" + + def safe_access_buffer_size(self): + """ + Safely access buffer_size with validation + Returns: (int, str) - (buffer_size, error_message) + """ + try: + is_valid, msg = self.validate_pointers() + if not is_valid: + return -1, f"Validation failed: {msg}" + + size = self.buffer_size + if size <= 0 or size > 10000: + return -1, f"Invalid buffer size: {size}" + + return size, "Success" + + except Exception as e: + return -1, f"Exception accessing buffer_size: {e}" + + def __str__(self): + """Debug representation of the structure""" + try: + return (f"PluginRuntimeArgs(\n" + f" bool_input=0x{ctypes.addressof(self.bool_input.contents) if self.bool_input else 0:x},\n" + f" bool_output=0x{ctypes.addressof(self.bool_output.contents) if self.bool_output else 0:x},\n" + f" byte_input=0x{ctypes.addressof(self.byte_input.contents) if self.byte_input else 0:x},\n" + f" byte_output=0x{ctypes.addressof(self.byte_output.contents) if self.byte_output else 0:x},\n" + f" int_input=0x{ctypes.addressof(self.int_input.contents) if self.int_input else 0:x},\n" + f" int_output=0x{ctypes.addressof(self.int_output.contents) if self.int_output else 0:x},\n" + f" dint_input=0x{ctypes.addressof(self.dint_input.contents) if self.dint_input else 0:x},\n" + f" dint_output=0x{ctypes.addressof(self.dint_output.contents) if self.dint_output else 0:x},\n" + f" lint_input=0x{ctypes.addressof(self.lint_input.contents) if self.lint_input else 0:x},\n" + f" lint_output=0x{ctypes.addressof(self.lint_output.contents) if self.lint_output else 0:x},\n" + f" int_memory=0x{ctypes.addressof(self.int_memory.contents) if self.int_memory else 0:x},\n" + f" buffer_size={self.buffer_size},\n" + f" bits_per_buffer={self.bits_per_buffer},\n" + f" buffer_mutex=0x{self.buffer_mutex or 0:x},\n" + f" mutex_take={'valid' if self.mutex_take else 'NULL'},\n" + f" mutex_give={'valid' if self.mutex_give else 'NULL'}\n" + f")") + except: + return "PluginRuntimeArgs(corrupted or invalid)" + +class PluginStructureValidator: + """Validates structure alignment and provides debugging tools""" + + @staticmethod + def validate_structure_alignment(): + """ + Validates that the Python ctypes structure has the expected size and alignment + Returns: (bool, str, dict) - (is_valid, message, debug_info) + """ + try: + # Calculate expected structure size + # This is platform-dependent but we can do basic checks + struct_size = ctypes.sizeof(PluginRuntimeArgs) + + debug_info = { + 'structure_size': struct_size, + 'pointer_size': ctypes.sizeof(ctypes.c_void_p), + 'int_size': ctypes.sizeof(ctypes.c_int), + 'platform': sys.platform, + 'architecture': sys.maxsize > 2**32 and '64-bit' or '32-bit' + } + + # Basic sanity checks + expected_min_size = ( + 13 * ctypes.sizeof(ctypes.c_void_p) + # 13 buffer pointers + 2 * ctypes.sizeof(ctypes.c_void_p) + # 2 function pointers + 1 * ctypes.sizeof(ctypes.c_void_p) + # 1 mutex pointer + 2 * ctypes.sizeof(ctypes.c_int) # 2 integers + ) + + if struct_size < expected_min_size: + return False, f"Structure too small: {struct_size} < {expected_min_size}", debug_info + + # Check field offsets make sense + buffer_size_offset = PluginRuntimeArgs.buffer_size.offset + bits_per_buffer_offset = PluginRuntimeArgs.bits_per_buffer.offset + + if bits_per_buffer_offset <= buffer_size_offset: + return False, "Field offsets are incorrect", debug_info + + debug_info['buffer_size_offset'] = buffer_size_offset + debug_info['bits_per_buffer_offset'] = bits_per_buffer_offset + + return True, "Structure validation passed", debug_info + + except Exception as e: + return False, f"Exception during validation: {e}", {} + + @staticmethod + def print_structure_info(): + """Print detailed structure information for debugging""" + is_valid, msg, debug_info = PluginStructureValidator.validate_structure_alignment() + + print("=== Plugin Structure Validation ===") + print(f"Status: {'VALID' if is_valid else 'INVALID'}") + print(f"Message: {msg}") + print("\nStructure Details:") + for key, value in debug_info.items(): + print(f" {key}: {value}") + + print(f"\nField Offsets:") + try: + for field_name, field_type in PluginRuntimeArgs._fields_: + offset = getattr(PluginRuntimeArgs, field_name).offset + print(f" {field_name}: offset {offset}") + except Exception as e: + print(f" Error getting field offsets: {e}") + +class SafeBufferAccess: + """Wrapper class for safe buffer operations with mutex handling""" + + def __init__(self, runtime_args): + """ + Initialize with validated runtime args + Args: + runtime_args: PluginRuntimeArgs instance + """ + self.args = runtime_args + self.is_valid, self.error_msg = runtime_args.validate_pointers() + + def safe_read_bool_output(self, buffer_idx, bit_idx): + """ + Safely read a boolean output with proper mutex handling + Returns: (bool, str) - (value, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Access the value - read from the actual value, not the pointer + value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) + return value, "Success" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except Exception as e: + return False, f"Exception during buffer access: {e}" + + def safe_write_bool_output(self, buffer_idx, bit_idx, value): + """ + Safely write a boolean output with proper mutex handling + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Set the value - access the actual value, not the pointer + self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 + return True, "Success" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except Exception as e: + return False, f"Exception during buffer access: {e}" + +def safe_extract_runtime_args_from_capsule(capsule): + """ + Enhanced capsule extraction with comprehensive validation + Args: + capsule: PyCapsule containing plugin_runtime_args_t structure + Returns: + (PluginRuntimeArgs, str) - (runtime_args, error_message) + """ + try: + # Validate capsule type + if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': + return None, f"Expected PyCapsule object, got {type(capsule)}" + + # Set up the Python API function signatures + ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] + ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p + + # Get the pointer from the capsule + ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") + if not ptr: + return None, "Failed to extract pointer from capsule - invalid capsule name or corrupted data" + + # Cast the pointer to our structure type + args_ptr = ctypes.cast(ptr, ctypes.POINTER(PluginRuntimeArgs)) + if not args_ptr: + return None, "Failed to cast pointer to PluginRuntimeArgs structure" + + runtime_args = args_ptr.contents + + # Validate the extracted structure + is_valid, validation_msg = runtime_args.validate_pointers() + if not is_valid: + return None, f"Structure validation failed: {validation_msg}" + + return runtime_args, "Success" + + except Exception as e: + return None, f"Exception during capsule extraction: {e}" + +if __name__ == "__main__": + # Self-test when run directly + print("OpenPLC Python Plugin Types - Self Test") + print("=" * 50) + + # Test structure validation + PluginStructureValidator.print_structure_info() + + print(f"\nIEC Type Sizes:") + print(f" IEC_BOOL: {ctypes.sizeof(IEC_BOOL)} bytes") + print(f" IEC_BYTE: {ctypes.sizeof(IEC_BYTE)} bytes") + print(f" IEC_UINT: {ctypes.sizeof(IEC_UINT)} bytes") + print(f" IEC_UDINT: {ctypes.sizeof(IEC_UDINT)} bytes") + print(f" IEC_ULINT: {ctypes.sizeof(IEC_ULINT)} bytes") diff --git a/core/src/drivers/plugin_config_example.txt b/core/src/drivers/plugin_config_example.txt index 36e45679..9fc52c28 100644 --- a/core/src/drivers/plugin_config_example.txt +++ b/core/src/drivers/plugin_config_example.txt @@ -1,6 +1,6 @@ # Plugin configuration file # Format: name,path,enabled,type,plugin_related_config_path # Example plugins -example_plugin1,./plugins/example1.so,1 -example_plugin2,./plugins/example2.so,0 -python_plugin,./plugins/python_bridge.so,1 + +# modbus_slave,./core/src/drivers/plugins/python/simple_modbus.py,1,0,./modbus_slave_config.ini +example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./modbus_slave_config.ini diff --git a/core/src/drivers/modbus_slave_config.ini b/core/src/drivers/plugins/python/modbus_slave_config.ini similarity index 100% rename from core/src/drivers/modbus_slave_config.ini rename to core/src/drivers/plugins/python/modbus_slave_config.ini diff --git a/core/src/plc_app/plc_main.c b/core/src/plc_app/plc_main.c index eb41ccb5..3ebfab67 100644 --- a/core/src/plc_app/plc_main.c +++ b/core/src/plc_app/plc_main.c @@ -86,8 +86,9 @@ int main() plugin_driver = plugin_driver_create(); if (plugin_driver) { + log_info("[PLUGIN]: Plugin driver system created"); // Load plugin configuration - if (plugin_driver_load_config(plugin_driver, "../plugins.conf") == 0) + if (plugin_driver_load_config(plugin_driver, "./plugins.conf") == 0) { // Start plugins plugin_driver_init(plugin_driver); diff --git a/plugins.conf b/plugins.conf index 8dc81b41..0c0df5cd 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,4 +1,4 @@ # Plugin configuration file # Format: name,path,enabled,type,config_path # modbus_slave,../core/src/drivers/plugins/python/simple_modbus.py,1,0,./modbus_slave_config.ini -example_plugin,../core/src/drivers/examples/example_python_plugin.py,1,0,./modbus_slave_config.ini +example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./modbus_slave_config.ini From 469fc7b721c47da9529a52521eb50dab3e22441a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Tue, 23 Sep 2025 15:46:53 +0200 Subject: [PATCH 23/44] Rtop 58 plugin modbus slave (#6) * thread safe and dump access in the same function * adding batch functions that allows multiple reads/writes * Providing wrappers for IS, IR and HR * avoiding magical numbers * avoiding generic exception handling --- .../drivers/examples/python_plugin_types.py | 318 ---- core/src/drivers/plugin_driver.c | 4 +- .../python/examples/buffer_access_example.py | 321 ++++ .../python}/examples/example_python_plugin.py | 9 +- .../modbus_slave_config.ini | 0 .../modbus_slave_plugin/simple_modbus.py | 529 ++++++ .../drivers/plugins/python/shared/__init__.py | 0 .../python/shared/python_plugin_types.py | 1531 +++++++++++++++++ .../drivers/plugins/python/simple_modbus.py | 322 ---- core/src/drivers/python_plugin_types.py | 318 ---- plugins.conf | 4 +- 11 files changed, 2393 insertions(+), 963 deletions(-) delete mode 100644 core/src/drivers/examples/python_plugin_types.py create mode 100644 core/src/drivers/plugins/python/examples/buffer_access_example.py rename core/src/drivers/{ => plugins/python}/examples/example_python_plugin.py (93%) rename core/src/drivers/plugins/python/{ => modbus_slave_plugin}/modbus_slave_config.ini (100%) create mode 100644 core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py create mode 100644 core/src/drivers/plugins/python/shared/__init__.py create mode 100644 core/src/drivers/plugins/python/shared/python_plugin_types.py delete mode 100644 core/src/drivers/plugins/python/simple_modbus.py delete mode 100644 core/src/drivers/python_plugin_types.py diff --git a/core/src/drivers/examples/python_plugin_types.py b/core/src/drivers/examples/python_plugin_types.py deleted file mode 100644 index f9275bc6..00000000 --- a/core/src/drivers/examples/python_plugin_types.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 -""" -Shared type definitions for OpenPLC Python plugins -This module provides correct ctypes mappings for the plugin_runtime_args_t structure -""" - -import ctypes -from ctypes import * -import sys - -# IEC type mappings based on iec_types.h -# These must match exactly with the C definitions -IEC_BOOL = ctypes.c_uint8 # typedef uint8_t IEC_BOOL; -IEC_BYTE = ctypes.c_uint8 # typedef uint8_t IEC_BYTE; -IEC_UINT = ctypes.c_uint16 # typedef uint16_t IEC_UINT; -IEC_UDINT = ctypes.c_uint32 # typedef uint32_t IEC_UDINT; -IEC_ULINT = ctypes.c_uint64 # typedef uint64_t IEC_ULINT; - -class PluginRuntimeArgs(ctypes.Structure): - """ - Python ctypes structure matching plugin_runtime_args_t from plugin_driver.h - - CRITICAL: This structure must match the C definition exactly to prevent - segmentation faults and memory corruption. - """ - _fields_ = [ - # Buffer arrays - these are pointers to arrays of pointers - # C: IEC_BOOL *(*bool_input)[8] means pointer to array of 8 pointers - ("bool_input", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), - ("bool_output", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), - ("byte_input", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), - ("byte_output", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), - ("int_input", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), - ("int_output", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), - ("dint_input", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), - ("dint_output", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), - ("lint_input", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), - ("lint_output", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), - ("int_memory", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), - ("dint_memory", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), - ("lint_memory", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), - - # Mutex function pointers - ("mutex_take", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), - ("mutex_give", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), - ("buffer_mutex", ctypes.c_void_p), - - # Buffer size information - ("buffer_size", ctypes.c_int), - ("bits_per_buffer", ctypes.c_int), - ] - - def validate_pointers(self): - """ - Validate that critical pointers are not NULL - Returns: (bool, str) - (is_valid, error_message) - """ - try: - # Check buffer mutex - if not self.buffer_mutex: - return False, "buffer_mutex is NULL" - - # Check mutex functions - if not self.mutex_take: - return False, "mutex_take function pointer is NULL" - if not self.mutex_give: - return False, "mutex_give function pointer is NULL" - - # Check buffer size is reasonable - if self.buffer_size <= 0 or self.buffer_size > 10000: - return False, f"buffer_size is invalid: {self.buffer_size}" - - if self.bits_per_buffer <= 0 or self.bits_per_buffer > 64: - return False, f"bits_per_buffer is invalid: {self.bits_per_buffer}" - - return True, "All pointers valid" - - except Exception as e: - return False, f"Exception during validation: {e}" - - def safe_access_buffer_size(self): - """ - Safely access buffer_size with validation - Returns: (int, str) - (buffer_size, error_message) - """ - try: - is_valid, msg = self.validate_pointers() - if not is_valid: - return -1, f"Validation failed: {msg}" - - size = self.buffer_size - if size <= 0 or size > 10000: - return -1, f"Invalid buffer size: {size}" - - return size, "Success" - - except Exception as e: - return -1, f"Exception accessing buffer_size: {e}" - - def __str__(self): - """Debug representation of the structure""" - try: - return (f"PluginRuntimeArgs(\n" - f" bool_input=0x{ctypes.addressof(self.bool_input.contents) if self.bool_input else 0:x},\n" - f" bool_output=0x{ctypes.addressof(self.bool_output.contents) if self.bool_output else 0:x},\n" - f" byte_input=0x{ctypes.addressof(self.byte_input.contents) if self.byte_input else 0:x},\n" - f" byte_output=0x{ctypes.addressof(self.byte_output.contents) if self.byte_output else 0:x},\n" - f" int_input=0x{ctypes.addressof(self.int_input.contents) if self.int_input else 0:x},\n" - f" int_output=0x{ctypes.addressof(self.int_output.contents) if self.int_output else 0:x},\n" - f" dint_input=0x{ctypes.addressof(self.dint_input.contents) if self.dint_input else 0:x},\n" - f" dint_output=0x{ctypes.addressof(self.dint_output.contents) if self.dint_output else 0:x},\n" - f" lint_input=0x{ctypes.addressof(self.lint_input.contents) if self.lint_input else 0:x},\n" - f" lint_output=0x{ctypes.addressof(self.lint_output.contents) if self.lint_output else 0:x},\n" - f" int_memory=0x{ctypes.addressof(self.int_memory.contents) if self.int_memory else 0:x},\n" - f" buffer_size={self.buffer_size},\n" - f" bits_per_buffer={self.bits_per_buffer},\n" - f" buffer_mutex=0x{self.buffer_mutex or 0:x},\n" - f" mutex_take={'valid' if self.mutex_take else 'NULL'},\n" - f" mutex_give={'valid' if self.mutex_give else 'NULL'}\n" - f")") - except: - return "PluginRuntimeArgs(corrupted or invalid)" - -class PluginStructureValidator: - """Validates structure alignment and provides debugging tools""" - - @staticmethod - def validate_structure_alignment(): - """ - Validates that the Python ctypes structure has the expected size and alignment - Returns: (bool, str, dict) - (is_valid, message, debug_info) - """ - try: - # Calculate expected structure size - # This is platform-dependent but we can do basic checks - struct_size = ctypes.sizeof(PluginRuntimeArgs) - - debug_info = { - 'structure_size': struct_size, - 'pointer_size': ctypes.sizeof(ctypes.c_void_p), - 'int_size': ctypes.sizeof(ctypes.c_int), - 'platform': sys.platform, - 'architecture': sys.maxsize > 2**32 and '64-bit' or '32-bit' - } - - # Basic sanity checks - expected_min_size = ( - 13 * ctypes.sizeof(ctypes.c_void_p) + # 13 buffer pointers - 2 * ctypes.sizeof(ctypes.c_void_p) + # 2 function pointers - 1 * ctypes.sizeof(ctypes.c_void_p) + # 1 mutex pointer - 2 * ctypes.sizeof(ctypes.c_int) # 2 integers - ) - - if struct_size < expected_min_size: - return False, f"Structure too small: {struct_size} < {expected_min_size}", debug_info - - # Check field offsets make sense - buffer_size_offset = PluginRuntimeArgs.buffer_size.offset - bits_per_buffer_offset = PluginRuntimeArgs.bits_per_buffer.offset - - if bits_per_buffer_offset <= buffer_size_offset: - return False, "Field offsets are incorrect", debug_info - - debug_info['buffer_size_offset'] = buffer_size_offset - debug_info['bits_per_buffer_offset'] = bits_per_buffer_offset - - return True, "Structure validation passed", debug_info - - except Exception as e: - return False, f"Exception during validation: {e}", {} - - @staticmethod - def print_structure_info(): - """Print detailed structure information for debugging""" - is_valid, msg, debug_info = PluginStructureValidator.validate_structure_alignment() - - print("=== Plugin Structure Validation ===") - print(f"Status: {'VALID' if is_valid else 'INVALID'}") - print(f"Message: {msg}") - print("\nStructure Details:") - for key, value in debug_info.items(): - print(f" {key}: {value}") - - print(f"\nField Offsets:") - try: - for field_name, field_type in PluginRuntimeArgs._fields_: - offset = getattr(PluginRuntimeArgs, field_name).offset - print(f" {field_name}: offset {offset}") - except Exception as e: - print(f" Error getting field offsets: {e}") - -class SafeBufferAccess: - """Wrapper class for safe buffer operations with mutex handling""" - - def __init__(self, runtime_args): - """ - Initialize with validated runtime args - Args: - runtime_args: PluginRuntimeArgs instance - """ - self.args = runtime_args - self.is_valid, self.error_msg = runtime_args.validate_pointers() - - def safe_read_bool_output(self, buffer_idx, bit_idx): - """ - Safely read a boolean output with proper mutex handling - Returns: (bool, str) - (value, error_message) - """ - if not self.is_valid: - return False, f"Invalid runtime args: {self.error_msg}" - - try: - # Take mutex - if self.args.mutex_take(self.args.buffer_mutex) != 0: - return False, "Failed to acquire mutex" - - try: - # Validate indices - if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: - return False, f"Invalid buffer index: {buffer_idx}" - if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: - return False, f"Invalid bit index: {bit_idx}" - - # Access the value - read from the actual value, not the pointer - value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) - return value, "Success" - - finally: - # Always release mutex - self.args.mutex_give(self.args.buffer_mutex) - - except Exception as e: - return False, f"Exception during buffer access: {e}" - - def safe_write_bool_output(self, buffer_idx, bit_idx, value): - """ - Safely write a boolean output with proper mutex handling - Returns: (bool, str) - (success, error_message) - """ - if not self.is_valid: - return False, f"Invalid runtime args: {self.error_msg}" - - try: - # Take mutex - if self.args.mutex_take(self.args.buffer_mutex) != 0: - return False, "Failed to acquire mutex" - - try: - # Validate indices - if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: - return False, f"Invalid buffer index: {buffer_idx}" - if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: - return False, f"Invalid bit index: {bit_idx}" - - # Set the value - access the actual value, not the pointer - self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 - return True, "Success" - - finally: - # Always release mutex - self.args.mutex_give(self.args.buffer_mutex) - - except Exception as e: - return False, f"Exception during buffer access: {e}" - -def safe_extract_runtime_args_from_capsule(capsule): - """ - Enhanced capsule extraction with comprehensive validation - Args: - capsule: PyCapsule containing plugin_runtime_args_t structure - Returns: - (PluginRuntimeArgs, str) - (runtime_args, error_message) - """ - try: - # Validate capsule type - if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': - return None, f"Expected PyCapsule object, got {type(capsule)}" - - # Set up the Python API function signatures - ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] - ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p - - # Get the pointer from the capsule - ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") - if not ptr: - return None, "Failed to extract pointer from capsule - invalid capsule name or corrupted data" - - # Cast the pointer to our structure type - args_ptr = ctypes.cast(ptr, ctypes.POINTER(PluginRuntimeArgs)) - if not args_ptr: - return None, "Failed to cast pointer to PluginRuntimeArgs structure" - - runtime_args = args_ptr.contents - - # Validate the extracted structure - is_valid, validation_msg = runtime_args.validate_pointers() - if not is_valid: - return None, f"Structure validation failed: {validation_msg}" - - return runtime_args, "Success" - - except Exception as e: - return None, f"Exception during capsule extraction: {e}" - -if __name__ == "__main__": - # Self-test when run directly - print("OpenPLC Python Plugin Types - Self Test") - print("=" * 50) - - # Test structure validation - PluginStructureValidator.print_structure_info() - - print(f"\nIEC Type Sizes:") - print(f" IEC_BOOL: {ctypes.sizeof(IEC_BOOL)} bytes") - print(f" IEC_BYTE: {ctypes.sizeof(IEC_BYTE)} bytes") - print(f" IEC_UINT: {ctypes.sizeof(IEC_UINT)} bytes") - print(f" IEC_UDINT: {ctypes.sizeof(IEC_UDINT)} bytes") - print(f" IEC_ULINT: {ctypes.sizeof(IEC_ULINT)} bytes") diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 85915770..5262a62a 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -213,7 +213,9 @@ int plugin_driver_start(plugin_driver_t *driver) { printf("[PLUGIN]: Plugin %s started successfully.\n", plugin->config.name); } - Py_DECREF(res); + Py_DECREF( + res); // There's no problem in calling DECREF here because it only + // handles the returned object from start_loop, not the function itself plugin->running = 1; } diff --git a/core/src/drivers/plugins/python/examples/buffer_access_example.py b/core/src/drivers/plugins/python/examples/buffer_access_example.py new file mode 100644 index 00000000..484cddd2 --- /dev/null +++ b/core/src/drivers/plugins/python/examples/buffer_access_example.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +""" +Example demonstrating comprehensive buffer access with the enhanced SafeBufferAccess class +This example shows how to use all the new read functions and batch operations for optimized mutex usage. +""" + +import sys +import os + +# Add the shared directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'shared')) + +from python_plugin_types import ( + PluginRuntimeArgs, + SafeBufferAccess, + safe_extract_runtime_args_from_capsule, + PluginStructureValidator +) + +def demonstrate_individual_operations(buffer_access): + """ + Demonstrate individual read/write operations with thread-safe parameter + """ + print("\n=== Individual Operations Demo ===") + + # Boolean operations + print("\n1. Boolean Operations:") + success, msg = buffer_access.write_bool_output(0, 0, True, thread_safe=True) + print(f"Write bool_output[0][0] = True: {success} - {msg}") + + value, msg = buffer_access.read_bool_output(0, 0, thread_safe=True) + print(f"Read bool_output[0][0]: {value} - {msg}") + + value, msg = buffer_access.read_bool_input(0, 0, thread_safe=True) + print(f"Read bool_input[0][0]: {value} - {msg}") + + # Byte operations + print("\n2. Byte Operations:") + success, msg = buffer_access.write_byte_output(0, 42, thread_safe=True) + print(f"Write byte_output[0] = 42: {success} - {msg}") + + value, msg = buffer_access.read_byte_output(0, thread_safe=True) + print(f"Read byte_output[0]: {value} - {msg}") + + value, msg = buffer_access.read_byte_input(0, thread_safe=True) + print(f"Read byte_input[0]: {value} - {msg}") + + # Int operations (16-bit) + print("\n3. Int Operations (16-bit):") + success, msg = buffer_access.write_int_output(0, 1234, thread_safe=True) + print(f"Write int_output[0] = 1234: {success} - {msg}") + + value, msg = buffer_access.read_int_output(0, thread_safe=True) + print(f"Read int_output[0]: {value} - {msg}") + + value, msg = buffer_access.read_int_input(0, thread_safe=True) + print(f"Read int_input[0]: {value} - {msg}") + + # Memory operations + print("\n4. Memory Operations:") + success, msg = buffer_access.write_int_memory(0, 5678, thread_safe=True) + print(f"Write int_memory[0] = 5678: {success} - {msg}") + + value, msg = buffer_access.read_int_memory(0, thread_safe=True) + print(f"Read int_memory[0]: {value} - {msg}") + + # Dint operations (32-bit) + print("\n5. Dint Operations (32-bit):") + success, msg = buffer_access.write_dint_output(0, 123456789, thread_safe=True) + print(f"Write dint_output[0] = 123456789: {success} - {msg}") + + value, msg = buffer_access.read_dint_output(0, thread_safe=True) + print(f"Read dint_output[0]: {value} - {msg}") + + value, msg = buffer_access.read_dint_input(0, thread_safe=True) + print(f"Read dint_input[0]: {value} - {msg}") + + success, msg = buffer_access.write_dint_memory(0, 987654321, thread_safe=True) + print(f"Write dint_memory[0] = 987654321: {success} - {msg}") + + value, msg = buffer_access.read_dint_memory(0, thread_safe=True) + print(f"Read dint_memory[0]: {value} - {msg}") + + # Lint operations (64-bit) + print("\n6. Lint Operations (64-bit):") + success, msg = buffer_access.write_lint_output(0, 1234567890123456789, thread_safe=True) + print(f"Write lint_output[0] = 1234567890123456789: {success} - {msg}") + + value, msg = buffer_access.read_lint_output(0, thread_safe=True) + print(f"Read lint_output[0]: {value} - {msg}") + + value, msg = buffer_access.read_lint_input(0, thread_safe=True) + print(f"Read lint_input[0]: {value} - {msg}") + + success, msg = buffer_access.write_lint_memory(0, 9876543210987654321, thread_safe=True) + print(f"Write lint_memory[0] = 9876543210987654321: {success} - {msg}") + + value, msg = buffer_access.read_lint_memory(0, thread_safe=True) + print(f"Read lint_memory[0]: {value} - {msg}") + +def demonstrate_batch_operations(buffer_access): + """ + Demonstrate batch operations for optimized mutex usage + """ + print("\n=== Batch Operations Demo ===") + + # Batch read operations + print("\n1. Batch Read Operations:") + read_operations = [ + ('bool_output', 0, 0), # Read bool_output[0][0] + ('byte_output', 0), # Read byte_output[0] + ('int_output', 0), # Read int_output[0] + ('dint_output', 0), # Read dint_output[0] + ('lint_output', 0), # Read lint_output[0] + ('int_memory', 0), # Read int_memory[0] + ('dint_memory', 0), # Read dint_memory[0] + ('lint_memory', 0), # Read lint_memory[0] + ] + + results, msg = buffer_access.batch_read_values(read_operations) + print(f"Batch read result: {msg}") + for i, (success, value, error_msg) in enumerate(results): + op = read_operations[i] + print(f" {op[0]}[{op[1]}{',' + str(op[2]) if len(op) > 2 else ''}]: {success} - Value: {value} - {error_msg}") + + # Batch write operations + print("\n2. Batch Write Operations:") + write_operations = [ + ('bool_output', 1, True, 0), # Write bool_output[1][0] = True + ('byte_output', 1, 100), # Write byte_output[1] = 100 + ('int_output', 1, 2000), # Write int_output[1] = 2000 + ('dint_output', 1, 300000000), # Write dint_output[1] = 300000000 + ('lint_output', 1, 4000000000000000000), # Write lint_output[1] = 4000000000000000000 + ('int_memory', 1, 1500), # Write int_memory[1] = 1500 + ('dint_memory', 1, 250000000), # Write dint_memory[1] = 250000000 + ('lint_memory', 1, 3000000000000000000), # Write lint_memory[1] = 3000000000000000000 + ] + + results, msg = buffer_access.batch_write_values(write_operations) + print(f"Batch write result: {msg}") + for i, (success, error_msg) in enumerate(results): + op = write_operations[i] + print(f" {op[0]}[{op[1]}{',' + str(op[3]) if len(op) > 3 else ''}] = {op[2]}: {success} - {error_msg}") + + # Mixed batch operations + print("\n3. Mixed Batch Operations:") + read_ops = [ + ('bool_output', 1, 0), # Read the boolean we just wrote + ('byte_output', 1), # Read the byte we just wrote + ('int_output', 1), # Read the int we just wrote + ] + + write_ops = [ + ('bool_output', 2, False, 0), # Write bool_output[2][0] = False + ('byte_output', 2, 200), # Write byte_output[2] = 200 + ('int_output', 2, 3000), # Write int_output[2] = 3000 + ] + + results, msg = buffer_access.batch_mixed_operations(read_ops, write_ops) + print(f"Mixed batch result: {msg}") + + if 'reads' in results: + print(" Read results:") + for i, (success, value, error_msg) in enumerate(results['reads']): + op = read_ops[i] + print(f" {op[0]}[{op[1]}{',' + str(op[2]) if len(op) > 2 else ''}]: {success} - Value: {value} - {error_msg}") + + if 'writes' in results: + print(" Write results:") + for i, (success, error_msg) in enumerate(results['writes']): + op = write_ops[i] + print(f" {op[0]}[{op[1]}{',' + str(op[3]) if len(op) > 3 else ''}] = {op[2]}: {success} - {error_msg}") + +def demonstrate_manual_mutex_control(buffer_access): + """ + Demonstrate manual mutex control for custom operations + """ + print("\n=== Manual Mutex Control Demo ===") + + # Acquire mutex manually + success, msg = buffer_access.acquire_mutex() + print(f"Manual mutex acquisition: {success} - {msg}") + + if success: + try: + # Perform multiple operations without individual mutex overhead + print("Performing operations with manual mutex control:") + + # Read some values (thread_safe=False since we already have the mutex) + value, msg = buffer_access.read_bool_output(0, 0, thread_safe=False) + print(f" Read bool_output[0][0]: {value} - {msg}") + + value, msg = buffer_access.read_byte_output(0, thread_safe=False) + print(f" Read byte_output[0]: {value} - {msg}") + + # Write some values (thread_safe=False since we already have the mutex) + success, msg = buffer_access.write_bool_output(3, 0, True, thread_safe=False) + print(f" Write bool_output[3][0] = True: {success} - {msg}") + + success, msg = buffer_access.write_byte_output(3, 255, thread_safe=False) + print(f" Write byte_output[3] = 255: {success} - {msg}") + + finally: + # Always release the mutex + success, msg = buffer_access.release_mutex() + print(f"Manual mutex release: {success} - {msg}") + +def demonstrate_thread_safe_parameter(buffer_access): + """ + Demonstrate the thread_safe parameter usage + """ + print("\n=== Thread-Safe Parameter Demo ===") + + print("1. Operations with thread_safe=True (default):") + value, msg = buffer_access.read_byte_output(0, thread_safe=True) + print(f" Read with mutex: {value} - {msg}") + + print("2. Operations with thread_safe=False (manual mutex control):") + # This would normally be used when you've manually acquired the mutex + # For demo purposes, we'll show it works but note it's not thread-safe + value, msg = buffer_access.read_byte_output(0, thread_safe=False) + print(f" Read without mutex: {value} - {msg}") + print(" Note: thread_safe=False should only be used with manual mutex control!") + +def main(): + """ + Main demonstration function + Note: This is a demonstration of the API. In a real plugin, you would receive + the runtime_args from the OpenPLC runtime via the plugin interface. + """ + print("OpenPLC Python Plugin Buffer Access Demonstration") + print("=" * 60) + + # Print structure information + print("\nStructure Validation:") + PluginStructureValidator.print_structure_info() + + print("\n" + "=" * 60) + print("IMPORTANT NOTE:") + print("This is a demonstration of the SafeBufferAccess API.") + print("In a real plugin, you would receive the runtime_args structure") + print("from the OpenPLC runtime via the plugin interface.") + print("The following demonstrations show the API usage patterns.") + print("=" * 60) + + # In a real plugin, you would get runtime_args from the plugin interface + # For demonstration purposes, we'll show the API patterns + print("\nAPI Usage Patterns:") + + print("\n1. Extracting runtime args from capsule:") + print(" runtime_args, error = safe_extract_runtime_args_from_capsule(capsule)") + print(" if runtime_args is None:") + print(" print(f'Failed to extract runtime args: {error}')") + print(" return") + + print("\n2. Creating SafeBufferAccess instance:") + print(" buffer_access = SafeBufferAccess(runtime_args)") + print(" if not buffer_access.is_valid:") + print(" print(f'Invalid buffer access: {buffer_access.error_msg}')") + print(" return") + + print("\n3. Individual operations:") + print(" # Read operations") + print(" value, msg = buffer_access.read_bool_input(0, 0)") + print(" value, msg = buffer_access.read_byte_input(0)") + print(" value, msg = buffer_access.read_int_input(0)") + print(" value, msg = buffer_access.read_dint_input(0)") + print(" value, msg = buffer_access.read_lint_input(0)") + print(" value, msg = buffer_access.read_int_memory(0)") + print(" value, msg = buffer_access.read_dint_memory(0)") + print(" value, msg = buffer_access.read_lint_memory(0)") + + print("\n # Write operations") + print(" success, msg = buffer_access.write_bool_output(0, 0, True)") + print(" success, msg = buffer_access.write_byte_output(0, 255)") + print(" success, msg = buffer_access.write_int_output(0, 65535)") + print(" success, msg = buffer_access.write_dint_output(0, 4294967295)") + print(" success, msg = buffer_access.write_lint_output(0, 18446744073709551615)") + print(" success, msg = buffer_access.write_int_memory(0, 1000)") + print(" success, msg = buffer_access.write_dint_memory(0, 2000000)") + print(" success, msg = buffer_access.write_lint_memory(0, 3000000000)") + + print("\n4. Batch operations for optimized mutex usage:") + print(" # Batch reads") + print(" read_ops = [('bool_input', 0, 0), ('byte_input', 0), ('int_input', 0)]") + print(" results, msg = buffer_access.batch_read_values(read_ops)") + + print("\n # Batch writes") + print(" write_ops = [('bool_output', 0, True, 0), ('byte_output', 0, 100)]") + print(" results, msg = buffer_access.batch_write_values(write_ops)") + + print("\n # Mixed batch operations") + print(" results, msg = buffer_access.batch_mixed_operations(read_ops, write_ops)") + + print("\n5. Manual mutex control:") + print(" success, msg = buffer_access.acquire_mutex()") + print(" try:") + print(" # Multiple operations with thread_safe=False") + print(" value, msg = buffer_access.read_byte_input(0, thread_safe=False)") + print(" success, msg = buffer_access.write_byte_output(0, 50, thread_safe=False)") + print(" finally:") + print(" success, msg = buffer_access.release_mutex()") + + print("\n6. Thread-safe parameter usage:") + print(" # Default behavior (thread_safe=True)") + print(" value, msg = buffer_access.read_byte_input(0)") + print(" # Manual mutex control (thread_safe=False)") + print(" value, msg = buffer_access.read_byte_input(0, thread_safe=False)") + + print("\n" + "=" * 60) + print("Key Benefits:") + print("- Complete read/write access to all IEC buffer types") + print("- Optional thread-safe parameter for all operations") + print("- Batch operations for optimized mutex usage") + print("- Manual mutex control for custom operation sequences") + print("- Comprehensive error handling and validation") + print("- Maximum flexibility for plugin developers") + print("=" * 60) + +if __name__ == "__main__": + main() diff --git a/core/src/drivers/examples/example_python_plugin.py b/core/src/drivers/plugins/python/examples/example_python_plugin.py similarity index 93% rename from core/src/drivers/examples/example_python_plugin.py rename to core/src/drivers/plugins/python/examples/example_python_plugin.py index 5794c58f..10cb580c 100644 --- a/core/src/drivers/examples/example_python_plugin.py +++ b/core/src/drivers/plugins/python/examples/example_python_plugin.py @@ -4,13 +4,18 @@ This demonstrates the expected functions that should be present in a Python plugin """ +from concurrent.futures import thread import time import ctypes from ctypes import * import threading + +# Add the parent directory to Python path to find shared module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + # Import the correct type definitions -from python_plugin_types import ( +from shared.python_plugin_types import ( PluginRuntimeArgs, safe_extract_runtime_args_from_capsule, SafeBufferAccess, @@ -88,7 +93,7 @@ def loop(): while not _stop.is_set(): time.sleep(0.1) addr = ctypes.addressof(_runtime_args.bool_output[0][0]) - value, msg = _safe_buffer_access.safe_read_bool_output(0,0) + value, msg = _safe_buffer_access.read_bool_output(0,0, thread_safe=True) print(f"Value at address 0x{addr:x}: {value} ({msg})") global _mainthread diff --git a/core/src/drivers/plugins/python/modbus_slave_config.ini b/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.ini similarity index 100% rename from core/src/drivers/plugins/python/modbus_slave_config.ini rename to core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.ini diff --git a/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py b/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py new file mode 100644 index 00000000..703322a7 --- /dev/null +++ b/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py @@ -0,0 +1,529 @@ +import asyncio +import ctypes +from operator import add +import threading +import time +import sys +import os +from pymodbus.server import StartAsyncTcpServer, ServerStop +from pymodbus.datastore import ( + ModbusSparseDataBlock, + ModbusDeviceContext, + ModbusServerContext, +) + +MAX_BITS = 8 + +# Add the parent directory to Python path to find shared module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +# Import the correct type definitions +from shared.python_plugin_types import ( + PluginRuntimeArgs, + safe_extract_runtime_args_from_capsule, + SafeBufferAccess, + PluginStructureValidator +) + +class OpenPLCCoilsDataBlock(ModbusSparseDataBlock): + """Custom Modbus coils data block that mirrors OpenPLC bool_output using SafeBufferAccess""" + + def __init__(self, runtime_args, num_coils=64): + self.runtime_args = runtime_args + self.num_coils = num_coils + + # Create safe buffer access wrapper + self.safe_buffer_access = SafeBufferAccess(runtime_args) + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Warning: Failed to create safe buffer access for coils: {self.safe_buffer_access.error_msg}") + + # Initialize with zeros + super().__init__([0] * num_coils) + + def getValues(self, address, count=1): + address = address - 1 # Modbus addresses are 0-based + + """Get coil values from OpenPLC bool_output using SafeBufferAccess""" + print(f"[MODBUS] Coils getValues called: address={address}, count={count}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return [0] * count + + # Ensure thread-safe access + self.safe_buffer_access.acquire_mutex() + + values = [] + for i in range(count): + coil_addr = address + i + + if coil_addr < self.num_coils: + # Map coil address to buffer and bit indices + buffer_idx = coil_addr // MAX_BITS # 8 bits per buffer + bit_idx = coil_addr % MAX_BITS # bit within buffer + + value, error_msg = self.safe_buffer_access.read_bool_output(buffer_idx, bit_idx, thread_safe=False) + if error_msg == "Success": + values.append(1 if value else 0) + print(f"[MODBUS] Read coil {coil_addr} (buf:{buffer_idx}, bit:{bit_idx}): {value}") + else: + print(f"[MODBUS] Error reading coil {coil_addr}: {error_msg}") + values.append(0) + else: + values.append(0) + + # Release mutex after access + self.safe_buffer_access.release_mutex() + + return values + + def setValues(self, address, values): + address = address - 1 # Modbus addresses are 0-based + """Set coil values to OpenPLC bool_output using SafeBufferAccess""" + print(f"[MODBUS] Coils setValues called: address={address}, values={values}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return + + # Ensure thread-safe access + self.safe_buffer_access.acquire_mutex() + + for i, value in enumerate(values): + coil_addr = address + i + + if coil_addr < self.num_coils: + # Map coil address to buffer and bit indices + buffer_idx = coil_addr // MAX_BITS # 8 bits per buffer + bit_idx = coil_addr % MAX_BITS # bit within buffer + + success, error_msg = self.safe_buffer_access.write_bool_output(buffer_idx, bit_idx, bool(value), thread_safe=False) + if error_msg == "Success": + print(f"[MODBUS] Set coil {coil_addr} (buf:{buffer_idx}, bit:{bit_idx}): {bool(value)}") + else: + print(f"[MODBUS] Error setting coil {coil_addr}: {error_msg}") + + # Release mutex after access + self.safe_buffer_access.release_mutex() + + +class OpenPLCDiscreteInputsDataBlock(ModbusSparseDataBlock): + """Custom Modbus discrete inputs data block that mirrors OpenPLC bool_input using SafeBufferAccess""" + + def __init__(self, runtime_args, num_inputs=64): + self.runtime_args = runtime_args + self.num_inputs = num_inputs + + # Create safe buffer access wrapper + self.safe_buffer_access = SafeBufferAccess(runtime_args) + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Warning: Failed to create safe buffer access for discrete inputs: {self.safe_buffer_access.error_msg}") + + # Initialize with zeros + super().__init__([0] * num_inputs) + + def getValues(self, address, count=1): + address = address - 1 # Modbus addresses are 0-based + """Get discrete input values from OpenPLC bool_input using SafeBufferAccess""" + print(f"[MODBUS] Discrete Inputs getValues called: address={address}, count={count}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return [0] * count + + # Ensure thread-safe access + self.safe_buffer_access.acquire_mutex() + + values = [] + for i in range(count): + input_addr = address + i + + if input_addr < self.num_inputs: + # Map input address to buffer and bit indices + buffer_idx = input_addr // MAX_BITS # 8 bits per buffer + bit_idx = input_addr % MAX_BITS # bit within buffer + + value, error_msg = self.safe_buffer_access.read_bool_input(buffer_idx, bit_idx, thread_safe=False) + if error_msg == "Success": + values.append(1 if value else 0) + print(f"[MODBUS] Read discrete input {input_addr} (buf:{buffer_idx}, bit:{bit_idx}): {value}") + else: + print(f"[MODBUS] Error reading discrete input {input_addr}: {error_msg}") + values.append(0) + else: + values.append(0) + + # Release mutex after access + self.safe_buffer_access.release_mutex() + + return values + + def setValues(self, address, values): + """Discrete inputs are read-only, this method should not be called""" + print(f"[MODBUS] Warning: Attempt to write to read-only discrete inputs at address {address}") + + +class OpenPLCInputRegistersDataBlock(ModbusSparseDataBlock): + """Custom Modbus input registers data block that mirrors OpenPLC analog inputs using SafeBufferAccess""" + + def __init__(self, runtime_args, num_registers=32): + self.runtime_args = runtime_args + self.num_registers = num_registers + + # Create safe buffer access wrapper + self.safe_buffer_access = SafeBufferAccess(runtime_args) + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Warning: Failed to create safe buffer access for input registers: {self.safe_buffer_access.error_msg}") + + # Initialize with zeros + super().__init__([0] * num_registers) + + def getValues(self, address, count=1): + address = address - 1 # Modbus addresses are 0-based + """Get input register values from OpenPLC int_input using SafeBufferAccess""" + print(f"[MODBUS] Input Registers getValues called: address={address}, count={count}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return [0] * count + + # Ensure buffer mutext + self.safe_buffer_access.acquire_mutex() + + values = [] + for i in range(count): + reg_addr = address + i + + if reg_addr < self.num_registers: + value, error_msg = self.safe_buffer_access.read_int_input(reg_addr, thread_safe=False) + if error_msg == "Success": + values.append(value) + print(f"[MODBUS] Read input register {reg_addr}: {value}") + else: + print(f"[MODBUS] Error reading input register {reg_addr}: {error_msg}") + values.append(0) + else: + values.append(0) + + # Release mutex after access + self.safe_buffer_access.release_mutex() + + return values + + def setValues(self, address, values): + """Input registers are read-only, this method should not be called""" + print(f"[MODBUS] Warning: Attempt to write to read-only input registers at address {address}") + + +class OpenPLCHoldingRegistersDataBlock(ModbusSparseDataBlock): + """Custom Modbus holding registers data block that mirrors OpenPLC analog outputs using SafeBufferAccess""" + + def __init__(self, runtime_args, num_registers=32): + self.runtime_args = runtime_args + self.num_registers = num_registers + + # Create safe buffer access wrapper + self.safe_buffer_access = SafeBufferAccess(runtime_args) + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Warning: Failed to create safe buffer access for holding registers: {self.safe_buffer_access.error_msg}") + + # Initialize with zeros + super().__init__([0] * num_registers) + + def getValues(self, address, count=1): + address = address - 1 # Modbus addresses are 0-based + """Get holding register values from OpenPLC int_output using SafeBufferAccess""" + print(f"[MODBUS] Holding Registers getValues called: address={address}, count={count}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return [0] * count + + # Ensure buffer mutex + self.safe_buffer_access.acquire_mutex() + + values = [] + for i in range(count): + reg_addr = address + i + + if reg_addr < self.num_registers: + value, error_msg = self.safe_buffer_access.read_int_output(reg_addr, thread_safe=False) + if error_msg == "Success": + values.append(value) + print(f"[MODBUS] Read holding register {reg_addr}: {value}") + else: + print(f"[MODBUS] Error reading holding register {reg_addr}: {error_msg}") + values.append(0) + else: + values.append(0) + + # Release mutex after access + self.safe_buffer_access.release_mutex() + return values + + def setValues(self, address, values): + address = address - 1 # Modbus addresses are 0-based + """Set holding register values to OpenPLC int_output using SafeBufferAccess""" + print(f"[MODBUS] Holding Registers setValues called: address={address}, values={values}") + + if not self.safe_buffer_access.is_valid: + print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") + return + + # Ensure buffer mutex + self.safe_buffer_access.acquire_mutex() + + for i, value in enumerate(values): + reg_addr = address + i + + if reg_addr < self.num_registers: + success, error_msg = self.safe_buffer_access.write_int_output(reg_addr, value, thread_safe=False) + if error_msg == "Success": + print(f"[MODBUS] Set holding register {reg_addr}: {value}") + else: + print(f"[MODBUS] Error setting holding register {reg_addr}: {error_msg}") + + # Release mutex after access + self.safe_buffer_access.release_mutex() + +# Global variables for plugin lifecycle +server_task = None +server_context = None +runtime_args = None +update_thread = None +running = False +gIp = "172.29.65.104" +gPort = 5020 + +def init(args_capsule, host="172.29.65.104", port=5020): + """Initialize the Modbus plugin""" + global runtime_args, server_context, gIp, gPort + gIp = host + gPort = port + + print("[MODBUS] Python plugin 'simple_modbus' initializing...") + + try: + # Print structure validation info for debugging + print("[MODBUS] Validating plugin structure alignment...") + PluginStructureValidator.print_structure_info() + + # Extract runtime args from capsule using safe method + if hasattr(args_capsule, '__class__') and 'PyCapsule' in str(type(args_capsule)): + # This is a PyCapsule from C - use safe extraction + runtime_args, error_msg = safe_extract_runtime_args_from_capsule(args_capsule) + if runtime_args is None: + print(f"[MODBUS] ✗ Failed to extract runtime args: {error_msg}") + return False + + print(f"[MODBUS] ✓ Runtime arguments extracted successfully") + else: + # This is a direct object (for testing) + runtime_args = args_capsule + print(f"[MODBUS] ✓ Using direct runtime args for testing") + + # Safely access buffer size using validation + buffer_size, size_error = runtime_args.safe_access_buffer_size() + if buffer_size == -1: + print(f"[MODBUS] ✗ Failed to access buffer size: {size_error}") + return False + + print(f"[MODBUS] Buffer size: {buffer_size}") + print(f"[MODBUS] Bits per buffer: {runtime_args.bits_per_buffer}") + print(f"[MODBUS] Structure details: {runtime_args}") + + # Create OpenPLC-connected data blocks for all Modbus types + coils_block = OpenPLCCoilsDataBlock(runtime_args, num_coils=64) + discrete_inputs_block = OpenPLCDiscreteInputsDataBlock(runtime_args, num_inputs=64) + input_registers_block = OpenPLCInputRegistersDataBlock(runtime_args, num_registers=32) + holding_registers_block = OpenPLCHoldingRegistersDataBlock(runtime_args, num_registers=32) + + # Create device context with all OpenPLC-connected data blocks + print(f"[MODBUS] Created data blocks:") + print(f"[MODBUS] - Coils (bool_output): {coils_block.num_coils} coils") + print(f"[MODBUS] - Discrete Inputs (bool_input): {discrete_inputs_block.num_inputs} inputs") + print(f"[MODBUS] - Input Registers (int_input): {input_registers_block.num_registers} registers") + print(f"[MODBUS] - Holding Registers (int_output): {holding_registers_block.num_registers} registers") + + device = ModbusDeviceContext( + di=discrete_inputs_block, # Discrete Inputs -> bool_input + co=coils_block, # Coils -> bool_output + ir=input_registers_block, # Input Registers -> int_input + hr=holding_registers_block # Holding Registers -> int_output + ) + server_context = ModbusServerContext(devices={1: device}, single=False) + + print(f"[MODBUS] ✓ Plugin initialized successfully - Host: {host}, Port: {port}") + return True + + except Exception as e: + print(f"[MODBUS] ✗ Plugin initialization failed: {e}") + import traceback + traceback.print_exc() + return False + +def start_loop(): + """Start the Modbus server""" + global server_task, running, update_thread, gIp, gPort + + if server_context is None: + print("[MODBUS] Error: Plugin not initialized") + return False + + print("[MODBUS] Server context is valid, proceeding with startup...") + print(f"[MODBUS] Server context created successfully") + + running = True + + # Start server in separate thread with proper asyncio handling + def run_server(): + try: + print("[MODBUS] Creating new event loop for server thread...") + # Create new event loop for this thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + print("[MODBUS] Event loop created successfully") + + # Start the server and keep it running + async def start_server(): + try: + print(f"[MODBUS] Attempting to start TCP server on {gIp}:{gPort}...") + try: + server = await StartAsyncTcpServer( + context=server_context, + address=(gIp, gPort) + ) + print(f"[MODBUS] Server successfully bound to {gIp}:{gPort}") + except Exception as bind_error: + print(f"[MODBUS] Failed to bind to {gIp}:{gPort}: {bind_error}") + print(f"[MODBUS] Attempting to bind to 0.0.0.0:{gPort} as fallback...") + server = await StartAsyncTcpServer( + context=server_context, + address=("0.0.0.0", gPort) + ) + print(f"[MODBUS] Server successfully bound to 0.0.0.0:{gPort} (fallback)") + + # Keep the server running + try: + print("[MODBUS] Server is now running and accepting connections") + while running: + await asyncio.sleep(1) + except asyncio.CancelledError: + print("[MODBUS] Server cancelled") + finally: + print("[MODBUS] Shutting down server...") + if hasattr(server, 'close'): + server.close() + if hasattr(server, 'wait_closed'): + await server.wait_closed() + print("[MODBUS] Server shutdown complete") + + except Exception as server_error: + print(f"[MODBUS] Error in start_server async function: {server_error}") + import traceback + print(f"[MODBUS] Traceback: {traceback.format_exc()}") + raise + + # Run the server + print("[MODBUS] Running server event loop...") + loop.run_until_complete(start_server()) + + except Exception as e: + print(f"[MODBUS] Error in run_server thread: {e}") + import traceback + print(f"[MODBUS] Full traceback: {traceback.format_exc()}") + finally: + print("[MODBUS] Closing event loop...") + loop.close() + print("[MODBUS] Event loop closed") + + server_task = threading.Thread(target=run_server, daemon=False) + server_task.start() + + print(f"[MODBUS] Server thread started on {gIp}:{gPort}") + return True + +def stop_loop(): + """Stop the Modbus server""" + global server_task, running, update_thread + + running = False + + if update_thread: + update_thread.join(timeout=1.0) + update_thread = None + + if server_task: + # Stop the asyncio server + try: + asyncio.run(ServerStop()) + except: + pass + + server_task.join(timeout=2.0) + server_task = None + + print("[MODBUS] Server stopped") + return True + +def cleanup(): + """Cleanup plugin resources""" + global server_context, runtime_args + + server_context = None + runtime_args = None + + print("[MODBUS] Plugin cleaned up") + return True + +async def main(): + """Standalone server for testing""" + # Create a proper mock runtime args that inherits from PluginRuntimeArgs + import ctypes + + # Create a mock that has the required methods + class MockArgs: + def __init__(self): + self.buffer_size = 1 + self.bits_per_buffer = 8 + # Create simple boolean list for testing + self.bool_data = [[False] * 8] # 1 buffer, 8 booleans + self.bool_output = self.bool_data # Simple reference + self.mutex_take = None + self.mutex_give = None + self.buffer_mutex = None + + def safe_access_buffer_size(self): + """Mock implementation of safe_access_buffer_size""" + return self.buffer_size, "Success" + + def validate_pointers(self): + """Mock implementation of validate_pointers""" + return True, "Mock validation passed" + + def __str__(self): + return f"MockArgs(buffer_size={self.buffer_size}, bits_per_buffer={self.bits_per_buffer})" + + mock_args = MockArgs() + + # Initialize and start + if init(mock_args): + if start_loop(): + print(f"Modbus server running on {gIp}:{gPort}") + print("Press Ctrl+C to stop...") + + try: + # Keep server running + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + print("\nStopping server...") + stop_loop() + cleanup() + else: + print("Failed to start server") + else: + print("Failed to initialize plugin") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/core/src/drivers/plugins/python/shared/__init__.py b/core/src/drivers/plugins/python/shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/core/src/drivers/plugins/python/shared/python_plugin_types.py b/core/src/drivers/plugins/python/shared/python_plugin_types.py new file mode 100644 index 00000000..9ad9ae56 --- /dev/null +++ b/core/src/drivers/plugins/python/shared/python_plugin_types.py @@ -0,0 +1,1531 @@ +#!/usr/bin/env python3 +""" +Shared type definitions for OpenPLC Python plugins +This module provides correct ctypes mappings for the plugin_runtime_args_t structure +""" + +import ctypes +from ctypes import * +import sys + +# IEC type mappings based on iec_types.h +# These must match exactly with the C definitions +IEC_BOOL = ctypes.c_uint8 # typedef uint8_t IEC_BOOL; +IEC_BYTE = ctypes.c_uint8 # typedef uint8_t IEC_BYTE; +IEC_UINT = ctypes.c_uint16 # typedef uint16_t IEC_UINT; +IEC_UDINT = ctypes.c_uint32 # typedef uint32_t IEC_UDINT; +IEC_ULINT = ctypes.c_uint64 # typedef uint64_t IEC_ULINT; + +class PluginRuntimeArgs(ctypes.Structure): + """ + Python ctypes structure matching plugin_runtime_args_t from plugin_driver.h + + CRITICAL: This structure must match the C definition exactly to prevent + segmentation faults and memory corruption. + """ + _fields_ = [ + # Buffer arrays - these are pointers to arrays of pointers + # C: IEC_BOOL *(*bool_input)[8] means pointer to array of 8 pointers + ("bool_input", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), + ("bool_output", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), + ("byte_input", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), + ("byte_output", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), + ("int_input", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("int_output", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("dint_input", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("dint_output", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("lint_input", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + ("lint_output", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + ("int_memory", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), + ("dint_memory", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), + ("lint_memory", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), + + # Mutex function pointers + ("mutex_take", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), + ("mutex_give", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), + ("buffer_mutex", ctypes.c_void_p), + + # Buffer size information + ("buffer_size", ctypes.c_int), + ("bits_per_buffer", ctypes.c_int), + ] + + def validate_pointers(self): + """ + Validate that critical pointers are not NULL + Returns: (bool, str) - (is_valid, error_message) + """ + try: + # Check buffer mutex + if not self.buffer_mutex: + return False, "buffer_mutex is NULL" + + # Check mutex functions + if not self.mutex_take: + return False, "mutex_take function pointer is NULL" + if not self.mutex_give: + return False, "mutex_give function pointer is NULL" + + # Check buffer size is reasonable + if self.buffer_size <= 0 or self.buffer_size > 10000: + return False, f"buffer_size is invalid: {self.buffer_size}" + + if self.bits_per_buffer <= 0 or self.bits_per_buffer > 64: + return False, f"bits_per_buffer is invalid: {self.bits_per_buffer}" + + return True, "All pointers valid" + + except (AttributeError, TypeError) as e: + return False, f"Structure access error during validation: {e}" + except (ValueError, OverflowError) as e: + return False, f"Value validation error: {e}" + except OSError as e: + return False, f"System error during validation: {e}" + + def safe_access_buffer_size(self): + """ + Safely access buffer_size with validation + Returns: (int, str) - (buffer_size, error_message) + """ + try: + is_valid, msg = self.validate_pointers() + if not is_valid: + return -1, f"Validation failed: {msg}" + + size = self.buffer_size + if size <= 0 or size > 10000: + return -1, f"Invalid buffer size: {size}" + + return size, "Success" + + except (AttributeError, TypeError) as e: + return -1, f"Structure access error: {e}" + except (ValueError, OverflowError) as e: + return -1, f"Value validation error: {e}" + except OSError as e: + return -1, f"System error accessing buffer_size: {e}" + + def __str__(self): + """Debug representation of the structure""" + try: + return (f"PluginRuntimeArgs(\n" + f" bool_input=0x{ctypes.addressof(self.bool_input.contents) if self.bool_input else 0:x},\n" + f" bool_output=0x{ctypes.addressof(self.bool_output.contents) if self.bool_output else 0:x},\n" + f" byte_input=0x{ctypes.addressof(self.byte_input.contents) if self.byte_input else 0:x},\n" + f" byte_output=0x{ctypes.addressof(self.byte_output.contents) if self.byte_output else 0:x},\n" + f" int_input=0x{ctypes.addressof(self.int_input.contents) if self.int_input else 0:x},\n" + f" int_output=0x{ctypes.addressof(self.int_output.contents) if self.int_output else 0:x},\n" + f" dint_input=0x{ctypes.addressof(self.dint_input.contents) if self.dint_input else 0:x},\n" + f" dint_output=0x{ctypes.addressof(self.dint_output.contents) if self.dint_output else 0:x},\n" + f" lint_input=0x{ctypes.addressof(self.lint_input.contents) if self.lint_input else 0:x},\n" + f" lint_output=0x{ctypes.addressof(self.lint_output.contents) if self.lint_output else 0:x},\n" + f" int_memory=0x{ctypes.addressof(self.int_memory.contents) if self.int_memory else 0:x},\n" + f" buffer_size={self.buffer_size},\n" + f" bits_per_buffer={self.bits_per_buffer},\n" + f" buffer_mutex=0x{self.buffer_mutex or 0:x},\n" + f" mutex_take={'valid' if self.mutex_take else 'NULL'},\n" + f" mutex_give={'valid' if self.mutex_give else 'NULL'}\n" + f")") + except: + return "PluginRuntimeArgs(corrupted or invalid)" + +class PluginStructureValidator: + """Validates structure alignment and provides debugging tools""" + + @staticmethod + def validate_structure_alignment(): + """ + Validates that the Python ctypes structure has the expected size and alignment + Returns: (bool, str, dict) - (is_valid, message, debug_info) + """ + try: + # Calculate expected structure size + # This is platform-dependent but we can do basic checks + struct_size = ctypes.sizeof(PluginRuntimeArgs) + + debug_info = { + 'structure_size': struct_size, + 'pointer_size': ctypes.sizeof(ctypes.c_void_p), + 'int_size': ctypes.sizeof(ctypes.c_int), + 'platform': sys.platform, + 'architecture': sys.maxsize > 2**32 and '64-bit' or '32-bit' + } + + # Basic sanity checks + expected_min_size = ( + 13 * ctypes.sizeof(ctypes.c_void_p) + # 13 buffer pointers + 2 * ctypes.sizeof(ctypes.c_void_p) + # 2 function pointers + 1 * ctypes.sizeof(ctypes.c_void_p) + # 1 mutex pointer + 2 * ctypes.sizeof(ctypes.c_int) # 2 integers + ) + + if struct_size < expected_min_size: + return False, f"Structure too small: {struct_size} < {expected_min_size}", debug_info + + # Check field offsets make sense + buffer_size_offset = PluginRuntimeArgs.buffer_size.offset + bits_per_buffer_offset = PluginRuntimeArgs.bits_per_buffer.offset + + if bits_per_buffer_offset <= buffer_size_offset: + return False, "Field offsets are incorrect", debug_info + + debug_info['buffer_size_offset'] = buffer_size_offset + debug_info['bits_per_buffer_offset'] = bits_per_buffer_offset + + return True, "Structure validation passed", debug_info + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, f"Exception during validation: {e}", {} + + @staticmethod + def print_structure_info(): + """Print detailed structure information for debugging""" + is_valid, msg, debug_info = PluginStructureValidator.validate_structure_alignment() + + print("=== Plugin Structure Validation ===") + print(f"Status: {'VALID' if is_valid else 'INVALID'}") + print(f"Message: {msg}") + print("\nStructure Details:") + for key, value in debug_info.items(): + print(f" {key}: {value}") + + print(f"\nField Offsets:") + try: + for field_name, field_type in PluginRuntimeArgs._fields_: + offset = getattr(PluginRuntimeArgs, field_name).offset + print(f" {field_name}: offset {offset}") + except (AttributeError, TypeError) as e: + print(f" Error getting field offsets: {e}") + +class SafeBufferAccess: + """Wrapper class for safe buffer operations with mutex handling""" + + def __init__(self, runtime_args): + """ + Initialize with validated runtime args + Args: + runtime_args: PluginRuntimeArgs instance + """ + self.args = runtime_args + self.is_valid, self.error_msg = runtime_args.validate_pointers() + + @staticmethod + def _handle_buffer_exception(exception, operation_name): + """ + Centralized exception handling for buffer operations + Args: + exception: The caught exception + operation_name: Name of the operation that failed + Returns: + str: Formatted error message + """ + if isinstance(exception, (AttributeError, TypeError)): + return f"Structure access error during {operation_name}: {exception}" + elif isinstance(exception, (ValueError, OverflowError)): + return f"Value validation error during {operation_name}: {exception}" + elif isinstance(exception, OSError): + return f"System error during {operation_name}: {exception}" + elif isinstance(exception, MemoryError): + return f"Memory error during {operation_name}: {exception}" + else: + return f"Unexpected error during {operation_name}: {exception}" + + def read_bool_input(self, buffer_idx, bit_idx, thread_safe=True): + """ + Safely read a boolean input with optional mutex handling + Args: + buffer_idx: Buffer index + bit_idx: Bit index within buffer + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (value, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Access the value - read from the actual value, not the pointer + value = bool(self.args.bool_input[buffer_idx][bit_idx].contents.value) + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + def read_bool_output(self, buffer_idx, bit_idx, thread_safe=True): + """ + Safely read a boolean output with optional mutex handling + Args: + buffer_idx: Buffer index + bit_idx: Bit index within buffer + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (value, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Access the value - read from the actual value, not the pointer + value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + def write_bool_output(self, buffer_idx, bit_idx, value, thread_safe=True): + """ + Safely write a boolean output with optional mutex handling + Args: + buffer_idx: Buffer index + bit_idx: Bit index within buffer + value: Boolean value to write + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + return False, f"Invalid bit index: {bit_idx}" + + # Set the value - access the actual value, not the pointer + self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + # Byte buffer access functions + def read_byte_input(self, buffer_idx, thread_safe=True): + """ + Safely read a byte input with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.byte_input[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_byte_output(self, buffer_idx, value, thread_safe=True): + """ + Safely write a byte output with optional mutex handling + Args: + buffer_idx: Buffer index + value: Byte value to write (0-255) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 255): + return False, f"Invalid byte value: {value} (must be 0-255)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.byte_output[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + def read_byte_output(self, buffer_idx, thread_safe=True): + """ + Safely read a byte output with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.byte_output[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + # Int buffer access functions (IEC_UINT - 16-bit) + def read_int_input(self, buffer_idx, thread_safe=True): + """ + Safely read an int input with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.int_input[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_int_output(self, buffer_idx, value, thread_safe=True): + """ + Safely write an int output with optional mutex handling + Args: + buffer_idx: Buffer index + value: Int value to write (0-65535) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 65535): + return False, f"Invalid int value: {value} (must be 0-65535)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.int_output[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + def read_int_output(self, buffer_idx, thread_safe=True): + """ + Safely read an int output with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.int_output[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + # Dint buffer access functions (IEC_UDINT - 32-bit) + def read_dint_input(self, buffer_idx, thread_safe=True): + """ + Safely read a dint input with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.dint_input[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_dint_output(self, buffer_idx, value, thread_safe=True): + """ + Safely write a dint output with optional mutex handling + Args: + buffer_idx: Buffer index + value: Dint value to write (0-4294967295) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 4294967295): + return False, f"Invalid dint value: {value} (must be 0-4294967295)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.dint_output[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + def read_dint_output(self, buffer_idx, thread_safe=True): + """ + Safely read a dint output with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.dint_output[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + # Lint buffer access functions (IEC_ULINT - 64-bit) + def read_lint_input(self, buffer_idx, thread_safe=True): + """ + Safely read a lint input with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.lint_input[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_lint_output(self, buffer_idx, value, thread_safe=True): + """ + Safely write a lint output with optional mutex handling + Args: + buffer_idx: Buffer index + value: Lint value to write (0-18446744073709551615) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 18446744073709551615): + return False, f"Invalid lint value: {value} (must be 0-18446744073709551615)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.lint_output[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + def read_lint_output(self, buffer_idx, thread_safe=True): + """ + Safely read a lint output with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.lint_output[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + # Memory buffer access functions (IEC_UINT - 16-bit) + def read_int_memory(self, buffer_idx, thread_safe=True): + """ + Safely read an int memory with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.int_memory[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_int_memory(self, buffer_idx, value, thread_safe=True): + """ + Safely write an int memory with optional mutex handling + Args: + buffer_idx: Buffer index + value: Int value to write (0-65535) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 65535): + return False, f"Invalid int value: {value} (must be 0-65535)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.int_memory[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + # Memory buffer access functions (IEC_UDINT - 32-bit) + def read_dint_memory(self, buffer_idx, thread_safe=True): + """ + Safely read a dint memory with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.dint_memory[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_dint_memory(self, buffer_idx, value, thread_safe=True): + """ + Safely write a dint memory with optional mutex handling + Args: + buffer_idx: Buffer index + value: Dint value to write (0-4294967295) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 4294967295): + return False, f"Invalid dint value: {value} (must be 0-4294967295)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.dint_memory[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + # Memory buffer access functions (IEC_ULINT - 64-bit) + def read_lint_memory(self, buffer_idx, thread_safe=True): + """ + Safely read a lint memory with optional mutex handling + Args: + buffer_idx: Buffer index + thread_safe: If True, uses mutex for thread-safe access + Returns: (int, str) - (value, error_message) + """ + if not self.is_valid: + return 0, f"Invalid runtime args: {self.error_msg}" + + try: + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return 0, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return 0, f"Invalid buffer index: {buffer_idx}" + + # Access the value + value = self.args.lint_memory[buffer_idx].contents.value + return value, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return 0, self._handle_buffer_exception(e, "buffer access") + + def write_lint_memory(self, buffer_idx, value, thread_safe=True): + """ + Safely write a lint memory with optional mutex handling + Args: + buffer_idx: Buffer index + value: Lint value to write (0-18446744073709551615) + thread_safe: If True, uses mutex for thread-safe access + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + # Validate value range + if not (0 <= value <= 18446744073709551615): + return False, f"Invalid lint value: {value} (must be 0-18446744073709551615)" + + # Take mutex only if thread_safe is True + mutex_acquired = False + if thread_safe: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + mutex_acquired = True + + try: + # Validate index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + return False, f"Invalid buffer index: {buffer_idx}" + + # Set the value + self.args.lint_memory[buffer_idx].contents.value = value + return True, "Success" + + finally: + # Release mutex only if it was acquired + if mutex_acquired: + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, self._handle_buffer_exception(e, "buffer access") + + # Mutex API functions for manual control + def acquire_mutex(self): + """ + Manually acquire the buffer mutex + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return False, "Failed to acquire mutex" + return True, "Mutex acquired successfully" + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, f"Exception during mutex acquisition: {e}" + + def release_mutex(self): + """ + Manually release the buffer mutex + Returns: (bool, str) - (success, error_message) + """ + if not self.is_valid: + return False, f"Invalid runtime args: {self.error_msg}" + + try: + if self.args.mutex_give(self.args.buffer_mutex) != 0: + return False, "Failed to release mutex" + return True, "Mutex released successfully" + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return False, f"Exception during mutex release: {e}" + + # Batch operations for optimized mutex usage + def batch_read_values(self, operations): + """ + Perform multiple read operations with a single mutex acquisition + Args: + operations: List of tuples describing read operations + Format: [('buffer_type', buffer_idx, bit_idx), ...] + buffer_type can be: 'bool_input', 'bool_output', 'byte_input', 'byte_output', + 'int_input', 'int_output', 'dint_input', 'dint_output', + 'lint_input', 'lint_output', 'int_memory', 'dint_memory', 'lint_memory' + bit_idx is only required for bool operations + Returns: (list, str) - (results, error_message) + results format: [(success, value, error_msg), ...] + """ + if not self.is_valid: + return [], f"Invalid runtime args: {self.error_msg}" + + if not operations: + return [], "No operations provided" + + results = [] + + try: + # Acquire mutex once for all operations + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return [], "Failed to acquire mutex" + + try: + for operation in operations: + try: + if len(operation) < 2: + results.append((False, None, "Invalid operation format")) + continue + + buffer_type = operation[0] + buffer_idx = operation[1] + + # Handle boolean operations (require bit_idx) + if buffer_type in ['bool_input', 'bool_output']: + if len(operation) < 3: + results.append((False, None, "Boolean operations require bit_idx")) + continue + bit_idx = operation[2] + + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + results.append((False, None, f"Invalid buffer index: {buffer_idx}")) + continue + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + results.append((False, None, f"Invalid bit index: {bit_idx}")) + continue + + if buffer_type == 'bool_input': + value = bool(self.args.bool_input[buffer_idx][bit_idx].contents.value) + else: # bool_output + value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) + + results.append((True, value, "Success")) + + # Handle other buffer types + else: + # Validate buffer index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + results.append((False, None, f"Invalid buffer index: {buffer_idx}")) + continue + + if buffer_type == 'byte_input': + value = self.args.byte_input[buffer_idx].contents.value + elif buffer_type == 'byte_output': + value = self.args.byte_output[buffer_idx].contents.value + elif buffer_type == 'int_input': + value = self.args.int_input[buffer_idx].contents.value + elif buffer_type == 'int_output': + value = self.args.int_output[buffer_idx].contents.value + elif buffer_type == 'dint_input': + value = self.args.dint_input[buffer_idx].contents.value + elif buffer_type == 'dint_output': + value = self.args.dint_output[buffer_idx].contents.value + elif buffer_type == 'lint_input': + value = self.args.lint_input[buffer_idx].contents.value + elif buffer_type == 'lint_output': + value = self.args.lint_output[buffer_idx].contents.value + elif buffer_type == 'int_memory': + value = self.args.int_memory[buffer_idx].contents.value + elif buffer_type == 'dint_memory': + value = self.args.dint_memory[buffer_idx].contents.value + elif buffer_type == 'lint_memory': + value = self.args.lint_memory[buffer_idx].contents.value + else: + results.append((False, None, f"Unknown buffer type: {buffer_type}")) + continue + + results.append((True, value, "Success")) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + results.append((False, None, f"Exception during operation: {e}")) + + return results, "Batch read completed" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return [], f"Exception during batch read: {e}" + + def batch_write_values(self, operations): + """ + Perform multiple write operations with a single mutex acquisition + Args: + operations: List of tuples describing write operations + Format: [('buffer_type', buffer_idx, value, bit_idx), ...] + buffer_type can be: 'bool_output', 'byte_output', 'int_output', 'dint_output', + 'lint_output', 'int_memory', 'dint_memory', 'lint_memory' + bit_idx is only required for bool operations (last parameter) + Returns: (list, str) - (results, error_message) + results format: [(success, error_msg), ...] + """ + if not self.is_valid: + return [], f"Invalid runtime args: {self.error_msg}" + + if not operations: + return [], "No operations provided" + + results = [] + + try: + # Acquire mutex once for all operations + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return [], "Failed to acquire mutex" + + try: + for operation in operations: + try: + if len(operation) < 3: + results.append((False, "Invalid operation format")) + continue + + buffer_type = operation[0] + buffer_idx = operation[1] + value = operation[2] + + # Handle boolean operations (require bit_idx) + if buffer_type == 'bool_output': + if len(operation) < 4: + results.append((False, "Boolean operations require bit_idx")) + continue + bit_idx = operation[3] + + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + results.append((False, f"Invalid buffer index: {buffer_idx}")) + continue + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + results.append((False, f"Invalid bit index: {bit_idx}")) + continue + + self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 + results.append((True, "Success")) + + # Handle other buffer types + else: + # Validate buffer index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + results.append((False, f"Invalid buffer index: {buffer_idx}")) + continue + + # Validate value ranges and write + if buffer_type == 'byte_output': + if not (0 <= value <= 255): + results.append((False, f"Invalid byte value: {value} (must be 0-255)")) + continue + self.args.byte_output[buffer_idx].contents.value = value + elif buffer_type == 'int_output': + if not (0 <= value <= 65535): + results.append((False, f"Invalid int value: {value} (must be 0-65535)")) + continue + self.args.int_output[buffer_idx].contents.value = value + elif buffer_type == 'dint_output': + if not (0 <= value <= 4294967295): + results.append((False, f"Invalid dint value: {value} (must be 0-4294967295)")) + continue + self.args.dint_output[buffer_idx].contents.value = value + elif buffer_type == 'lint_output': + if not (0 <= value <= 18446744073709551615): + results.append((False, f"Invalid lint value: {value} (must be 0-18446744073709551615)")) + continue + self.args.lint_output[buffer_idx].contents.value = value + elif buffer_type == 'int_memory': + if not (0 <= value <= 65535): + results.append((False, f"Invalid int value: {value} (must be 0-65535)")) + continue + self.args.int_memory[buffer_idx].contents.value = value + elif buffer_type == 'dint_memory': + if not (0 <= value <= 4294967295): + results.append((False, f"Invalid dint value: {value} (must be 0-4294967295)")) + continue + self.args.dint_memory[buffer_idx].contents.value = value + elif buffer_type == 'lint_memory': + if not (0 <= value <= 18446744073709551615): + results.append((False, f"Invalid lint value: {value} (must be 0-18446744073709551615)")) + continue + self.args.lint_memory[buffer_idx].contents.value = value + else: + results.append((False, f"Unknown buffer type: {buffer_type}")) + continue + + results.append((True, "Success")) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + results.append((False, f"Exception during operation: {e}")) + + return results, "Batch write completed" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return [], f"Exception during batch write: {e}" + + def batch_mixed_operations(self, read_operations, write_operations): + """ + Perform mixed read and write operations with a single mutex acquisition + Args: + read_operations: List of read operation tuples (same format as batch_read_values) + write_operations: List of write operation tuples (same format as batch_write_values) + Returns: (dict, str) - (results, error_message) + results format: {'reads': [(success, value, error_msg), ...], 'writes': [(success, error_msg), ...]} + """ + if not self.is_valid: + return {}, f"Invalid runtime args: {self.error_msg}" + + if not read_operations and not write_operations: + return {}, "No operations provided" + + read_results = [] + write_results = [] + + try: + # Acquire mutex once for all operations + if self.args.mutex_take(self.args.buffer_mutex) != 0: + return {}, "Failed to acquire mutex" + + try: + # Perform read operations first (typically safer) + if read_operations: + for operation in read_operations: + try: + if len(operation) < 2: + read_results.append((False, None, "Invalid operation format")) + continue + + buffer_type = operation[0] + buffer_idx = operation[1] + + # Handle boolean operations (require bit_idx) + if buffer_type in ['bool_input', 'bool_output']: + if len(operation) < 3: + read_results.append((False, None, "Boolean operations require bit_idx")) + continue + bit_idx = operation[2] + + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + read_results.append((False, None, f"Invalid buffer index: {buffer_idx}")) + continue + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + read_results.append((False, None, f"Invalid bit index: {bit_idx}")) + continue + + if buffer_type == 'bool_input': + value = bool(self.args.bool_input[buffer_idx][bit_idx].contents.value) + else: # bool_output + value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) + + read_results.append((True, value, "Success")) + + # Handle other buffer types + else: + # Validate buffer index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + read_results.append((False, None, f"Invalid buffer index: {buffer_idx}")) + continue + + if buffer_type == 'byte_input': + value = self.args.byte_input[buffer_idx].contents.value + elif buffer_type == 'byte_output': + value = self.args.byte_output[buffer_idx].contents.value + elif buffer_type == 'int_input': + value = self.args.int_input[buffer_idx].contents.value + elif buffer_type == 'int_output': + value = self.args.int_output[buffer_idx].contents.value + elif buffer_type == 'dint_input': + value = self.args.dint_input[buffer_idx].contents.value + elif buffer_type == 'dint_output': + value = self.args.dint_output[buffer_idx].contents.value + elif buffer_type == 'lint_input': + value = self.args.lint_input[buffer_idx].contents.value + elif buffer_type == 'lint_output': + value = self.args.lint_output[buffer_idx].contents.value + elif buffer_type == 'int_memory': + value = self.args.int_memory[buffer_idx].contents.value + elif buffer_type == 'dint_memory': + value = self.args.dint_memory[buffer_idx].contents.value + elif buffer_type == 'lint_memory': + value = self.args.lint_memory[buffer_idx].contents.value + else: + read_results.append((False, None, f"Unknown buffer type: {buffer_type}")) + continue + + read_results.append((True, value, "Success")) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + read_results.append((False, None, f"Exception during read operation: {e}")) + + # Perform write operations + if write_operations: + for operation in write_operations: + try: + if len(operation) < 3: + write_results.append((False, "Invalid operation format")) + continue + + buffer_type = operation[0] + buffer_idx = operation[1] + value = operation[2] + + # Handle boolean operations (require bit_idx) + if buffer_type == 'bool_output': + if len(operation) < 4: + write_results.append((False, "Boolean operations require bit_idx")) + continue + bit_idx = operation[3] + + # Validate indices + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + write_results.append((False, f"Invalid buffer index: {buffer_idx}")) + continue + if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: + write_results.append((False, f"Invalid bit index: {bit_idx}")) + continue + + self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 + write_results.append((True, "Success")) + + # Handle other buffer types + else: + # Validate buffer index + if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: + write_results.append((False, f"Invalid buffer index: {buffer_idx}")) + continue + + # Validate value ranges and write + if buffer_type == 'byte_output': + if not (0 <= value <= 255): + write_results.append((False, f"Invalid byte value: {value} (must be 0-255)")) + continue + self.args.byte_output[buffer_idx].contents.value = value + elif buffer_type == 'int_output': + if not (0 <= value <= 65535): + write_results.append((False, f"Invalid int value: {value} (must be 0-65535)")) + continue + self.args.int_output[buffer_idx].contents.value = value + elif buffer_type == 'dint_output': + if not (0 <= value <= 4294967295): + write_results.append((False, f"Invalid dint value: {value} (must be 0-4294967295)")) + continue + self.args.dint_output[buffer_idx].contents.value = value + elif buffer_type == 'lint_output': + if not (0 <= value <= 18446744073709551615): + write_results.append((False, f"Invalid lint value: {value} (must be 0-18446744073709551615)")) + continue + self.args.lint_output[buffer_idx].contents.value = value + elif buffer_type == 'int_memory': + if not (0 <= value <= 65535): + write_results.append((False, f"Invalid int value: {value} (must be 0-65535)")) + continue + self.args.int_memory[buffer_idx].contents.value = value + elif buffer_type == 'dint_memory': + if not (0 <= value <= 4294967295): + write_results.append((False, f"Invalid dint value: {value} (must be 0-4294967295)")) + continue + self.args.dint_memory[buffer_idx].contents.value = value + elif buffer_type == 'lint_memory': + if not (0 <= value <= 18446744073709551615): + write_results.append((False, f"Invalid lint value: {value} (must be 0-18446744073709551615)")) + continue + self.args.lint_memory[buffer_idx].contents.value = value + else: + write_results.append((False, f"Unknown buffer type: {buffer_type}")) + continue + + write_results.append((True, "Success")) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + write_results.append((False, f"Exception during write operation: {e}")) + + return {'reads': read_results, 'writes': write_results}, "Batch mixed operations completed" + + finally: + # Always release mutex + self.args.mutex_give(self.args.buffer_mutex) + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return {}, f"Exception during batch mixed operations: {e}" + +def safe_extract_runtime_args_from_capsule(capsule): + """ + Enhanced capsule extraction with comprehensive validation + Args: + capsule: PyCapsule containing plugin_runtime_args_t structure + Returns: + (PluginRuntimeArgs, str) - (runtime_args, error_message) + """ + try: + # Validate capsule type + if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': + return None, f"Expected PyCapsule object, got {type(capsule)}" + + # Set up the Python API function signatures + ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] + ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p + + # Get the pointer from the capsule + ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") + if not ptr: + return None, "Failed to extract pointer from capsule - invalid capsule name or corrupted data" + + # Cast the pointer to our structure type + args_ptr = ctypes.cast(ptr, ctypes.POINTER(PluginRuntimeArgs)) + if not args_ptr: + return None, "Failed to cast pointer to PluginRuntimeArgs structure" + + runtime_args = args_ptr.contents + + # Validate the extracted structure + is_valid, validation_msg = runtime_args.validate_pointers() + if not is_valid: + return None, f"Structure validation failed: {validation_msg}" + + return runtime_args, "Success" + + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return None, f"Exception during capsule extraction: {e}" + +if __name__ == "__main__": + # Self-test when run directly + print("OpenPLC Python Plugin Types - Self Test") + print("=" * 50) + + # Test structure validation + # PluginStructureValidator.print_structure_info() + + print(f"\nIEC Type Sizes:") + print(f" IEC_BOOL: {ctypes.sizeof(IEC_BOOL)} bytes") + print(f" IEC_BYTE: {ctypes.sizeof(IEC_BYTE)} bytes") + print(f" IEC_UINT: {ctypes.sizeof(IEC_UINT)} bytes") + print(f" IEC_UDINT: {ctypes.sizeof(IEC_UDINT)} bytes") + print(f" IEC_ULINT: {ctypes.sizeof(IEC_ULINT)} bytes") diff --git a/core/src/drivers/plugins/python/simple_modbus.py b/core/src/drivers/plugins/python/simple_modbus.py deleted file mode 100644 index 71c77a96..00000000 --- a/core/src/drivers/plugins/python/simple_modbus.py +++ /dev/null @@ -1,322 +0,0 @@ -import asyncio -import ctypes -import threading -import time -from pymodbus.server import StartAsyncTcpServer, ServerStop -from pymodbus.datastore import ( - ModbusSparseDataBlock, - ModbusDeviceContext, - ModbusServerContext, -) - -# Import the correct type definitions -from python_plugin_types import ( - PluginRuntimeArgs, - safe_extract_runtime_args_from_capsule, - SafeBufferAccess, - PluginStructureValidator -) - -class OpenPLCModbusDataBlock(ModbusSparseDataBlock): - """Custom Modbus data block that mirrors OpenPLC bool_output using SafeBufferAccess""" - - def __init__(self, runtime_args, buffer_index=0, num_coils=64): - self.runtime_args = runtime_args - self.buffer_index = buffer_index - self.num_coils = num_coils - - # Create safe buffer access wrapper - self.safe_buffer_access = SafeBufferAccess(runtime_args) - if not self.safe_buffer_access.is_valid: - print(f"[MODBUS] Warning: Failed to create safe buffer access: {self.safe_buffer_access.error_msg}") - - # Initialize with zeros - super().__init__([0] * num_coils) - - def getValues(self, address, count=1): - """Get coil values from OpenPLC bool_output using SafeBufferAccess""" - print(f"[MODBUS] getValues called: address={address}, count={count}") - address = address - 1 - - if not self.safe_buffer_access.is_valid: - print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") - return [0] * count - - values = [] - for i in range(count): - coil_addr = address + i - - # Use SafeBufferAccess to safely read the boolean value - if coil_addr < self.num_coils: - # Map coil address to buffer and bit indices - # For now, use buffer_index and coil_addr as bit_idx - if coil_addr < 8: # 8 boolean values per buffer - value, error_msg = self.safe_buffer_access.safe_read_bool_output(self.buffer_index, coil_addr) - if error_msg == "Success": - values.append(1 if value else 0) - print(f"[MODBUS] Read coil {coil_addr}: {value}") - else: - print(f"[MODBUS] Error reading coil {coil_addr}: {error_msg}") - values.append(0) - else: - values.append(0) - else: - values.append(0) - - return values - - def setValues(self, address, values): - """Set coil values to OpenPLC bool_output using SafeBufferAccess""" - print(f"[MODBUS] setValues called: address={address}, values={values}") - - if not self.safe_buffer_access.is_valid: - print(f"[MODBUS] Error: Safe buffer access not valid: {self.safe_buffer_access.error_msg}") - return - - for i, value in enumerate(values): - coil_addr = address + i - - # Use SafeBufferAccess to safely write the boolean value - if coil_addr < self.num_coils: - # Map coil address to buffer and bit indices - # For now, use buffer_index and coil_addr as bit_idx - if coil_addr < 8: # 8 boolean values per buffer - success, error_msg = self.safe_buffer_access.safe_write_bool_output(self.buffer_index, coil_addr, bool(value)) - if error_msg == "Success": - print(f"[MODBUS] Set coil {coil_addr}: {bool(value)}") - else: - print(f"[MODBUS] Error setting coil {coil_addr}: {error_msg}") - -# Global variables for plugin lifecycle -server_task = None -server_context = None -runtime_args = None -update_thread = None -running = False -gIp = "172.29.65.104" -gPort = 5020 - -def init(args_capsule, host="172.29.65.104", port=5020): - """Initialize the Modbus plugin""" - global runtime_args, server_context, gIp, gPort - gIp = host - gPort = port - - print("[MODBUS] Python plugin 'simple_modbus' initializing...") - - try: - # Print structure validation info for debugging - print("[MODBUS] Validating plugin structure alignment...") - PluginStructureValidator.print_structure_info() - - # Extract runtime args from capsule using safe method - if hasattr(args_capsule, '__class__') and 'PyCapsule' in str(type(args_capsule)): - # This is a PyCapsule from C - use safe extraction - runtime_args, error_msg = safe_extract_runtime_args_from_capsule(args_capsule) - if runtime_args is None: - print(f"[MODBUS] ✗ Failed to extract runtime args: {error_msg}") - return False - - print(f"[MODBUS] ✓ Runtime arguments extracted successfully") - else: - # This is a direct object (for testing) - runtime_args = args_capsule - print(f"[MODBUS] ✓ Using direct runtime args for testing") - - # Safely access buffer size using validation - buffer_size, size_error = runtime_args.safe_access_buffer_size() - if buffer_size == -1: - print(f"[MODBUS] ✗ Failed to access buffer size: {size_error}") - return False - - print(f"[MODBUS] Buffer size: {buffer_size}") - print(f"[MODBUS] Bits per buffer: {runtime_args.bits_per_buffer}") - print(f"[MODBUS] Structure details: {runtime_args}") - - # Create OpenPLC-connected coils data block - coils_block = OpenPLCModbusDataBlock(runtime_args, buffer_index=0, num_coils=64) - - # Standard data blocks for other Modbus types - di = ModbusSparseDataBlock([0] * 64) # Discrete Inputs - ir = ModbusSparseDataBlock([0] * 32) # Input Registers (16-bit) - hr = ModbusSparseDataBlock([0] * 32) # Holding Registers (16-bit) - - # Create device context with OpenPLC-connected coils - print(f"[MODBUS] coils_block created with {coils_block} coils") - device = ModbusDeviceContext(di=di, co=coils_block, ir=ir, hr=hr) - server_context = ModbusServerContext(devices={1: device}, single=False) - - print(f"[MODBUS] ✓ Plugin initialized successfully - Host: {host}, Port: {port}") - return True - - except Exception as e: - print(f"[MODBUS] ✗ Plugin initialization failed: {e}") - import traceback - traceback.print_exc() - return False - -def start_loop(): - """Start the Modbus server""" - global server_task, running, update_thread, gIp, gPort - - if server_context is None: - print("[MODBUS] Error: Plugin not initialized") - return False - - print("[MODBUS] Server context is valid, proceeding with startup...") - print(f"[MODBUS] Server context created successfully") - - running = True - - # Start server in separate thread with proper asyncio handling - def run_server(): - try: - print("[MODBUS] Creating new event loop for server thread...") - # Create new event loop for this thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - print("[MODBUS] Event loop created successfully") - - # Start the server and keep it running - async def start_server(): - try: - print(f"[MODBUS] Attempting to start TCP server on {gIp}:{gPort}...") - try: - server = await StartAsyncTcpServer( - context=server_context, - address=(gIp, gPort) - ) - print(f"[MODBUS] Server successfully bound to {gIp}:{gPort}") - except Exception as bind_error: - print(f"[MODBUS] Failed to bind to {gIp}:{gPort}: {bind_error}") - print(f"[MODBUS] Attempting to bind to 0.0.0.0:{gPort} as fallback...") - server = await StartAsyncTcpServer( - context=server_context, - address=("0.0.0.0", gPort) - ) - print(f"[MODBUS] Server successfully bound to 0.0.0.0:{gPort} (fallback)") - - # Keep the server running - try: - print("[MODBUS] Server is now running and accepting connections") - while running: - await asyncio.sleep(1) - except asyncio.CancelledError: - print("[MODBUS] Server cancelled") - finally: - print("[MODBUS] Shutting down server...") - if hasattr(server, 'close'): - server.close() - if hasattr(server, 'wait_closed'): - await server.wait_closed() - print("[MODBUS] Server shutdown complete") - - except Exception as server_error: - print(f"[MODBUS] Error in start_server async function: {server_error}") - import traceback - print(f"[MODBUS] Traceback: {traceback.format_exc()}") - raise - - # Run the server - print("[MODBUS] Running server event loop...") - loop.run_until_complete(start_server()) - - except Exception as e: - print(f"[MODBUS] Error in run_server thread: {e}") - import traceback - print(f"[MODBUS] Full traceback: {traceback.format_exc()}") - finally: - print("[MODBUS] Closing event loop...") - loop.close() - print("[MODBUS] Event loop closed") - - server_task = threading.Thread(target=run_server, daemon=False) - server_task.start() - - print(f"[MODBUS] Server thread started on {gIp}:{gPort}") - return True - -def stop_loop(): - """Stop the Modbus server""" - global server_task, running, update_thread - - running = False - - if update_thread: - update_thread.join(timeout=1.0) - update_thread = None - - if server_task: - # Stop the asyncio server - try: - asyncio.run(ServerStop()) - except: - pass - - server_task.join(timeout=2.0) - server_task = None - - print("[MODBUS] Server stopped") - return True - -def cleanup(): - """Cleanup plugin resources""" - global server_context, runtime_args - - server_context = None - runtime_args = None - - print("[MODBUS] Plugin cleaned up") - return True - -async def main(): - """Standalone server for testing""" - # Create a proper mock runtime args that inherits from PluginRuntimeArgs - import ctypes - - # Create a mock that has the required methods - class MockArgs: - def __init__(self): - self.buffer_size = 1 - self.bits_per_buffer = 8 - # Create simple boolean list for testing - self.bool_data = [[False] * 8] # 1 buffer, 8 booleans - self.bool_output = self.bool_data # Simple reference - self.mutex_take = None - self.mutex_give = None - self.buffer_mutex = None - - def safe_access_buffer_size(self): - """Mock implementation of safe_access_buffer_size""" - return self.buffer_size, "Success" - - def validate_pointers(self): - """Mock implementation of validate_pointers""" - return True, "Mock validation passed" - - def __str__(self): - return f"MockArgs(buffer_size={self.buffer_size}, bits_per_buffer={self.bits_per_buffer})" - - mock_args = MockArgs() - - # Initialize and start - if init(mock_args): - if start_loop(): - print(f"Modbus server running on {gIp}:{gPort}") - print("Press Ctrl+C to stop...") - - try: - # Keep server running - while True: - await asyncio.sleep(1) - except KeyboardInterrupt: - print("\nStopping server...") - stop_loop() - cleanup() - else: - print("Failed to start server") - else: - print("Failed to initialize plugin") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/core/src/drivers/python_plugin_types.py b/core/src/drivers/python_plugin_types.py deleted file mode 100644 index f9275bc6..00000000 --- a/core/src/drivers/python_plugin_types.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 -""" -Shared type definitions for OpenPLC Python plugins -This module provides correct ctypes mappings for the plugin_runtime_args_t structure -""" - -import ctypes -from ctypes import * -import sys - -# IEC type mappings based on iec_types.h -# These must match exactly with the C definitions -IEC_BOOL = ctypes.c_uint8 # typedef uint8_t IEC_BOOL; -IEC_BYTE = ctypes.c_uint8 # typedef uint8_t IEC_BYTE; -IEC_UINT = ctypes.c_uint16 # typedef uint16_t IEC_UINT; -IEC_UDINT = ctypes.c_uint32 # typedef uint32_t IEC_UDINT; -IEC_ULINT = ctypes.c_uint64 # typedef uint64_t IEC_ULINT; - -class PluginRuntimeArgs(ctypes.Structure): - """ - Python ctypes structure matching plugin_runtime_args_t from plugin_driver.h - - CRITICAL: This structure must match the C definition exactly to prevent - segmentation faults and memory corruption. - """ - _fields_ = [ - # Buffer arrays - these are pointers to arrays of pointers - # C: IEC_BOOL *(*bool_input)[8] means pointer to array of 8 pointers - ("bool_input", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), - ("bool_output", ctypes.POINTER(ctypes.POINTER(IEC_BOOL) * 8)), - ("byte_input", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), - ("byte_output", ctypes.POINTER(ctypes.POINTER(IEC_BYTE))), - ("int_input", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), - ("int_output", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), - ("dint_input", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), - ("dint_output", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), - ("lint_input", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), - ("lint_output", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), - ("int_memory", ctypes.POINTER(ctypes.POINTER(IEC_UINT))), - ("dint_memory", ctypes.POINTER(ctypes.POINTER(IEC_UDINT))), - ("lint_memory", ctypes.POINTER(ctypes.POINTER(IEC_ULINT))), - - # Mutex function pointers - ("mutex_take", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), - ("mutex_give", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), - ("buffer_mutex", ctypes.c_void_p), - - # Buffer size information - ("buffer_size", ctypes.c_int), - ("bits_per_buffer", ctypes.c_int), - ] - - def validate_pointers(self): - """ - Validate that critical pointers are not NULL - Returns: (bool, str) - (is_valid, error_message) - """ - try: - # Check buffer mutex - if not self.buffer_mutex: - return False, "buffer_mutex is NULL" - - # Check mutex functions - if not self.mutex_take: - return False, "mutex_take function pointer is NULL" - if not self.mutex_give: - return False, "mutex_give function pointer is NULL" - - # Check buffer size is reasonable - if self.buffer_size <= 0 or self.buffer_size > 10000: - return False, f"buffer_size is invalid: {self.buffer_size}" - - if self.bits_per_buffer <= 0 or self.bits_per_buffer > 64: - return False, f"bits_per_buffer is invalid: {self.bits_per_buffer}" - - return True, "All pointers valid" - - except Exception as e: - return False, f"Exception during validation: {e}" - - def safe_access_buffer_size(self): - """ - Safely access buffer_size with validation - Returns: (int, str) - (buffer_size, error_message) - """ - try: - is_valid, msg = self.validate_pointers() - if not is_valid: - return -1, f"Validation failed: {msg}" - - size = self.buffer_size - if size <= 0 or size > 10000: - return -1, f"Invalid buffer size: {size}" - - return size, "Success" - - except Exception as e: - return -1, f"Exception accessing buffer_size: {e}" - - def __str__(self): - """Debug representation of the structure""" - try: - return (f"PluginRuntimeArgs(\n" - f" bool_input=0x{ctypes.addressof(self.bool_input.contents) if self.bool_input else 0:x},\n" - f" bool_output=0x{ctypes.addressof(self.bool_output.contents) if self.bool_output else 0:x},\n" - f" byte_input=0x{ctypes.addressof(self.byte_input.contents) if self.byte_input else 0:x},\n" - f" byte_output=0x{ctypes.addressof(self.byte_output.contents) if self.byte_output else 0:x},\n" - f" int_input=0x{ctypes.addressof(self.int_input.contents) if self.int_input else 0:x},\n" - f" int_output=0x{ctypes.addressof(self.int_output.contents) if self.int_output else 0:x},\n" - f" dint_input=0x{ctypes.addressof(self.dint_input.contents) if self.dint_input else 0:x},\n" - f" dint_output=0x{ctypes.addressof(self.dint_output.contents) if self.dint_output else 0:x},\n" - f" lint_input=0x{ctypes.addressof(self.lint_input.contents) if self.lint_input else 0:x},\n" - f" lint_output=0x{ctypes.addressof(self.lint_output.contents) if self.lint_output else 0:x},\n" - f" int_memory=0x{ctypes.addressof(self.int_memory.contents) if self.int_memory else 0:x},\n" - f" buffer_size={self.buffer_size},\n" - f" bits_per_buffer={self.bits_per_buffer},\n" - f" buffer_mutex=0x{self.buffer_mutex or 0:x},\n" - f" mutex_take={'valid' if self.mutex_take else 'NULL'},\n" - f" mutex_give={'valid' if self.mutex_give else 'NULL'}\n" - f")") - except: - return "PluginRuntimeArgs(corrupted or invalid)" - -class PluginStructureValidator: - """Validates structure alignment and provides debugging tools""" - - @staticmethod - def validate_structure_alignment(): - """ - Validates that the Python ctypes structure has the expected size and alignment - Returns: (bool, str, dict) - (is_valid, message, debug_info) - """ - try: - # Calculate expected structure size - # This is platform-dependent but we can do basic checks - struct_size = ctypes.sizeof(PluginRuntimeArgs) - - debug_info = { - 'structure_size': struct_size, - 'pointer_size': ctypes.sizeof(ctypes.c_void_p), - 'int_size': ctypes.sizeof(ctypes.c_int), - 'platform': sys.platform, - 'architecture': sys.maxsize > 2**32 and '64-bit' or '32-bit' - } - - # Basic sanity checks - expected_min_size = ( - 13 * ctypes.sizeof(ctypes.c_void_p) + # 13 buffer pointers - 2 * ctypes.sizeof(ctypes.c_void_p) + # 2 function pointers - 1 * ctypes.sizeof(ctypes.c_void_p) + # 1 mutex pointer - 2 * ctypes.sizeof(ctypes.c_int) # 2 integers - ) - - if struct_size < expected_min_size: - return False, f"Structure too small: {struct_size} < {expected_min_size}", debug_info - - # Check field offsets make sense - buffer_size_offset = PluginRuntimeArgs.buffer_size.offset - bits_per_buffer_offset = PluginRuntimeArgs.bits_per_buffer.offset - - if bits_per_buffer_offset <= buffer_size_offset: - return False, "Field offsets are incorrect", debug_info - - debug_info['buffer_size_offset'] = buffer_size_offset - debug_info['bits_per_buffer_offset'] = bits_per_buffer_offset - - return True, "Structure validation passed", debug_info - - except Exception as e: - return False, f"Exception during validation: {e}", {} - - @staticmethod - def print_structure_info(): - """Print detailed structure information for debugging""" - is_valid, msg, debug_info = PluginStructureValidator.validate_structure_alignment() - - print("=== Plugin Structure Validation ===") - print(f"Status: {'VALID' if is_valid else 'INVALID'}") - print(f"Message: {msg}") - print("\nStructure Details:") - for key, value in debug_info.items(): - print(f" {key}: {value}") - - print(f"\nField Offsets:") - try: - for field_name, field_type in PluginRuntimeArgs._fields_: - offset = getattr(PluginRuntimeArgs, field_name).offset - print(f" {field_name}: offset {offset}") - except Exception as e: - print(f" Error getting field offsets: {e}") - -class SafeBufferAccess: - """Wrapper class for safe buffer operations with mutex handling""" - - def __init__(self, runtime_args): - """ - Initialize with validated runtime args - Args: - runtime_args: PluginRuntimeArgs instance - """ - self.args = runtime_args - self.is_valid, self.error_msg = runtime_args.validate_pointers() - - def safe_read_bool_output(self, buffer_idx, bit_idx): - """ - Safely read a boolean output with proper mutex handling - Returns: (bool, str) - (value, error_message) - """ - if not self.is_valid: - return False, f"Invalid runtime args: {self.error_msg}" - - try: - # Take mutex - if self.args.mutex_take(self.args.buffer_mutex) != 0: - return False, "Failed to acquire mutex" - - try: - # Validate indices - if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: - return False, f"Invalid buffer index: {buffer_idx}" - if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: - return False, f"Invalid bit index: {bit_idx}" - - # Access the value - read from the actual value, not the pointer - value = bool(self.args.bool_output[buffer_idx][bit_idx].contents.value) - return value, "Success" - - finally: - # Always release mutex - self.args.mutex_give(self.args.buffer_mutex) - - except Exception as e: - return False, f"Exception during buffer access: {e}" - - def safe_write_bool_output(self, buffer_idx, bit_idx, value): - """ - Safely write a boolean output with proper mutex handling - Returns: (bool, str) - (success, error_message) - """ - if not self.is_valid: - return False, f"Invalid runtime args: {self.error_msg}" - - try: - # Take mutex - if self.args.mutex_take(self.args.buffer_mutex) != 0: - return False, "Failed to acquire mutex" - - try: - # Validate indices - if buffer_idx < 0 or buffer_idx >= self.args.buffer_size: - return False, f"Invalid buffer index: {buffer_idx}" - if bit_idx < 0 or bit_idx >= self.args.bits_per_buffer: - return False, f"Invalid bit index: {bit_idx}" - - # Set the value - access the actual value, not the pointer - self.args.bool_output[buffer_idx][bit_idx].contents.value = 1 if value else 0 - return True, "Success" - - finally: - # Always release mutex - self.args.mutex_give(self.args.buffer_mutex) - - except Exception as e: - return False, f"Exception during buffer access: {e}" - -def safe_extract_runtime_args_from_capsule(capsule): - """ - Enhanced capsule extraction with comprehensive validation - Args: - capsule: PyCapsule containing plugin_runtime_args_t structure - Returns: - (PluginRuntimeArgs, str) - (runtime_args, error_message) - """ - try: - # Validate capsule type - if not hasattr(capsule, '__class__') or capsule.__class__.__name__ != 'PyCapsule': - return None, f"Expected PyCapsule object, got {type(capsule)}" - - # Set up the Python API function signatures - ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] - ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p - - # Get the pointer from the capsule - ptr = ctypes.pythonapi.PyCapsule_GetPointer(capsule, b"openplc_runtime_args") - if not ptr: - return None, "Failed to extract pointer from capsule - invalid capsule name or corrupted data" - - # Cast the pointer to our structure type - args_ptr = ctypes.cast(ptr, ctypes.POINTER(PluginRuntimeArgs)) - if not args_ptr: - return None, "Failed to cast pointer to PluginRuntimeArgs structure" - - runtime_args = args_ptr.contents - - # Validate the extracted structure - is_valid, validation_msg = runtime_args.validate_pointers() - if not is_valid: - return None, f"Structure validation failed: {validation_msg}" - - return runtime_args, "Success" - - except Exception as e: - return None, f"Exception during capsule extraction: {e}" - -if __name__ == "__main__": - # Self-test when run directly - print("OpenPLC Python Plugin Types - Self Test") - print("=" * 50) - - # Test structure validation - PluginStructureValidator.print_structure_info() - - print(f"\nIEC Type Sizes:") - print(f" IEC_BOOL: {ctypes.sizeof(IEC_BOOL)} bytes") - print(f" IEC_BYTE: {ctypes.sizeof(IEC_BYTE)} bytes") - print(f" IEC_UINT: {ctypes.sizeof(IEC_UINT)} bytes") - print(f" IEC_UDINT: {ctypes.sizeof(IEC_UDINT)} bytes") - print(f" IEC_ULINT: {ctypes.sizeof(IEC_ULINT)} bytes") diff --git a/plugins.conf b/plugins.conf index 0c0df5cd..a83e8278 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,4 +1,4 @@ # Plugin configuration file # Format: name,path,enabled,type,config_path -# modbus_slave,../core/src/drivers/plugins/python/simple_modbus.py,1,0,./modbus_slave_config.ini -example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./modbus_slave_config.ini +modbus_slave,./core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py,1,0,./modbus_slave_config.ini +# example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./example_plugin_config.ini From 9cf4e5f83209098918315a51a30a2b3ecaf83e80 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Wed, 24 Sep 2025 13:14:29 +0200 Subject: [PATCH 24/44] fixing plugin's dedicated data retrieval --- core/src/drivers/plugin_driver.c | 13 +++- core/src/drivers/plugin_driver.h | 4 +- .../modbus_slave_config.ini | 23 ------- .../modbus_slave_config.json | 21 +++++++ .../modbus_slave_plugin/simple_modbus.py | 38 ++++++++---- .../python/shared/python_plugin_types.py | 60 +++++++++++++++++++ plugins.conf | 3 +- 7 files changed, 123 insertions(+), 39 deletions(-) delete mode 100644 core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.ini create mode 100644 core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 5262a62a..ab7617a4 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -150,7 +150,7 @@ int plugin_driver_init(plugin_driver_t *driver) { // Generate structured args for Python plugin PyObject *args = - (PyObject *)generate_structured_args_with_driver(PLUGIN_TYPE_PYTHON, driver); + (PyObject *)generate_structured_args_with_driver(PLUGIN_TYPE_PYTHON, driver, i); if (!args) { fprintf(stderr, "Failed to generate runtime args for plugin: %s\n", @@ -330,7 +330,8 @@ void plugin_driver_destroy(plugin_driver_t *driver) * For PLUGIN_TYPE_NATIVE: Returns plugin_runtime_args_t* * For PLUGIN_TYPE_PYTHON: Returns PyObject* (PyCapsule containing plugin_runtime_args_t*) */ -void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver) +void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver, + int plugin_index) { printf("[PLUGIN]: Generating structured args for plugin type %d\n", type); @@ -371,6 +372,14 @@ void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t * // Set buffer mutex from driver args->buffer_mutex = &driver->buffer_mutex; + // Initialize plugin specific config path as empty + memset(args->plugin_specific_config_file_path, '\0', + sizeof(args->plugin_specific_config_file_path)); + + memcpy(args->plugin_specific_config_file_path, + driver->plugins[plugin_index].config.plugin_related_config_path, + sizeof(driver->plugins[plugin_index].config.plugin_related_config_path)); + // Initialize buffer size info args->buffer_size = BUFFER_SIZE; args->bits_per_buffer = 8; diff --git a/core/src/drivers/plugin_driver.h b/core/src/drivers/plugin_driver.h index 6893f25c..f7eae73d 100644 --- a/core/src/drivers/plugin_driver.h +++ b/core/src/drivers/plugin_driver.h @@ -56,6 +56,7 @@ typedef struct int (*mutex_take)(pthread_mutex_t *mutex); int (*mutex_give)(pthread_mutex_t *mutex); pthread_mutex_t *buffer_mutex; + char plugin_specific_config_file_path[256]; // Buffer size information int buffer_size; @@ -94,7 +95,8 @@ int plugin_mutex_give(pthread_mutex_t *mutex); int python_plugin_get_symbols(plugin_instance_t *plugin); // Runtime arguments generation -void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver); +void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t *driver, + int plugin_index); void free_structured_args(plugin_runtime_args_t *args); #endif // PLUGIN_DRIVER_H diff --git a/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.ini b/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.ini deleted file mode 100644 index 70e9ff40..00000000 --- a/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.ini +++ /dev/null @@ -1,23 +0,0 @@ -# Modbus Slave Plugin Configuration Example -# This file shows how to configure the Modbus slave plugin - -[plugin_modbus_slave] -type = PLUGIN_TYPE_PYTHON -path = /home/marcone/Documents/Github/openplc-runtime/core/src/drivers/modbus_slave.py -enabled = true -description = "Modbus TCP Slave server that exposes OpenPLC buffers" - -# Network configuration -host = 172.29.65.104 -port = 5020 - -# Buffer mapping configuration -# Coils (Read/Write) -> bool_output[0-7999] (1000 buffers * 8 bits) -# Discrete Inputs (Read Only) -> bool_input[0-7999] (1000 buffers * 8 bits) -# Holding Registers -> int_output[0-999] (future implementation) -# Input Registers -> int_input[0-999] (future implementation) - -max_coils = 8000 -max_discrete_inputs = 8000 -max_holding_registers = 1000 -max_input_registers = 1000 diff --git a/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json b/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json new file mode 100644 index 00000000..984972e0 --- /dev/null +++ b/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json @@ -0,0 +1,21 @@ +{ + "_comment": "Modbus Slave Plugin Configuration Example - JSON format", + "_description": "This file shows how to configure the Modbus slave plugin", + "plugin_modbus_slave": { + "type": "PLUGIN_TYPE_PYTHON", + "path": "/home/marcone/Documents/Github/openplc-runtime/core/src/drivers/modbus_slave.py", + "enabled": true, + "description": "Modbus TCP Slave server that exposes OpenPLC buffers" + }, + "network_configuration": { + "host": "172.29.65.104", + "port": 5024 + }, + "buffer_mapping": { + "_comment": "Buffer mapping configuration", + "max_coils": 8000, + "max_discrete_inputs": 8000, + "max_holding_registers": 1000, + "max_input_registers": 1000 + } +} \ No newline at end of file diff --git a/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py b/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py index 703322a7..22d316b0 100644 --- a/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py +++ b/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py @@ -5,6 +5,7 @@ import time import sys import os +import json from pymodbus.server import StartAsyncTcpServer, ServerStop from pymodbus.datastore import ( ModbusSparseDataBlock, @@ -290,16 +291,13 @@ def setValues(self, address, values): server_task = None server_context = None runtime_args = None -update_thread = None running = False -gIp = "172.29.65.104" +gIp = "172.29.65.104" # Default values gPort = 5020 -def init(args_capsule, host="172.29.65.104", port=5020): +def init(args_capsule): """Initialize the Modbus plugin""" global runtime_args, server_context, gIp, gPort - gIp = host - gPort = port print("[MODBUS] Python plugin 'simple_modbus' initializing...") @@ -322,6 +320,26 @@ def init(args_capsule, host="172.29.65.104", port=5020): runtime_args = args_capsule print(f"[MODBUS] ✓ Using direct runtime args for testing") + # Try to load configuration from plugin_specific_config_file_path + try: + config_map, status = SafeBufferAccess(runtime_args).get_config_file_args_as_map() + if status == "Success" and config_map: + # Try to extract network configuration + network_config = config_map.get('network_configuration', {}) + if network_config and 'host' in network_config and 'port' in network_config: + gIp = str(network_config['host']) + gPort = int(network_config['port']) + print(f"[MODBUS] ✓ Configuration loaded - Host: {gIp}, Port: {gPort}") + else: + print(f"[MODBUS] ⚠ Config file loaded but network_configuration section missing or incomplete - using defaults") + print(f"[MODBUS] Available config sections: {list(config_map.keys())}") + else: + print(f"[MODBUS] ✗ Failed to load configuration file: {status} - using defaults") + except Exception as config_error: + print(f"[MODBUS] ⚠ Exception while loading config: {config_error} - using defaults") + import traceback + traceback.print_exc() + # Safely access buffer size using validation buffer_size, size_error = runtime_args.safe_access_buffer_size() if buffer_size == -1: @@ -353,7 +371,7 @@ def init(args_capsule, host="172.29.65.104", port=5020): ) server_context = ModbusServerContext(devices={1: device}, single=False) - print(f"[MODBUS] ✓ Plugin initialized successfully - Host: {host}, Port: {port}") + print(f"[MODBUS] ✓ Plugin initialized successfully - Host: {gIp}, Port: {gPort}") return True except Exception as e: @@ -364,7 +382,7 @@ def init(args_capsule, host="172.29.65.104", port=5020): def start_loop(): """Start the Modbus server""" - global server_task, running, update_thread, gIp, gPort + global server_task, running, gIp, gPort if server_context is None: print("[MODBUS] Error: Plugin not initialized") @@ -445,14 +463,10 @@ async def start_server(): def stop_loop(): """Stop the Modbus server""" - global server_task, running, update_thread + global server_task, running running = False - if update_thread: - update_thread.join(timeout=1.0) - update_thread = None - if server_task: # Stop the asyncio server try: diff --git a/core/src/drivers/plugins/python/shared/python_plugin_types.py b/core/src/drivers/plugins/python/shared/python_plugin_types.py index 9ad9ae56..46a61a2d 100644 --- a/core/src/drivers/plugins/python/shared/python_plugin_types.py +++ b/core/src/drivers/plugins/python/shared/python_plugin_types.py @@ -6,6 +6,7 @@ import ctypes from ctypes import * +import json import sys # IEC type mappings based on iec_types.h @@ -44,6 +45,7 @@ class PluginRuntimeArgs(ctypes.Structure): ("mutex_take", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), ("mutex_give", ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)), ("buffer_mutex", ctypes.c_void_p), + ("plugin_specific_config_file_path", ctypes.c_char * 256), # Buffer size information ("buffer_size", ctypes.c_int), @@ -1475,6 +1477,64 @@ def batch_mixed_operations(self, read_operations, write_operations): except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: return {}, f"Exception during batch mixed operations: {e}" + + def get_config_path(self): + """ + Retrieve the plugin-specific configuration file path + Returns: (str, str) - (config_path, error_message) + """ + if not self.is_valid: + return "", f"Invalid runtime args: {self.error_msg}" + + try: + config_path_bytes = self.args.plugin_specific_config_file_path + + # Handle different types of C char arrays + if isinstance(config_path_bytes, (bytes, bytearray)): + config_path = config_path_bytes.decode('utf-8').rstrip('\x00') + elif hasattr(config_path_bytes, 'value'): + config_path = config_path_bytes.value.decode('utf-8').rstrip('\x00') + elif hasattr(config_path_bytes, 'raw'): + config_path = config_path_bytes.raw.decode('utf-8').rstrip('\x00') + else: + # Try to convert to bytes first + config_path = bytes(config_path_bytes).decode('utf-8').rstrip('\x00') + + # Clean up the path - remove all whitespace and control characters + config_path = config_path.strip() + + return config_path, "Success" + except (AttributeError, TypeError, ValueError, OverflowError, OSError, MemoryError) as e: + return "", f"Exception retrieving config path: {e}" + + def get_config_file_args_as_map(self): + """ + Parse the plugin-specific configuration file as a key-value map + Supports JSON format for flexibility + Returns: (dict, str) - (config_map, error_message) + """ + import os + + config_path, err_msg = self.get_config_path() + if not config_path: + return {}, f"Failed to get config path: {err_msg}" + + # Debug information + debug_info = f"Original path: '{config_path}', CWD: '{os.getcwd()}'" + + try: + with open(config_path, 'r') as config_file: + config_data = json.load(config_file) + if not isinstance(config_data, dict): + return {}, "Configuration file must contain a JSON object at the top level" + return config_data, "Success" + except FileNotFoundError: + return {}, f"Configuration file not found: {config_path}" + except json.JSONDecodeError as e: + return {}, f"JSON parsing error in config file {config_path}: {e}" + except (OSError, MemoryError) as e: + return {}, f"Exception reading config file {config_path}: {e}" + def safe_extract_runtime_args_from_capsule(capsule): """ diff --git a/plugins.conf b/plugins.conf index a83e8278..4b25fe01 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,4 +1,5 @@ # Plugin configuration file # Format: name,path,enabled,type,config_path -modbus_slave,./core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py,1,0,./modbus_slave_config.ini +modbus_slave,./core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py,1,0,./core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json # example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./example_plugin_config.ini + From 9bb279c7040643b7a0facf85681b597ff45988e6 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Thu, 25 Sep 2025 10:10:54 +0200 Subject: [PATCH 25/44] RTOP 74 adding plugin individual venv usage --- .gitignore | 2 +- core/src/drivers/plugin_config.c | 13 + core/src/drivers/plugin_config.h | 1 + core/src/drivers/plugin_config_example.txt | 6 - core/src/drivers/plugin_driver.c | 21 ++ .../modbus_slave_config.json | 0 .../simple_modbus.py | 0 docs/PLUGIN_VENV_GUIDE.md | 189 ++++++++++++ plugins.conf | 9 +- scripts/manage_plugin_venvs.sh | 283 ++++++++++++++++++ 10 files changed, 513 insertions(+), 11 deletions(-) delete mode 100644 core/src/drivers/plugin_config_example.txt rename core/src/drivers/plugins/python/{modbus_slave_plugin => modbus_slave}/modbus_slave_config.json (100%) rename core/src/drivers/plugins/python/{modbus_slave_plugin => modbus_slave}/simple_modbus.py (100%) create mode 100644 docs/PLUGIN_VENV_GUIDE.md create mode 100755 scripts/manage_plugin_venvs.sh diff --git a/.gitignore b/.gitignore index 50c20853..a25a1049 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ /memory-bank # .vscode/ .*/ -venv/ +/venvs/ __pycache__/ .clinerules # Temporary files diff --git a/core/src/drivers/plugin_config.c b/core/src/drivers/plugin_config.c index 74dfd3b3..5971ca2b 100644 --- a/core/src/drivers/plugin_config.c +++ b/core/src/drivers/plugin_config.c @@ -54,6 +54,19 @@ int parse_plugin_config(const char *config_file, plugin_config_t *configs, int m strncpy(configs[config_count].plugin_related_config_path, token, sizeof(configs[config_count].plugin_related_config_path) - 1); + // parsing venv_path (optional field) + token = strtok(NULL, ",\n\r"); + if (token) + { + strncpy(configs[config_count].venv_path, token, + sizeof(configs[config_count].venv_path) - 1); + } + else + { + // No venv_path specified, use empty string + configs[config_count].venv_path[0] = '\0'; + } + // Incrementing index to target next config config_count++; } diff --git a/core/src/drivers/plugin_config.h b/core/src/drivers/plugin_config.h index ab287f7f..5a88c517 100644 --- a/core/src/drivers/plugin_config.h +++ b/core/src/drivers/plugin_config.h @@ -11,6 +11,7 @@ typedef struct int enabled; int type; // 0 = native, 1 = python char plugin_related_config_path[MAX_PLUGIN_PATH_LEN]; + char venv_path[MAX_PLUGIN_PATH_LEN]; // Path to virtual environment } plugin_config_t; int parse_plugin_config(const char *config_file, plugin_config_t *configs, int max_configs); diff --git a/core/src/drivers/plugin_config_example.txt b/core/src/drivers/plugin_config_example.txt deleted file mode 100644 index 9fc52c28..00000000 --- a/core/src/drivers/plugin_config_example.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Plugin configuration file -# Format: name,path,enabled,type,plugin_related_config_path -# Example plugins - -# modbus_slave,./core/src/drivers/plugins/python/simple_modbus.py,1,0,./modbus_slave_config.ini -example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./modbus_slave_config.ini diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index ab7617a4..412ae7e8 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -507,6 +507,27 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) PyRun_SimpleString(python_path_cmd); + // Setup virtual environment if specified + if (strlen(plugin->config.venv_path) > 0) + { + char venv_setup_cmd[1024]; + snprintf(venv_setup_cmd, sizeof(venv_setup_cmd), + "import sys\n" + "venv_path = '%s/lib/python%d.%d/site-packages'\n" + "if venv_path not in sys.path:\n" + " sys.path.insert(0, venv_path)\n" + "print('[PLUGIN] Using venv for %s: %s')", + plugin->config.venv_path, PY_MAJOR_VERSION, PY_MINOR_VERSION, plugin->config.name, + plugin->config.venv_path); + + if (PyRun_SimpleString(venv_setup_cmd) != 0) + { + fprintf(stderr, "Failed to setup venv for plugin: %s\n", plugin->config.name); + free(py_binds); + return -1; + } + } + // Load the Python module py_binds->pModule = PyImport_ImportModule(module_name); if (!py_binds->pModule) diff --git a/core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json b/core/src/drivers/plugins/python/modbus_slave/modbus_slave_config.json similarity index 100% rename from core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json rename to core/src/drivers/plugins/python/modbus_slave/modbus_slave_config.json diff --git a/core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py b/core/src/drivers/plugins/python/modbus_slave/simple_modbus.py similarity index 100% rename from core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py rename to core/src/drivers/plugins/python/modbus_slave/simple_modbus.py diff --git a/docs/PLUGIN_VENV_GUIDE.md b/docs/PLUGIN_VENV_GUIDE.md new file mode 100644 index 00000000..feb7e282 --- /dev/null +++ b/docs/PLUGIN_VENV_GUIDE.md @@ -0,0 +1,189 @@ +# Virtual Environment Guide for Python Plugins + +This document describes how to use separate virtual environments (venv) for Python plugins in the OpenPLC Runtime, allowing each plugin to have its own dependencies without conflicts. + +## Overview + +The separated VENV system allows you to: + +* Let each Python plugin use specific library versions +* Avoid conflicts between dependencies of different plugins +* Simplify plugin development and maintenance +* Keep compatibility with existing plugins + +## File Structure + +``` +openplc-runtime/ +├── venvs/ # Directory for virtual environments +│ ├── modbus_slave/ # venv for the Modbus plugin +│ └── mqtt_client/ # venv for the MQTT plugin +├── core/src/drivers/plugins/python/ +│ ├── modbus_slave_plugin/ +│ │ ├── simple_modbus.py +│ │ ├── modbus_slave_config.json +│ │ └── requirements.txt # Plugin-specific dependencies +│ └── mqtt_plugin/ +│ ├── plugin.py +│ ├── config.json +│ └── requirements.txt +└── scripts/ + └── manage_plugin_venvs.sh # Management script +``` + +## How to Use + +### 1. Creating a Plugin with a VENV + +1. **Create the plugin directory:** + + ```bash + mkdir core/src/drivers/plugins/python/my_plugin + ``` + +2. **Create the requirements.txt file:** + + ```bash + echo "pymodbus==3.6.4" > core/src/drivers/plugins/python/my_plugin/requirements.txt + echo "paho-mqtt==2.1.0" >> core/src/drivers/plugins/python/my_plugin/requirements.txt + ``` + +3. **Create the virtual environment:** + + ```bash + ./scripts/manage_plugin_venvs.sh create my_plugin + ``` + +4. **Configure plugins.conf:** + + ``` + my_plugin,./core/src/drivers/plugins/python/my_plugin/plugin.py,1,0,./config.json,./venvs/my_plugin + ``` + +### 2. Managing Virtual Environments + +#### Create a VENV for a plugin: + +```bash +./scripts/manage_plugin_venvs.sh create PLUGIN_NAME +``` + +#### List all VENVs: + +```bash +./scripts/manage_plugin_venvs.sh list +``` + +#### Install dependencies: + +```bash +./scripts/manage_plugin_venvs.sh install PLUGIN_NAME +``` + +#### Remove a VENV: + +```bash +./scripts/manage_plugin_venvs.sh remove PLUGIN_NAME +``` + +#### VENV information: + +```bash +./scripts/manage_plugin_venvs.sh info PLUGIN_NAME +``` + +## plugins.conf Format + +### New format (with VENV): + +``` +# name,path,enabled,type,config_path,venv_path +modbus_slave,./core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py,1,0,./config.json,./venvs/modbus_slave +``` + +### Old format (without VENV – still compatible): + +``` +# name,path,enabled,type,config_path +example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./example_config.ini +``` + +## Practical Example + +### Modbus Plugin with a specific VENV: + +1. **Create requirements.txt:** + + ```bash + cat > core/src/drivers/plugins/python/modbus_slave_plugin/requirements.txt << EOF + pymodbus==3.6.4 + asyncio-mqtt==0.16.2 + EOF + ``` + +2. **Create the virtual environment:** + + ```bash + ./scripts/manage_plugin_venvs.sh create modbus_slave + ``` + +3. **Configure plugins.conf:** + + ``` + modbus_slave,./core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py,1,0,./core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json,./venvs/modbus_slave + ``` + +4. **Verify installation:** + + ```bash + ./scripts/manage_plugin_venvs.sh info modbus_slave + ``` + +## Compatibility + +* **Existing plugins:** Continue working normally without changes +* **Legacy system:** If `venv_path` is empty or missing, the system Python is used +* **Python versions:** Works with Python 3.6+ + +## Troubleshooting + +### Plugin can’t find a module: + +* Check if the venv was created: `./scripts/manage_plugin_venvs.sh list` +* Check if dependencies were installed: `./scripts/manage_plugin_venvs.sh info PLUGIN_NAME` +* Check the path in `plugins.conf` + +### Dependency conflicts: + +* Each plugin has its own isolated venv +* Use specific versions in `requirements.txt` +* Recreate the venv if needed: `./scripts/manage_plugin_venvs.sh remove PLUGIN_NAME && ./scripts/manage_plugin_venvs.sh create PLUGIN_NAME` + +### Build error: + +* Recompile after changes: `./scripts/compile.sh` +* Verify Python headers are installed: `sudo apt install python3-dev` + +## Limitations + +* Each venv uses additional disk space +* Slightly longer startup time +* Requires Python 3.3+ for the native `venv` + +## Technical Architecture + +### Implementation: + +1. **plugin\_config.h:** Added `venv_path` field to the structure +2. **plugin\_config.c:** Parser updated to read the optional 6th field +3. **plugin\_driver.c:** Logic to set up `sys.path` before importing the plugin +4. **manage\_plugin\_venvs.sh:** Full management script + +### Loading flow: + +1. The system reads `plugins.conf` +2. If `venv_path` is specified, it sets up `sys.path` to include the venv’s site-packages +3. Imports the plugin’s Python module +4. Executes `init`/`start`/`stop`/`cleanup` functions as usual + +This system maintains full compatibility with existing plugins while enabling dependency isolation when needed. diff --git a/plugins.conf b/plugins.conf index 4b25fe01..1fcba473 100644 --- a/plugins.conf +++ b/plugins.conf @@ -1,5 +1,6 @@ # Plugin configuration file -# Format: name,path,enabled,type,config_path -modbus_slave,./core/src/drivers/plugins/python/modbus_slave_plugin/simple_modbus.py,1,0,./core/src/drivers/plugins/python/modbus_slave_plugin/modbus_slave_config.json -# example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./example_plugin_config.ini - +# Format: name,path,enabled,type,config_path,venv_path +# Note: venv_path is optional - leave empty or omit for system Python +modbus_slave,./core/src/drivers/plugins/python/modbus_slave/simple_modbus.py,1,0,./core/src/drivers/plugins/python/modbus_slave/modbus_slave_config.json,./venvs/modbus_slave +# example_plugin,./core/src/drivers/examples/example_python_plugin.py,1,0,./example_plugin_config.ini, +# mqtt_client,./core/src/drivers/plugins/python/mqtt_plugin/mqtt_client.py,1,0,./core/src/drivers/plugins/python/mqtt_plugin/config.json,./venvs/mqtt_client diff --git a/scripts/manage_plugin_venvs.sh b/scripts/manage_plugin_venvs.sh new file mode 100755 index 00000000..97bae66a --- /dev/null +++ b/scripts/manage_plugin_venvs.sh @@ -0,0 +1,283 @@ +#!/bin/bash +# OpenPLC Runtime Plugin Virtual Environment Manager +# Manages virtual environments for Python plugins to avoid dependency conflicts + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +VENVS_DIR="$PROJECT_ROOT/venvs" +PLUGINS_DIR="$PROJECT_ROOT/core/src/drivers/plugins/python" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if Python3 is available +check_python() { + if ! command -v python3 &> /dev/null; then + log_error "python3 is not installed or not in PATH" + exit 1 + fi + + local python_version=$(python3 --version | cut -d' ' -f2) + log_info "Using Python version: $python_version" +} + +# Create virtual environment for a plugin +create_plugin_venv() { + local plugin_name="$1" + + if [ -z "$plugin_name" ]; then + log_error "Plugin name is required" + show_usage + exit 1 + fi + + local venv_path="$VENVS_DIR/$plugin_name" + local plugin_path="$PLUGINS_DIR/${plugin_name}" + local requirements_file="$plugin_path/requirements.txt" + + log_info "Creating virtual environment for plugin: $plugin_name" + + # Create venvs directory if it doesn't exist + mkdir -p "$VENVS_DIR" + + # Check if venv already exists + if [ -d "$venv_path" ]; then + log_warning "Virtual environment already exists at: $venv_path" + read -p "Do you want to recreate it? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "Removing existing virtual environment..." + rm -rf "$venv_path" + else + log_info "Keeping existing virtual environment" + return 0 + fi + fi + + # Create virtual environment + log_info "Creating Python virtual environment at: $venv_path" + python3 -m venv "$venv_path" + + # Upgrade pip + log_info "Upgrading pip..." + "$venv_path/bin/pip" install --upgrade pip + + # Install requirements if they exist + if [ -f "$requirements_file" ]; then + log_info "Installing dependencies from: $requirements_file" + "$venv_path/bin/pip" install -r "$requirements_file" + log_success "Dependencies installed successfully" + else + log_warning "No requirements.txt found at: $requirements_file" + log_info "You can add dependencies later by creating requirements.txt and running:" + log_info " $0 install $plugin_name" + fi + + log_success "Virtual environment created successfully at: $venv_path" + log_info "To use this venv in plugins.conf, add the venv path as the 6th field:" + log_info " $plugin_name,./path/to/plugin.py,1,0,./path/to/config.json,$venv_path" +} + +# Install dependencies for existing venv +install_dependencies() { + local plugin_name="$1" + + if [ -z "$plugin_name" ]; then + log_error "Plugin name is required" + show_usage + exit 1 + fi + + local venv_path="$VENVS_DIR/$plugin_name" + local plugin_path="$PLUGINS_DIR/${plugin_name}_plugin" + local requirements_file="$plugin_path/requirements.txt" + + if [ ! -d "$venv_path" ]; then + log_error "Virtual environment not found: $venv_path" + log_info "Create it first with: $0 create $plugin_name" + exit 1 + fi + + if [ ! -f "$requirements_file" ]; then + log_error "Requirements file not found: $requirements_file" + exit 1 + fi + + log_info "Installing dependencies for plugin: $plugin_name" + "$venv_path/bin/pip" install -r "$requirements_file" + log_success "Dependencies installed successfully" +} + +# List all virtual environments +list_venvs() { + log_info "Listing plugin virtual environments in: $VENVS_DIR" + + if [ ! -d "$VENVS_DIR" ]; then + log_warning "No virtual environments directory found at: $VENVS_DIR" + return 0 + fi + + local count=0 + for venv_dir in "$VENVS_DIR"/*; do + if [ -d "$venv_dir" ] && [ -f "$venv_dir/bin/python" ]; then + local venv_name=$(basename "$venv_dir") + local python_version=$("$venv_dir/bin/python" --version 2>&1 | cut -d' ' -f2) + local pip_packages=$("$venv_dir/bin/pip" list --format=freeze | wc -l) + + echo -e "${GREEN}$venv_name${NC}" + echo " Path: $venv_dir" + echo " Python: $python_version" + echo " Packages: $pip_packages installed" + echo + ((count++)) + fi + done + + if [ $count -eq 0 ]; then + log_warning "No virtual environments found" + else + log_success "Found $count virtual environment(s)" + fi +} + +# Remove virtual environment +remove_venv() { + local plugin_name="$1" + + if [ -z "$plugin_name" ]; then + log_error "Plugin name is required" + show_usage + exit 1 + fi + + local venv_path="$VENVS_DIR/$plugin_name" + + if [ ! -d "$venv_path" ]; then + log_error "Virtual environment not found: $venv_path" + exit 1 + fi + + log_warning "This will permanently remove the virtual environment for: $plugin_name" + read -p "Are you sure? (y/N): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "Removing virtual environment: $venv_path" + rm -rf "$venv_path" + log_success "Virtual environment removed successfully" + else + log_info "Operation cancelled" + fi +} + +# Show package information for a venv +show_info() { + local plugin_name="$1" + + if [ -z "$plugin_name" ]; then + log_error "Plugin name is required" + show_usage + exit 1 + fi + + local venv_path="$VENVS_DIR/$plugin_name" + + if [ ! -d "$venv_path" ]; then + log_error "Virtual environment not found: $venv_path" + exit 1 + fi + + log_info "Virtual environment information for: $plugin_name" + echo "Path: $venv_path" + echo "Python version: $("$venv_path/bin/python" --version)" + echo "Pip version: $("$venv_path/bin/pip" --version)" + echo + log_info "Installed packages:" + "$venv_path/bin/pip" list +} + +# Show usage information +show_usage() { + echo "OpenPLC Runtime Plugin Virtual Environment Manager" + echo + echo "Usage: $0 COMMAND [PLUGIN_NAME]" + echo + echo "Commands:" + echo " create PLUGIN_NAME Create virtual environment for plugin" + echo " install PLUGIN_NAME Install dependencies for existing venv" + echo " list List all plugin virtual environments" + echo " remove PLUGIN_NAME Remove virtual environment for plugin" + echo " info PLUGIN_NAME Show information about plugin venv" + echo " help Show this help message" + echo + echo "Examples:" + echo " $0 create modbus # Create venv for modbus plugin" + echo " $0 list # List all plugin venvs" + echo " $0 remove modbus # Remove modbus plugin venv" + echo + echo "Notes:" + echo " - Plugin requirements should be in: $PLUGINS_DIR/PLUGIN_NAME_plugin/requirements.txt" + echo " - Virtual environments are created in: $VENVS_DIR/" + echo " - Add venv path to plugins.conf as the 6th field to use it" +} + +# Main function +main() { + local command="$1" + local plugin_name="$2" + + # Check Python availability + check_python + + case "$command" in + "create") + create_plugin_venv "$plugin_name" + ;; + "install") + install_dependencies "$plugin_name" + ;; + "list") + list_venvs + ;; + "remove") + remove_venv "$plugin_name" + ;; + "info") + show_info "$plugin_name" + ;; + "help"|"--help"|"-h"|"") + show_usage + ;; + *) + log_error "Unknown command: $command" + show_usage + exit 1 + ;; + esac +} + +# Run main function with all arguments +main "$@" From 69d10562e40100252f95bdc54f364631c4d0026e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcone=20Ten=C3=B3rio=20da=20Silva=20Filho?= Date: Thu, 25 Sep 2025 14:27:57 +0200 Subject: [PATCH 26/44] Update scripts/manage_plugin_venvs.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/manage_plugin_venvs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/manage_plugin_venvs.sh b/scripts/manage_plugin_venvs.sh index 97bae66a..f1d65669 100755 --- a/scripts/manage_plugin_venvs.sh +++ b/scripts/manage_plugin_venvs.sh @@ -112,7 +112,7 @@ install_dependencies() { fi local venv_path="$VENVS_DIR/$plugin_name" - local plugin_path="$PLUGINS_DIR/${plugin_name}_plugin" + local plugin_path="$PLUGINS_DIR/${plugin_name}" local requirements_file="$plugin_path/requirements.txt" if [ ! -d "$venv_path" ]; then From cefb954754535b648161181abc0c32363a3e490b Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Fri, 26 Sep 2025 08:46:25 +0200 Subject: [PATCH 27/44] Disabling initial plugin prints and avoiding code insertion --- core/src/drivers/plugin_driver.c | 52 ++++++++++++------- .../python/modbus_slave/simple_modbus.py | 18 +++---- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 412ae7e8..39d58a5a 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -384,13 +384,13 @@ void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t * args->buffer_size = BUFFER_SIZE; args->bits_per_buffer = 8; - printf("[PLUGIN]: Runtime args initialized:\n"); - printf("[PLUGIN]: buffer_size = %d\n", args->buffer_size); - printf("[PLUGIN]: bits_per_buffer = %d\n", args->bits_per_buffer); - printf("[PLUGIN]: buffer_mutex = %p\n", (void *)args->buffer_mutex); - printf("[PLUGIN]: bool_input = %p\n", (void *)args->bool_input); - printf("[PLUGIN]: mutex_take = %p\n", (void *)args->mutex_take); - printf("[PLUGIN]: mutex_give = %p\n", (void *)args->mutex_give); + // printf("[PLUGIN]: Runtime args initialized:\n"); + // printf("[PLUGIN]: buffer_size = %d\n", args->buffer_size); + // printf("[PLUGIN]: bits_per_buffer = %d\n", args->bits_per_buffer); + // printf("[PLUGIN]: buffer_mutex = %p\n", (void *)args->buffer_mutex); + // printf("[PLUGIN]: bool_input = %p\n", (void *)args->bool_input); + // printf("[PLUGIN]: mutex_take = %p\n", (void *)args->mutex_take); + // printf("[PLUGIN]: mutex_give = %p\n", (void *)args->mutex_give); // Validate critical pointers if (!args->buffer_mutex) @@ -510,22 +510,36 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) // Setup virtual environment if specified if (strlen(plugin->config.venv_path) > 0) { - char venv_setup_cmd[1024]; - snprintf(venv_setup_cmd, sizeof(venv_setup_cmd), - "import sys\n" - "venv_path = '%s/lib/python%d.%d/site-packages'\n" - "if venv_path not in sys.path:\n" - " sys.path.insert(0, venv_path)\n" - "print('[PLUGIN] Using venv for %s: %s')", - plugin->config.venv_path, PY_MAJOR_VERSION, PY_MINOR_VERSION, plugin->config.name, - plugin->config.venv_path); - - if (PyRun_SimpleString(venv_setup_cmd) != 0) + // Construct the venv site-packages path + char venv_site_packages[512]; + snprintf(venv_site_packages, sizeof(venv_site_packages), "%s/lib/python%d.%d/site-packages", + plugin->config.venv_path, PY_MAJOR_VERSION, PY_MINOR_VERSION); + // Get sys.path + PyObject *sys_path = PySys_GetObject("path"); + if (sys_path && PyList_Check(sys_path)) { - fprintf(stderr, "Failed to setup venv for plugin: %s\n", plugin->config.name); + PyObject *venv_path_obj = PyUnicode_FromString(venv_site_packages); + int found = PySequence_Contains(sys_path, venv_path_obj); + if (found == 0) + { // Not found + if (PyList_Insert(sys_path, 0, venv_path_obj) != 0) + { + fprintf(stderr, "Failed to insert venv path into sys.path for plugin: %s\n", + plugin->config.name); + Py_DECREF(venv_path_obj); + free(py_binds); + return -1; + } + } + Py_DECREF(venv_path_obj); + } + else + { + fprintf(stderr, "Failed to get sys.path for plugin: %s\n", plugin->config.name); free(py_binds); return -1; } + printf("[PLUGIN] Using venv for %s: %s\n", plugin->config.name, venv_site_packages); } // Load the Python module diff --git a/core/src/drivers/plugins/python/modbus_slave/simple_modbus.py b/core/src/drivers/plugins/python/modbus_slave/simple_modbus.py index 22d316b0..beab3089 100644 --- a/core/src/drivers/plugins/python/modbus_slave/simple_modbus.py +++ b/core/src/drivers/plugins/python/modbus_slave/simple_modbus.py @@ -304,7 +304,7 @@ def init(args_capsule): try: # Print structure validation info for debugging print("[MODBUS] Validating plugin structure alignment...") - PluginStructureValidator.print_structure_info() + # PluginStructureValidator.print_structure_info() # Extract runtime args from capsule using safe method if hasattr(args_capsule, '__class__') and 'PyCapsule' in str(type(args_capsule)): @@ -346,9 +346,9 @@ def init(args_capsule): print(f"[MODBUS] ✗ Failed to access buffer size: {size_error}") return False - print(f"[MODBUS] Buffer size: {buffer_size}") - print(f"[MODBUS] Bits per buffer: {runtime_args.bits_per_buffer}") - print(f"[MODBUS] Structure details: {runtime_args}") + # print(f"[MODBUS] Buffer size: {buffer_size}") + # print(f"[MODBUS] Bits per buffer: {runtime_args.bits_per_buffer}") + # print(f"[MODBUS] Structure details: {runtime_args}") # Create OpenPLC-connected data blocks for all Modbus types coils_block = OpenPLCCoilsDataBlock(runtime_args, num_coils=64) @@ -357,11 +357,11 @@ def init(args_capsule): holding_registers_block = OpenPLCHoldingRegistersDataBlock(runtime_args, num_registers=32) # Create device context with all OpenPLC-connected data blocks - print(f"[MODBUS] Created data blocks:") - print(f"[MODBUS] - Coils (bool_output): {coils_block.num_coils} coils") - print(f"[MODBUS] - Discrete Inputs (bool_input): {discrete_inputs_block.num_inputs} inputs") - print(f"[MODBUS] - Input Registers (int_input): {input_registers_block.num_registers} registers") - print(f"[MODBUS] - Holding Registers (int_output): {holding_registers_block.num_registers} registers") + # print(f"[MODBUS] Created data blocks:") + # print(f"[MODBUS] - Coils (bool_output): {coils_block.num_coils} coils") + # print(f"[MODBUS] - Discrete Inputs (bool_input): {discrete_inputs_block.num_inputs} inputs") + # print(f"[MODBUS] - Input Registers (int_input): {input_registers_block.num_registers} registers") + # print(f"[MODBUS] - Holding Registers (int_output): {holding_registers_block.num_registers} registers") device = ModbusDeviceContext( di=discrete_inputs_block, # Discrete Inputs -> bool_input From f8ab31131933df1ca13b7f9832436a43a4a76315 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Fri, 26 Sep 2025 14:57:30 +0200 Subject: [PATCH 28/44] install now has support for apt yum and dfs and plc program is being built --- install.sh | 85 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/install.sh b/install.sh index abfe9a8d..01721b76 100755 --- a/install.sh +++ b/install.sh @@ -3,8 +3,43 @@ set -e OPENPLC_DIR="$PWD" VENV_DIR="$OPENPLC_DIR/.venv" +SCRIPTS_DIR="$OPENPLC_DIR/scripts" -install_dependencies() { +install_dependencies() +{ + source /etc/os-release + echo "Distro: $ID" + + case "$ID" in + ubuntu|debian) + install_deps_apt "$1" + ;; + centos) + if [[ "$VERSION_ID" == 7* ]]; then + install_deps_yum "$1" + else + install_deps_dnf "$1" + fi + ;; + rhel) + if [[ "$VERSION_ID" == 7* ]]; then + install_deps_yum "$1" + else + install_deps_dnf "$1" + fi + ;; + fedora) + install_deps_dnf "$1" + ;; + *) + echo "Unsupported Linux distro: $ID" >&2 + return 1 + ;; + esac +} + +# For Ubuntu/Debian +install_deps_apt() { apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ @@ -15,22 +50,48 @@ install_dependencies() { && rm -rf /var/lib/apt/lists/* } -if [ "$1" = "docker" ]; then - install_dependencies - python3 -m venv "$VENV_DIR" - "$VENV_DIR/bin/python3" -m pip install --upgrade pip - "$VENV_DIR/bin/python3" -m pip install -r requirements.txt -fi +# For CentOS 7/RHEL 7 (older) +install_deps_yum() { + yum install -y \ + gcc gcc-c++ make cmake \ + python3 python3-devel python3-pip python3-venv \ + && yum clean all +} + +# For Fedora/RHEL 8+/CentOS Stream +install_deps_dnf() { + dnf install -y \ + gcc gcc-c++ make cmake \ + python3 python3-devel python3-pip python3-venv \ + && dnf clean all +} + +compile_plc() { + mkdir -p "$OPENPLC_DIR/build" + cd "$OPENPLC_DIR/build" + cmake .. + make -j"$(nproc)" + cd "$OPENPLC_DIR" +} if [ "$1" = "linux" ]; then mkdir -p /var/run/runtime chmod 775 /var/run/runtime chmod +x install.sh chmod +x scripts/* - install_dependencies - python3 -m venv "$VENV_DIR" - "$VENV_DIR/bin/python3" -m pip install --upgrade pip - "$VENV_DIR/bin/python3" -m pip install -r requirements.txt fi -echo "Dependencies installed." +install_dependencies +python3 -m venv "$VENV_DIR" +"$VENV_DIR/bin/python3" -m pip install --upgrade pip +"$VENV_DIR/bin/python3" -m pip install -r requirements.txt + + +echo "Dependencies installed..." +echo "Virtual environment created at $VENV_DIR" + +echo "Compiling OpenPLC..." +#compile openplc +compile_plc + +echo "OpenPLC compiled successfully." \ No newline at end of file From e086fea14837fbf453d5fefafc1297173d750112 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Mon, 29 Sep 2025 09:08:38 +0200 Subject: [PATCH 29/44] adding proper build error and success logs --- install.sh | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index 01721b76..556e5bfd 100755 --- a/install.sh +++ b/install.sh @@ -67,11 +67,38 @@ install_deps_dnf() { } compile_plc() { - mkdir -p "$OPENPLC_DIR/build" - cd "$OPENPLC_DIR/build" - cmake .. - make -j"$(nproc)" - cd "$OPENPLC_DIR" + echo "Creating build directory..." + if ! mkdir -p "$OPENPLC_DIR/build"; then + echo "ERROR: Failed to create build directory" >&2 + return 1 + fi + + cd "$OPENPLC_DIR/build" || { + echo "ERROR: Failed to change to build directory" >&2 + return 1 + } + + echo "Running cmake configuration..." + if ! cmake ..; then + echo "ERROR: CMake configuration failed" >&2 + cd "$OPENPLC_DIR" + return 1 + fi + + echo "Compiling with make (using $(nproc) cores)..." + if ! make -j"$(nproc)"; then + echo "ERROR: Compilation failed" >&2 + cd "$OPENPLC_DIR" + return 1 + fi + + cd "$OPENPLC_DIR" || { + echo "ERROR: Failed to return to main directory" >&2 + return 1 + } + + echo "SUCCESS: OpenPLC compiled successfully!" + return 0 } if [ "$1" = "linux" ]; then @@ -91,7 +118,11 @@ echo "Dependencies installed..." echo "Virtual environment created at $VENV_DIR" echo "Compiling OpenPLC..." -#compile openplc -compile_plc - -echo "OpenPLC compiled successfully." \ No newline at end of file +if compile_plc; then + echo "Build process completed successfully!" + echo "OpenPLC Runtime is ready to use." +else + echo "ERROR: Build process failed!" >&2 + echo "Please check the error messages above for details." >&2 + exit 1 +fi \ No newline at end of file From 55dfcd29ea9d72016086487e2b2aac1a12f8fba0 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Mon, 29 Sep 2025 14:57:12 +0200 Subject: [PATCH 30/44] [RTOP 74][WIP] implementing initialization scripts --- Dockerfile | 9 ++- core/src/drivers/plugin_driver.c | 4 +- install.sh | 39 +++++++--- scripts/run-image.sh | 6 +- start_openplc.sh | 122 +++++++++++++++++++++++++++++++ webserver/runtimemanager.py | 13 +++- 6 files changed, 173 insertions(+), 20 deletions(-) create mode 100755 start_openplc.sh diff --git a/Dockerfile b/Dockerfile index 72dff11c..357b3050 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,17 @@ FROM debian:bookworm-slim RUN apt-get update && apt-get install -y \ python3 python3-venv python3-pip bash \ + pkg-config \ && rm -rf /var/lib/apt/lists/* WORKDIR /workdir COPY . . RUN mkdir -p /var/run/runtime -RUN chmod +x install.sh scripts/* build/* -RUN ./install.sh docker +# Clean any existing build artifacts to ensure clean Docker build +RUN rm -rf build/ .venv/ venvs/ 2>/dev/null || true +RUN chmod +x install.sh scripts/* start_openplc.sh +RUN ./install.sh EXPOSE 8443 -CMD ["bash", "-c", "./build/plc_main & .venv/bin/python3 webserver/app.py"] +CMD ["bash", "./start_openplc.sh"] diff --git a/core/src/drivers/plugin_driver.c b/core/src/drivers/plugin_driver.c index 39d58a5a..26ad4a14 100644 --- a/core/src/drivers/plugin_driver.c +++ b/core/src/drivers/plugin_driver.c @@ -421,7 +421,7 @@ void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t * if (!capsule) { fprintf(stderr, "[PLUGIN]: Error - failed to create Python capsule\n"); - free(args); + // Note: create_python_runtime_args_capsule already freed args on failure return NULL; } printf("[PLUGIN]: Python capsule created successfully\n"); @@ -448,7 +448,7 @@ void free_structured_args(plugin_runtime_args_t *args) int python_plugin_get_symbols(plugin_instance_t *plugin) { - if (!plugin || !plugin->config.path) + if (!plugin || plugin->config.path[0] == '\0') { return -1; } diff --git a/install.sh b/install.sh index 556e5bfd..b0c8e63b 100755 --- a/install.sh +++ b/install.sh @@ -1,10 +1,20 @@ #!/bin/bash set -e -OPENPLC_DIR="$PWD" +# Detect the project root directory +# This works whether the script is called from project root, Docker, or anywhere else +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OPENPLC_DIR="$SCRIPT_DIR" VENV_DIR="$OPENPLC_DIR/.venv" SCRIPTS_DIR="$OPENPLC_DIR/scripts" +# Ensure we're in the project directory +cd "$OPENPLC_DIR" + +echo "OpenPLC Runtime Installation" +echo "Project directory: $OPENPLC_DIR" +echo "Working directory: $(pwd)" + install_dependencies() { source /etc/os-release @@ -67,7 +77,16 @@ install_deps_dnf() { } compile_plc() { - echo "Creating build directory..." + echo "Preparing build directory..." + + # Always clean build directory for Docker environment or when CMake cache exists + # This prevents cross-contamination between Linux and Docker builds + if [ -d "$OPENPLC_DIR/build" ] && [ -f "$OPENPLC_DIR/build/CMakeCache.txt" ]; then + echo "Cleaning existing build directory to ensure clean build..." + rm -rf "$OPENPLC_DIR/build" + fi + + # Create build directory if ! mkdir -p "$OPENPLC_DIR/build"; then echo "ERROR: Failed to create build directory" >&2 return 1 @@ -101,17 +120,19 @@ compile_plc() { return 0 } -if [ "$1" = "linux" ]; then - mkdir -p /var/run/runtime - chmod 775 /var/run/runtime - chmod +x install.sh - chmod +x scripts/* -fi +# Setup runtime directory (needed for both Linux and Docker) +mkdir -p /var/run/runtime +chmod 775 /var/run/runtime 2>/dev/null || true # Ignore permission errors in Docker + +# Make scripts executable +chmod +x "$OPENPLC_DIR/install.sh" 2>/dev/null || true +chmod +x "$OPENPLC_DIR/scripts/"* 2>/dev/null || true +chmod +x "$OPENPLC_DIR/start_openplc.sh" 2>/dev/null || true install_dependencies python3 -m venv "$VENV_DIR" "$VENV_DIR/bin/python3" -m pip install --upgrade pip -"$VENV_DIR/bin/python3" -m pip install -r requirements.txt +"$VENV_DIR/bin/python3" -m pip install -r "$OPENPLC_DIR/requirements.txt" echo "Dependencies installed..." diff --git a/scripts/run-image.sh b/scripts/run-image.sh index 6076c527..5296e99b 100755 --- a/scripts/run-image.sh +++ b/scripts/run-image.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash -# Run container mounting current directory into /workspace +# Run container mounting only source code (preserves built venv) docker run --rm -it \ - -v $(pwd):/workdir \ + -v $(pwd)/core:/workdir/core \ + -v $(pwd)/webserver:/workdir/webserver \ + -v $(pwd)/scripts:/workdir/scripts \ --cap-add=sys_nice \ --ulimit rtprio=99 \ --ulimit memlock=-1 \ diff --git a/start_openplc.sh b/start_openplc.sh new file mode 100755 index 00000000..a5f61ce0 --- /dev/null +++ b/start_openplc.sh @@ -0,0 +1,122 @@ +#!/bin/bash +set -euo pipefail + +# Detect the project root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OPENPLC_DIR="$SCRIPT_DIR" + +# Ensure we're in the project directory +cd "$OPENPLC_DIR" + +echo "Starting OpenPLC Runtime" +echo "Project directory: $OPENPLC_DIR" +echo "Working directory: $(pwd)" + +# MANAGE PLUGIN VENVS +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to setup plugin virtual environments +setup_plugin_venvs() { + local plugins_dir="$OPENPLC_DIR/core/src/drivers/plugins/python" + local manage_script="$OPENPLC_DIR/scripts/manage_plugin_venvs.sh" + + log_info "Checking for plugins that need virtual environments..." + + # Check if plugins directory exists + if [ ! -d "$plugins_dir" ]; then + log_warning "Plugins directory not found: $plugins_dir" + return 0 + fi + + # Find all directories with requirements.txt + local plugins_with_requirements=() + while IFS= read -r -d '' requirements_file; do + # Get the directory name (plugin name) + local plugin_dir=$(dirname "$requirements_file") + local plugin_name=$(basename "$plugin_dir") + + # Skip if it's in examples or shared directories (common libraries) + if [[ "$plugin_dir" == *"/examples/"* ]] || [[ "$plugin_dir" == *"/shared/"* ]]; then + log_info "Skipping $plugin_name (in examples/shared directory)" + continue + fi + + plugins_with_requirements+=("$plugin_name") + log_info "Found plugin with requirements: $plugin_name" + done < <(find "$plugins_dir" -name "requirements.txt" -type f -print0) + + # If no plugins found, return + if [ ${#plugins_with_requirements[@]} -eq 0 ]; then + log_info "No plugins with requirements.txt found" + return 0 + fi + + log_info "Found ${#plugins_with_requirements[@]} plugin(s) that need virtual environments" + + # Create virtual environments for each plugin + for plugin_name in "${plugins_with_requirements[@]}"; do + local venv_path="$OPENPLC_DIR/venvs/$plugin_name" + local requirements_file="$plugins_dir/$plugin_name/requirements.txt" + + if [ -d "$venv_path" ]; then + log_info "Virtual environment already exists for $plugin_name" + + # Check if requirements.txt is newer than the venv (dependencies may have changed) + if [ "$requirements_file" -nt "$venv_path" ]; then + log_warning "Requirements file is newer than venv for $plugin_name" + log_info "Updating dependencies for $plugin_name..." + + if bash "$manage_script" install "$plugin_name"; then + log_success "Dependencies updated for $plugin_name" + else + log_error "Failed to update dependencies for $plugin_name" + return 1 + fi + else + log_info "Dependencies are up to date for $plugin_name" + fi + else + log_info "Creating virtual environment for plugin: $plugin_name" + + if bash "$manage_script" create "$plugin_name"; then + log_success "Virtual environment created for $plugin_name" + else + log_error "Failed to create virtual environment for $plugin_name" + return 1 + fi + fi + done + + log_success "All plugin virtual environments are ready" + return 0 +} + +# Setup plugin virtual environments +setup_plugin_venvs + +source "$OPENPLC_DIR/.venv/bin/activate" + +# Start the PLC webserver +"$OPENPLC_DIR/.venv/bin/python3" "$OPENPLC_DIR/webserver/app.py" \ No newline at end of file diff --git a/webserver/runtimemanager.py b/webserver/runtimemanager.py index d4b7ff99..d4fb99e9 100644 --- a/webserver/runtimemanager.py +++ b/webserver/runtimemanager.py @@ -29,12 +29,17 @@ def find_running_process(self): # Find the running PLC runtime process by executable path for proc in psutil.process_iter(['pid', 'exe', 'cmdline']): try: + # First try to match by executable path (most reliable) if proc.info['exe'] and os.path.samefile(proc.info['exe'], self.runtime_path): return proc - # Alternatively, match by command line - if self.runtime_path in ' '.join(proc.info['cmdline']): - return proc - except (OSError, psutil.Error): + + # Alternatively, match by command line (fallback) + cmdline = proc.info.get('cmdline') + if cmdline and isinstance(cmdline, (list, tuple)) and len(cmdline) > 0: + cmdline_str = ' '.join(str(arg) for arg in cmdline if arg is not None) + if self.runtime_path in cmdline_str: + return proc + except (OSError, psutil.Error, TypeError, ValueError): continue return None From 7f9874867ff5b3c96f214dc5701242e58df937a0 Mon Sep 17 00:00:00 2001 From: Marcone Tenorio Date: Mon, 29 Sep 2025 16:15:09 +0200 Subject: [PATCH 31/44] changing runtime venv from .venv to venvs/runtime/ --- Dockerfile | 2 +- install.sh | 2 +- scripts/exec.sh | 2 +- start_openplc.sh | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 357b3050..6d6636a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /workdir COPY . . RUN mkdir -p /var/run/runtime # Clean any existing build artifacts to ensure clean Docker build -RUN rm -rf build/ .venv/ venvs/ 2>/dev/null || true +RUN rm -rf build/ venvs/ 2>/dev/null || true RUN chmod +x install.sh scripts/* start_openplc.sh RUN ./install.sh diff --git a/install.sh b/install.sh index b0c8e63b..5d8f474e 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ set -e # This works whether the script is called from project root, Docker, or anywhere else SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OPENPLC_DIR="$SCRIPT_DIR" -VENV_DIR="$OPENPLC_DIR/.venv" +VENV_DIR="$OPENPLC_DIR/venvs/runtime" SCRIPTS_DIR="$OPENPLC_DIR/scripts" # Ensure we're in the project directory diff --git a/scripts/exec.sh b/scripts/exec.sh index 2344e19e..e6fead1c 100755 --- a/scripts/exec.sh +++ b/scripts/exec.sh @@ -2,4 +2,4 @@ set -euo pipefail # Start the PLC webserver -./.venv/bin/python3 webserver/app.py +./venvs/runtime/bin/python3 webserver/app.py diff --git a/start_openplc.sh b/start_openplc.sh index a5f61ce0..54b2a3bb 100755 --- a/start_openplc.sh +++ b/start_openplc.sh @@ -116,7 +116,7 @@ setup_plugin_venvs() { # Setup plugin virtual environments setup_plugin_venvs -source "$OPENPLC_DIR/.venv/bin/activate" +source "$OPENPLC_DIR/venvs/runtime/bin/activate" # Start the PLC webserver -"$OPENPLC_DIR/.venv/bin/python3" "$OPENPLC_DIR/webserver/app.py" \ No newline at end of file +"$OPENPLC_DIR/venvs/runtime/bin/python3" "$OPENPLC_DIR/webserver/app.py" \ No newline at end of file From a919d86d41f3170728bb77beaff9eca3d78d3f2a Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 29 Sep 2025 15:29:31 -0400 Subject: [PATCH 32/44] Add requirements.txt to the Modbus driver --- core/src/drivers/plugins/python/modbus_slave/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 core/src/drivers/plugins/python/modbus_slave/requirements.txt diff --git a/core/src/drivers/plugins/python/modbus_slave/requirements.txt b/core/src/drivers/plugins/python/modbus_slave/requirements.txt new file mode 100644 index 00000000..3c90e4c5 --- /dev/null +++ b/core/src/drivers/plugins/python/modbus_slave/requirements.txt @@ -0,0 +1,2 @@ +pymodbus==3.11.2 +asyncio-mqtt==0.16.2 \ No newline at end of file From c2acb8c0dd721a4fdc294b2265042829beebe3b4 Mon Sep 17 00:00:00 2001 From: Autonomy Server Date: Mon, 29 Sep 2025 16:13:39 -0400 Subject: [PATCH 33/44] Add checks on install and start_openplc scripts --- install.sh | 23 ++++++++++++++++++++++- start_openplc.sh | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 5d8f474e..73139eb1 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,19 @@ #!/bin/bash set -e +# Check for root privileges +check_root() +{ + if [[ $EUID -ne 0 ]]; then + echo "ERROR: This script must be run as root" >&2 + echo "Example: sudo ./install.sh" >&2 + exit 1 + fi +} + +# Make sure we are root before proceeding +check_root + # Detect the project root directory # This works whether the script is called from project root, Docker, or anywhere else SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -141,7 +154,15 @@ echo "Virtual environment created at $VENV_DIR" echo "Compiling OpenPLC..." if compile_plc; then echo "Build process completed successfully!" - echo "OpenPLC Runtime is ready to use." + echo "OpenPLC Runtime v4 is ready to use." + echo "" + echo "To start the OpenPLC Runtime v4, run:" + echo "sudo ./start_openplc.sh" + + # Create installation marker + touch "$OPENPLC_DIR/.installed" + echo "Installation completed at $(date)" > "$OPENPLC_DIR/.installed" + else echo "ERROR: Build process failed!" >&2 echo "Please check the error messages above for details." >&2 diff --git a/start_openplc.sh b/start_openplc.sh index 54b2a3bb..30c93955 100755 --- a/start_openplc.sh +++ b/start_openplc.sh @@ -1,6 +1,29 @@ #!/bin/bash set -euo pipefail +check_root() +{ + if [[ $EUID -ne 0 ]]; then + echo "ERROR: This script must be run as root" >&2 + echo "Example: sudo ./start_openplc.sh" >&2 + exit 1 + fi +} + +check_installation() +{ + if [ ! -f "$OPENPLC_DIR/.installed" ]; then + echo "ERROR: OpenPLC Runtime v4 is not installed." >&2 + echo "Please run the install script first:" >&2 + echo " sudo ./install.sh" >&2 + exit 1 + fi +} + +# Startup checks +check_installation +check_root + # Detect the project root directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OPENPLC_DIR="$SCRIPT_DIR" From f8974ee25b5f2c478671612743a9e29e7575995f Mon Sep 17 00:00:00 2001 From: Autonomy Server Date: Mon, 29 Sep 2025 16:16:17 -0400 Subject: [PATCH 34/44] Quick fix on start_openplc.sh --- start_openplc.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/start_openplc.sh b/start_openplc.sh index 30c93955..29ecc697 100755 --- a/start_openplc.sh +++ b/start_openplc.sh @@ -1,6 +1,13 @@ #!/bin/bash set -euo pipefail +# Detect the project root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OPENPLC_DIR="$SCRIPT_DIR" + +# Ensure we're in the project directory +cd "$OPENPLC_DIR" + check_root() { if [[ $EUID -ne 0 ]]; then @@ -24,13 +31,6 @@ check_installation() check_installation check_root -# Detect the project root directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -OPENPLC_DIR="$SCRIPT_DIR" - -# Ensure we're in the project directory -cd "$OPENPLC_DIR" - echo "Starting OpenPLC Runtime" echo "Project directory: $OPENPLC_DIR" echo "Working directory: $(pwd)" From aad7e535771a0f516ee2898be8c574d2bfa61c23 Mon Sep 17 00:00:00 2001 From: lucasbutzke Date: Mon, 29 Sep 2025 21:40:19 -0300 Subject: [PATCH 35/44] Fix zip file check to allow generated bash script --- webserver/plcapp_management.py | 216 +++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 webserver/plcapp_management.py diff --git a/webserver/plcapp_management.py b/webserver/plcapp_management.py new file mode 100644 index 00000000..ec996163 --- /dev/null +++ b/webserver/plcapp_management.py @@ -0,0 +1,216 @@ +from dataclasses import dataclass, field +from enum import Enum, auto +import logging +import os +import zipfile +import subprocess +import threading +from typing import Final + +from runtimemanager import RuntimeManager + +logger = logging.getLogger(__name__) + +MAX_FILE_SIZE: Final[int] = 10 * 1024 * 1024 # 10 MB per file +MAX_TOTAL_SIZE: Final[int] = 50 * 1024 * 1024 # 50 MB total +DISALLOWED_EXT = (".exe", ".dll", ".sh", ".bat", ".js", ".vbs", ".scr") +ALLOWED_FILENAME = "create_standard_function_txt.sh" + +class BuildStatus(Enum): + IDLE = auto() + UNZIPPING = auto() + COMPILING = auto() + SUCCESS = auto() + FAILED = auto() + +@dataclass +class BuildProcess: + status: BuildStatus = BuildStatus.IDLE + logs: list[str] = field(default_factory=list) + exit_code: int | None = None + + def log(self, msg: str): + logger.info(msg) + self.logs.append(msg) + + def clear(self): + self.status = BuildStatus.IDLE + self.logs.clear() + self.exit_code = None + + +build_state = BuildProcess() # global-ish singleton for status + + +def analyze_zip(zip_path) -> tuple[bool, list]: + """Analyze the ZIP file for safety before extraction.""" + build_state.status = BuildStatus.UNZIPPING + build_state.log(f"[INFO] Analyzing ZIP file: {zip_path}\n") + + if not zipfile.is_zipfile(zip_path): + build_state.log("Not a valid ZIP file.") + return False, [] + + with zipfile.ZipFile(zip_path, "r") as zf: + total_size = 0 + safe = True + valid_files = [] + + for info in zf.infolist(): + filename = info.filename + uncompressed_size = info.file_size + compressed_size = info.compress_size + ext = os.path.splitext(filename)[1].lower() + + # Check for path traversal or absolute paths + if filename.startswith("/") or ".." in filename or ":" in filename: + logger.warning("Dangerous path: %s", filename) + safe = False + + # Check uncompressed size + if uncompressed_size > MAX_FILE_SIZE: + logger.warning("File too large: %s (%d bytes)", + filename, uncompressed_size) + safe = False + + # Check compression ratio (ZIP bomb detection) + if compressed_size > 0 and uncompressed_size / compressed_size > 1000: + logger.warning("Suspicious compression ratio in %s", + filename) + safe = False + + # Check disallowed extensions + # TODO remove this additional BASH SCRIPT check + if ALLOWED_FILENAME not in filename: + if ext in DISALLOWED_EXT: + print("Disallowed extension: %s", + filename) + safe = False + + total_size += uncompressed_size + valid_files.append(info) + + # Check total size + if total_size > MAX_TOTAL_SIZE: + logger.warning("Total uncompressed size too large: %d bytes", + total_size) + safe = False + + if safe: + logger.info("ZIP file looks safe to extract (based on static checks).") + else: + logger.warning("ZIP file failed safety checks.") + + return safe, valid_files + + +def safe_extract(zip_path, dest_dir, valid_files): + """Extract files safely to a target directory. + - Skips macOS metadata (__MACOSX, .DS_Store) + - Auto-strips a single common root folder if present + """ + build_state.status = BuildStatus.UNZIPPING + + with zipfile.ZipFile(zip_path, "r") as zf: + # Detect roots (ignoring macOS junk) + roots = set() + for info in valid_files: + if info.filename.startswith("__MACOSX/") or info.filename.endswith(".DS_Store"): + continue + parts = info.filename.split("/", 1) + if parts and parts[0]: + roots.add(parts[0]) + strip_root = len(roots) == 1 + + for info in valid_files: + filename = info.filename + + # Skip macOS junk and directories + if filename.startswith("__MACOSX/") or filename.endswith(".DS_Store") or filename.endswith("/"): + continue + + # Optionally strip single root folder + if strip_root: + parts = filename.split("/", 1) + if len(parts) == 2: + filename = parts[1] + else: + filename = parts[0] + + out_path = os.path.join(dest_dir, filename) + out_path = os.path.abspath(out_path) + + # Ensure extraction stays inside destination + if not out_path.startswith(os.path.abspath(dest_dir)): + logger.warning("Skipping suspicious path: %s", filename) + continue + + os.makedirs(os.path.dirname(out_path), exist_ok=True) + + with zf.open(info) as src, open(out_path, "wb") as dst: + dst.write(src.read()) + + logger.info("Extracted: %s", out_path) + +def run_compile(runtime_manager: RuntimeManager, cwd: str = "core/generated"): + """Run compile script synchronously (wait for completion) and update status/logs.""" + script_path: str = "./scripts/compile.sh" + + build_state.status = BuildStatus.COMPILING + build_state.log(f"[INFO] Starting compilation: {script_path}\n") + + def stream_output(pipe, prefix): + for line in iter(pipe.readline, ''): + msg = f"{prefix}{line}" + build_state.log(msg) + pipe.close() + + def wait_and_finish(proc: subprocess.Popen, step_name: str): + exit_code = proc.wait() + build_state.exit_code = exit_code + if exit_code == 0: + build_state.status = BuildStatus.SUCCESS + build_state.log(f"[INFO] {step_name} succeeded\n") + else: + build_state.status = BuildStatus.FAILED + build_state.log(f"[INFO] {step_name} failed (exit={exit_code})\n") + raise RuntimeError(f"{step_name} failed (exit={exit_code})") + + # --- Compile step --- + compile_proc = subprocess.Popen( + ["bash", script_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + + threading.Thread(target=stream_output, args=(compile_proc.stdout, "[OUT] "), daemon=True).start() + threading.Thread(target=stream_output, args=(compile_proc.stderr, "[ERR] "), daemon=True).start() + + # Block until compile finishes + wait_and_finish(compile_proc, "Compilation") + + # Stop PLC before cleanup + runtime_manager.stop_plc() + + # --- Cleanup step --- + cleanup_proc = subprocess.Popen( + ["bash", "./scripts/compile-clean.sh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + + threading.Thread(target=stream_output, args=(cleanup_proc.stdout, "[CLEAN-OUT] "), daemon=True).start() + threading.Thread(target=stream_output, args=(cleanup_proc.stderr, "[CLEAN-ERR] "), daemon=True).start() + + # Block until cleanup finishes + wait_and_finish(cleanup_proc, "Cleanup") + + # Restart PLC only if everything succeeded + if build_state.status == BuildStatus.SUCCESS: + runtime_manager.start_plc() + else: + build_state.log("[INFO] PLC will not be restarted due to failed build/cleanup\n") From 865c42945806fb3e4bc41b6c1e3c243c69338c42 Mon Sep 17 00:00:00 2001 From: lucasbutzke Date: Tue, 30 Sep 2025 14:42:09 -0300 Subject: [PATCH 36/44] [RTOP-72] Parse and log unix socket log messages --- webserver/unixserver.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/webserver/unixserver.py b/webserver/unixserver.py index c7739214..6947fb24 100644 --- a/webserver/unixserver.py +++ b/webserver/unixserver.py @@ -3,9 +3,24 @@ import collections import logging import os +import re logger = logging.getLogger(__name__) +LOG_PATTERN = re.compile(r""" + ^\[(?P