diff --git a/core/src/drivers/plugins/native/gpiod/Makefile b/core/src/drivers/plugins/native/gpiod/Makefile new file mode 100644 index 00000000..f8e354c7 --- /dev/null +++ b/core/src/drivers/plugins/native/gpiod/Makefile @@ -0,0 +1,28 @@ +# Makefile for test plugin + +CC = gcc +CFLAGS = -fPIC -I../../../../lib -I../../../../drivers -I.. +LDFLAGS = -shared -lpthread -lgpiod + +TARGET = gpiod_plugin.so +SOURCE = gpiod_plugin.c +PLUGIN_LOGGER = ../plugin_logger.c + +LOADER_TARGET = test_plugin_loader +LOADER_SOURCE = test_plugin_loader.c + +all: $(TARGET) $(LOADER_TARGET) + +$(TARGET): $(SOURCE) $(PLUGIN_LOGGER) + $(CC) $(CFLAGS) $(SOURCE) $(PLUGIN_LOGGER) $(LDFLAGS) -o $(TARGET) + +$(LOADER_TARGET): $(LOADER_SOURCE) + $(CC) -o $(LOADER_TARGET) $(LOADER_SOURCE) -ldl -lpthread -lgpiod + +test: $(TARGET) $(LOADER_TARGET) + ./$(LOADER_TARGET) + +clean: + rm -f $(TARGET) $(LOADER_TARGET) + +.PHONY: all clean test diff --git a/core/src/drivers/plugins/native/gpiod/README.md b/core/src/drivers/plugins/native/gpiod/README.md new file mode 100644 index 00000000..4143e027 --- /dev/null +++ b/core/src/drivers/plugins/native/gpiod/README.md @@ -0,0 +1,140 @@ +# GPIOD Plugin for OpenPLC + +A native plugin for OpenPLC that provides GPIO (General Purpose Input/Output) support using the `libgpiod` library. +It can be used on any Linux device with GPIOs, not only on RaspberryPi, even on PC with USB-GPIO devices + +## Dependencies + +### Required Packages +To compile this plugin, you must have the `libgpiod-dev` package installed: + +```bash +# On Debian/Ubuntu +sudo apt-get install libgpiod-dev + +# On Fedora/RHEL +sudo dnf install libgpiod-devel +``` + +## Building + +```bash +make +``` + +## Configuration + +The plugin requires a CSV configuration file that maps IEC addresses to GPIO lines. The file path is specified in the OpenPLC plugin configuration. + +### CSV File Format + +The CSV file contains three columns: `IEC_ADDRESS`, `CHIP_NAME`, `LINE_IDENTIFIER` + +**Header (required):** +``` +IEC_ADDRESS,CHIP_NAME,LINE_IDENTIFIER +``` + +**Field Descriptions:** +- **IEC_ADDRESS**: IEC 61131-3 format address + - Input format: `%IX.` (e.g., `%IX0.0`) + - Output format: `%QX.` (e.g., `%QX0.1`) + - ``: Buffer index (0-1023) + - ``: Bit position within byte (0-7) + +- **CHIP_NAME**: GPIO chip device path + - Typically `/dev/gpiochip0`, `/dev/gpiochip1`, etc. + - Found in `/dev/*` directory + +- **LINE_IDENTIFIER**: GPIO line reference + - Can be a numeric offset (e.g., `0`, `5`, `17`) + - Or a named line (e.g., `LED_STATUS`, `BUTTON_RESET`) + - Named lines are resolved via the chip's line names information + +### Example I/O mapping CSV File + +**File: io-map.csv** + +```csv +IEC_ADDRESS,CHIP_NAME,LINE_IDENTIFIER +%IX0.2,/dev/gpiochip0,GPIO2 +%IX0.3,/dev/gpiochip0,GPIO3 +%IX0.4,/dev/gpiochip0,GPIO4 +%IX0.3,/dev/gpiochip0,GPIO5 +%QX0.0,/dev/gpiochip0,GPIO10 +%QX0.1,/dev/gpiochip0,GPIO11 +%QX0.2,/dev/gpiochip0,GPIO12 +%QX0.3,/dev/gpiochip0,GPIO13 +``` + +### Plugin Configuration + +Add following line to **plugins.conf** +```csv +gpiod,./core/src/drivers/plugins/native/gpiod/gpiod_plugin.so,1,1,./config/io-map.csv +``` + +### Running plugin + +When OpenPLC runtime is running mapped GPIO lines are controlled by OpenPLC, and consumer names are set +according given map configuration. + +Here is an example for raspberrypi: +```bash +pi@raspberrypi:~ $ gpioinfo +gpiochip0 - 54 lines: + line 0: "ID_SDA" input + line 1: "ID_SCL" input + line 2: "GPIO2" input consumer="OpenPLC-%IX0.2" + line 3: "GPIO3" input consumer="OpenPLC-%IX0.3" + line 4: "GPIO4" input consumer="OpenPLC-%IX0.4" + line 5: "GPIO5" input consumer="OpenPLC-%IX0.3" + line 6: "GPIO6" input + line 7: "GPIO7" input + line 8: "GPIO8" input + line 9: "GPIO9" input + line 10: "GPIO10" output consumer="OpenPLC-%QX0.0" + line 11: "GPIO11" output consumer="OpenPLC-%QX0.1" + line 12: "GPIO12" output consumer="OpenPLC-%QX0.2" + line 13: "GPIO13" output consumer="OpenPLC-%QX0.3" + line 14: "GPIO14" input + line 15: "GPIO15" input + line 16: "GPIO16" input + line 17: "GPIO17" input + line 18: "GPIO18" input + line 19: "GPIO19" input + line 20: "GPIO20" input + line 21: "GPIO21" input + line 22: "GPIO22" input + line 23: "GPIO23" input + line 24: "GPIO24" input + line 25: "GPIO25" input + line 26: "GPIO26" input + line 27: "GPIO27" input + line 28: "SDA0" input + line 29: "SCL0" input + line 30: "NC" input + line 31: "LAN_RUN" output + line 32: "CAM_GPIO1" output + line 33: "NC" input + line 34: "NC" input + line 35: "PWR_LOW_N" input consumer="PWR" + line 36: "NC" input + line 37: "NC" input + line 38: "USB_LIMIT" output + line 39: "NC" input + line 40: "PWM0_OUT" input + line 41: "CAM_GPIO0" output consumer="cam1_regulator" + line 42: "NC" input + line 43: "NC" input + line 44: "ETH_CLK" input + line 45: "PWM1_OUT" input + line 46: "HDMI_HPD_N" input active-low consumer="hpd" + line 47: "STATUS_LED" output consumer="ACT" + line 48: "SD_CLK_R" input + line 49: "SD_CMD_R" input + line 50: "SD_DATA0_R" input + line 51: "SD_DATA1_R" input + line 52: "SD_DATA2_R" input + line 53: "SD_DATA3_R" input +``` diff --git a/core/src/drivers/plugins/native/gpiod/gpiod_plugin.c b/core/src/drivers/plugins/native/gpiod/gpiod_plugin.c new file mode 100644 index 00000000..c8180c84 --- /dev/null +++ b/core/src/drivers/plugins/native/gpiod/gpiod_plugin.c @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include +#include + +/* OpenPLC plugin includes */ +#include "../plugin_logger.h" +#include "iec_types.h" +#include "plugin_types.h" + +/* Logger */ +static plugin_logger_t g_logger; + +/* Runtime args */ +static plugin_runtime_args_t g_args; // Plugin-owned copy + +/* Plugin state */ +static int plugin_initialized = 0; +static int plugin_running = 0; + +/* Fixed-size arrays */ +#define BUFFER_SIZE 1024 +#define BITS_PER_BYTE 8 + +static struct gpiod_line_request *inputs[BUFFER_SIZE][BITS_PER_BYTE] = {0}; +static struct gpiod_line_request *outputs[BUFFER_SIZE][BITS_PER_BYTE] = {0}; + +static int io_initialized = 0; + +/* ----------------- IEC parser ----------------- */ +static int parse_iec(const char *s, uint16_t *byte, uint8_t *bit, int *is_input) +{ + char io, type; + int b, bt; + if (sscanf(s, "%%%c%c%d.%d", &io, &type, &b, &bt) != 4) + return -1; + if (type != 'X' || bt < 0 || bt > 7) + return -1; + *byte = (uint16_t)b; + *bit = (uint8_t)bt; + *is_input = (io == 'I') ? 1 : 0; + return 0; +} + +static int resolve_line_offset(const char *chip_name, const char *line_name, unsigned *offset_out) +{ + struct gpiod_chip *chip = gpiod_chip_open(chip_name); + if (!chip) + return -1; + + struct gpiod_chip_info *chip_info = gpiod_chip_get_info(chip); + if (!chip_info) + { + gpiod_chip_close(chip); + return -1; + } + + unsigned nlines = gpiod_chip_info_get_num_lines(chip_info); + gpiod_chip_info_free(chip_info); + + struct gpiod_line_info *info; + for (unsigned i = 0; i < nlines; i++) + { + info = gpiod_chip_get_line_info(chip, i); + if (!info) + continue; + const char *name = gpiod_line_info_get_name(info); + if (name && strcmp(name, line_name) == 0) + { + *offset_out = i; + gpiod_line_info_free(info); + gpiod_chip_close(chip); + return 0; + } + gpiod_line_info_free(info); + } + gpiod_chip_close(chip); + return -1; +} + +static int parse_line_identifier(const char *s, const char *chip, unsigned *offset) +{ + char *end; + unsigned long v = strtoul(s, &end, 10); + if (*end == '\0') + { + *offset = (unsigned)v; + return 0; + } + return resolve_line_offset(chip, s, offset); +} + +static int init_io_from_csv(const char *filename) +{ + FILE *fp = fopen(filename, "r"); + if (!fp) + { + plugin_logger_error(&g_logger, "Failed to open CSV: %s", filename); + return -1; + } + + char line[256]; + fgets(line, sizeof(line), fp); // skip header + + for (int b = 0; b < BUFFER_SIZE; b++) + for (int bit = 0; bit < BITS_PER_BYTE; bit++) + inputs[b][bit] = outputs[b][bit] = NULL; + + while (fgets(line, sizeof(line), fp)) + { + /* Strip trailing CR/LF */ + line[strcspn(line, "\r")] = '\0'; + line[strcspn(line, "\n")] = '\0'; + + char *iec = strtok(line, ","); + char *chip = strtok(NULL, ","); + char *lin = strtok(NULL, ","); + if (!iec || !chip || !lin) + continue; + + uint16_t byte; + uint8_t bit; + int is_input; + if (parse_iec(iec, &byte, &bit, &is_input) != 0) + continue; + if (byte >= BUFFER_SIZE || bit >= BITS_PER_BYTE) + continue; + + unsigned offset; + if (parse_line_identifier(lin, chip, &offset) != 0) + { + plugin_logger_warn(&g_logger, "Line %s:%s not found", chip, lin); + continue; + } + + struct gpiod_chip *gchip = gpiod_chip_open(chip); + if (!gchip) + { + plugin_logger_error(&g_logger, "Cannot open chip %s", chip); + goto error; + } + + struct gpiod_line_settings *settings = gpiod_line_settings_new(); + enum gpiod_line_direction direction = + is_input ? GPIOD_LINE_DIRECTION_INPUT : GPIOD_LINE_DIRECTION_OUTPUT; + + gpiod_line_settings_set_direction(settings, direction); + if (!is_input) + gpiod_line_settings_set_output_value(settings, 0); + + struct gpiod_line_config *lcfg = gpiod_line_config_new(); + gpiod_line_config_add_line_settings(lcfg, &offset, 1, settings); + + struct gpiod_request_config *rcfg = gpiod_request_config_new(); + + /* --- Dynamic consumer name based on IEC address --- */ + char consumer_name[64]; + snprintf(consumer_name, sizeof(consumer_name), "OpenPLC-%s", iec); + gpiod_request_config_set_consumer(rcfg, consumer_name); + + struct gpiod_line_request *req = gpiod_chip_request_lines(gchip, rcfg, lcfg); + if (!req) + { + plugin_logger_error(&g_logger, "Line request failed"); + gpiod_line_config_free(lcfg); + gpiod_request_config_free(rcfg); + gpiod_line_settings_free(settings); + gpiod_chip_close(gchip); + goto error; + } + + if (is_input) + inputs[byte][bit] = req; + else + outputs[byte][bit] = req; + + gpiod_line_config_free(lcfg); + gpiod_request_config_free(rcfg); + gpiod_line_settings_free(settings); + gpiod_chip_close(gchip); + } + + fclose(fp); + io_initialized = 1; + return 0; + +error: + fclose(fp); + return -1; +} + +/* ----------------- Plugin hooks ----------------- */ +int init(void *args) +{ + plugin_logger_init(&g_logger, "GPIOD_PLUGIN", NULL); + plugin_logger_info(&g_logger, "Initializing GPIOD plugin..."); + + if (!args) + { + plugin_logger_error(&g_logger, "init args NULL"); + return -1; + } + // Copy the entire structure + memcpy(&g_args, args, sizeof(plugin_runtime_args_t)); + // Now g_args can be safely used in start_loop, stop_loop, etc. + + plugin_logger_init(&g_logger, "GPIOD_PLUGIN", args); + plugin_logger_info(&g_logger, "Plugin CSV: %s", g_args.plugin_specific_config_file_path); + + if (init_io_from_csv(g_args.plugin_specific_config_file_path) != 0) + { + plugin_logger_error(&g_logger, "Failed to initialize I/O from CSV"); + return -1; + } + + plugin_initialized = 1; + plugin_logger_info(&g_logger, "GPIOD plugin initialized successfully"); + return 0; +} + +void start_loop(void) +{ + if (!plugin_initialized) + { + plugin_logger_error(&g_logger, "Cannot start - not initialized"); + return; + } + plugin_logger_info(&g_logger, "GPIOD plugin loop started"); + plugin_running = 1; +} + +void stop_loop(void) +{ + if (!plugin_running) + return; + plugin_logger_info(&g_logger, "GPIOD plugin loop stopped"); + plugin_running = 0; +} + +void cycle_start(void) +{ + enum gpiod_line_value val; + + if (!plugin_initialized || !plugin_running || !io_initialized) + return; + + /* Buffer mutex already held by OpenPLC */ + for (int b = 0; b < BUFFER_SIZE; b++) + { + for (int bit = 0; bit < BITS_PER_BYTE; bit++) + { + if (inputs[b][bit]) + { + val = gpiod_line_request_get_value(inputs[b][bit], 0); + if (val != GPIOD_LINE_VALUE_ERROR) + *g_args.bool_input[b][bit] = val ? true : false; + } + } + } +} + +void cycle_end(void) +{ + enum gpiod_line_value val; + + if (!plugin_initialized || !plugin_running || !io_initialized) + return; + + /* Buffer mutex already held by OpenPLC */ + for (int b = 0; b < BUFFER_SIZE; b++) + { + for (int bit = 0; bit < BITS_PER_BYTE; bit++) + { + if (outputs[b][bit]) + { + val = *g_args.bool_output[b][bit] ? GPIOD_LINE_VALUE_ACTIVE + : GPIOD_LINE_VALUE_INACTIVE; + gpiod_line_request_set_value(outputs[b][bit], 0, val); + } + } + } +} + +void cleanup(void) +{ + plugin_logger_info(&g_logger, "Cleaning up GPIOD plugin..."); + + if (plugin_running) + stop_loop(); + + if (io_initialized) + { + for (int b = 0; b < BUFFER_SIZE; b++) + { + for (int bit = 0; bit < BITS_PER_BYTE; bit++) + { + if (inputs[b][bit]) + { + gpiod_line_request_release(inputs[b][bit]); + inputs[b][bit] = NULL; + } + if (outputs[b][bit]) + { + gpiod_line_request_release(outputs[b][bit]); + outputs[b][bit] = NULL; + } + } + } + io_initialized = 0; + } + + plugin_initialized = 0; + memset(&g_args, 0, sizeof(plugin_runtime_args_t)); + plugin_logger_info(&g_logger, "GPIOD plugin cleanup complete"); +} diff --git a/core/src/drivers/plugins/native/gpiod/test_plugin_loader.c b/core/src/drivers/plugins/native/gpiod/test_plugin_loader.c new file mode 100644 index 00000000..04f07839 --- /dev/null +++ b/core/src/drivers/plugins/native/gpiod/test_plugin_loader.c @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include + +// Define plugin_runtime_args_t structure locally (same as in plugin) +// TODO: Ideally, include from a shared header but avoiding Python dependencies here +typedef struct +{ + // Buffer pointers + void *(*bool_input)[8]; + void *(*bool_output)[8]; + void **byte_input; + void **byte_output; + void **int_input; + void **int_output; + void **dint_input; + void **dint_output; + void **lint_input; + void **lint_output; + void **int_memory; + void **dint_memory; + void **lint_memory; + + // Mutex functions + 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; + int bits_per_buffer; +} plugin_runtime_args_t; + +// Function pointer types +typedef int (*plugin_init_func_t)(void *); + +// Simple mutex functions for testing +int test_mutex_take(pthread_mutex_t *mutex) { + return pthread_mutex_lock(mutex); +} + +int test_mutex_give(pthread_mutex_t *mutex) { + return pthread_mutex_unlock(mutex); +} + +int main() { + printf("Testing native plugin loading...\n"); + + // Create a mock runtime args structure + plugin_runtime_args_t args; + memset(&args, 0, sizeof(plugin_runtime_args_t)); + + // Initialize with test values + args.buffer_size = 1024; + args.bits_per_buffer = 8; + strcpy(args.plugin_specific_config_file_path, "./test_config.ini"); + + // Create a test mutex + pthread_mutex_t test_mutex; + pthread_mutex_init(&test_mutex, NULL); + args.buffer_mutex = &test_mutex; + args.mutex_take = test_mutex_take; + args.mutex_give = test_mutex_give; + + // Load the plugin + void *handle = dlopen("./gpiod_plugin.so", RTLD_LAZY); + if (!handle) { + fprintf(stderr, "Failed to load plugin: %s\n", dlerror()); + return 1; + } + + printf("Plugin loaded successfully!\n"); + + // Clear any existing error + dlerror(); + + // Get the init function + plugin_init_func_t init_func = (plugin_init_func_t)dlsym(handle, "init"); + if (!init_func) { + fprintf(stderr, "Failed to find 'init' function: %s\n", dlerror()); + dlclose(handle); + return 1; + } + + printf("Found 'init' function!\n"); + + // Call the init function + int result = init_func(&args); + if (result != 0) { + fprintf(stderr, "Plugin init failed with code: %d\n", result); + dlclose(handle); + return 1; + } + + printf("Plugin initialized successfully!\n"); + + // Test other functions if they exist + void (*start_func)() = (void (*)())dlsym(handle, "start_loop"); + if (start_func) { + printf("Found 'start_loop' function, calling it...\n"); + start_func(); + } else { + printf("'start_loop' function not found (optional)\n"); + } + + void (*stop_func)() = (void (*)())dlsym(handle, "stop_loop"); + if (stop_func) { + printf("Found 'stop_loop' function, calling it...\n"); + stop_func(); + } else { + printf("'stop_loop' function not found (optional)\n"); + } + + void (*cleanup_func)() = (void (*)())dlsym(handle, "cleanup"); + if (cleanup_func) { + printf("Found 'cleanup' function, calling it...\n"); + cleanup_func(); + } else { + printf("'cleanup' function not found (optional)\n"); + } + + // Close the plugin + dlclose(handle); + pthread_mutex_destroy(&test_mutex); + + printf("Plugin test completed successfully!\n"); + return 0; +}