Design and Implementation of a Modbus Calculation Rule Engine Based on CSV Configuration

In industrial automation systems, real-time calculation and processing of Modbus register data is often required. There is an urgent need for a lightweight calculation rule engine implemented based on CSV configuration files, supporting basic operations and complex multi-register calculations.

System Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   CSV Configuration File │───▶│   Rule Parser    │───▶│   Calculation Engine      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                                        │
┌─────────────────┐    ┌─────────────────┐              │
│   Modbus Device    │◀───│   Result Output      │◀─────────────┘
└─────────────────┘    └─────────────────┘

CSV Configuration File Format

Basic Format

Rule ID,Input Register,Calculation Formula,Output Register,Data Type
1,40001,40002,+,40003,uint16
2,40004,40005,*,40006,int32
3,40007,40008,/,40009,float

Complex Calculation Format

Rule ID,Input Register,Calculation Formula,Output Register,Data Type
4,40001,40002,40003,(R40001+R40002)*R40003,40004,float
5,40005,40006,40007,40008,(R40005-R40006)/(R40007+R40008),40009,float

Core Data Structures

// Register Data Type Enumeration
typedef enum {
    REG_UINT16 = 0,
    REG_INT16,
    REG_UINT32,
    REG_INT32,
    REG_FLOAT
} reg_data_type_t;

// Calculation Rule Structure
typedef struct {
    uint16_t rule_id;
    uint16_t input_regs[8];      // Supports up to 8 input registers
    uint8_t input_count;
    char formula[128];           // Calculation formula string
    uint16_t output_reg;
    reg_data_type_t data_type;
} calc_rule_t;

// Calculation Engine Structure
typedef struct {
    calc_rule_t rules[MAX_RULES];
    uint16_t rule_count;
    float* modbus_data;          // Modbus register data cache
} calc_engine_t;

Core Implementation Code

1. CSV Parser

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_RULES 64
#define MAX_LINE_LEN 256

// Parse CSV file
int parse_csv_rules(const char* filename, calc_engine_t* engine) {
    FILE* file = fopen(filename, "r");
    if (!file) {
        printf("Error: Cannot open file %s\n", filename);
        return -1;
    }
    
    char line[MAX_LINE_LEN];
    int rule_count = 0;
    
    // Skip header line
    fgets(line, sizeof(line), file);
    
    while (fgets(line, sizeof(line), file) && rule_count < MAX_RULES) {
        calc_rule_t* rule = &engine->rules[rule_count];
        
        // Parse CSV line
        char* token = strtok(line, ",");
        if (token) rule->rule_id = atoi(token);
        
        // Parse input registers
        rule->input_count = 0;
        while ((token = strtok(NULL, ",")) && rule->input_count < 8) {
            if (strstr(token, "R") == token) {
                rule->input_regs[rule->input_count++] = atoi(token + 1);
            } else if (strchr(token, '+') || strchr(token, '-') || 
                      strchr(token, '*') || strchr(token, '/')) {
                // Stop parsing input registers upon encountering an operator
                strcpy(rule->formula, token);
                break;
            }
        }
        
        // Parse output register and data type
        if ((token = strtok(NULL, ","))) {
            rule->output_reg = atoi(token);
        }
        if ((token = strtok(NULL, ","))) {
            rule->data_type = parse_data_type(token);
        }
        
        rule_count++;
    }
    
    engine->rule_count = rule_count;
    fclose(file);
    return 0;
}

// Parse data type
reg_data_type_t parse_data_type(const char* type_str) {
    if (strcmp(type_str, "uint16") == 0) return REG_UINT16;
    if (strcmp(type_str, "int16") == 0) return REG_INT16;
    if (strcmp(type_str, "uint32") == 0) return REG_UINT32;
    if (strcmp(type_str, "int32") == 0) return REG_INT32;
    if (strcmp(type_str, "float") == 0) return REG_FLOAT;
    return REG_UINT16; // Default type
}

2. Core of the Calculation Engine

// Execute a single rule calculation
float execute_rule(const calc_rule_t* rule, const float* modbus_data) {
    float result = 0.0f;
    
    // Simple arithmetic operations
    if (rule->input_count == 2) {
        float val1 = modbus_data[rule->input_regs[0] - 40001];
        float val2 = modbus_data[rule->input_regs[1] - 40001];
        
        if (strcmp(rule->formula, "+") == 0) {
            result = val1 + val2;
        } else if (strcmp(rule->formula, "-") == 0) {
            result = val1 - val2;
        } else if (strcmp(rule->formula, "*") == 0) {
            result = val1 * val2;
        } else if (strcmp(rule->formula, "/") == 0) {
            result = (val2 != 0) ? val1 / val2 : 0;
        }
    }
    // Complex formula calculation
    else if (strlen(rule->formula) > 1) {
        result = evaluate_complex_formula(rule, modbus_data);
    }
    
    return result;
}

// Complex formula calculation (simplified)
float evaluate_complex_formula(const calc_rule_t* rule, const float* modbus_data) {
    // Implement a simplified expression parser
    // Supports format: (R40001+R40002)*R40003
    
    char* formula = rule->formula;
    float result = 0.0f;
    float operands[8];
    char operators[7];
    int op_count = 0, val_count = 0;
    
    // Parse operands and operators
    char* token = strtok(formula, "()+-*/");
    while (token) {
        if (strstr(token, "R") == token) {
            int reg_addr = atoi(token + 1);
            operands[val_count++] = modbus_data[reg_addr - 40001];
        }
        token = strtok(NULL, "()+-*/");
    }
    
    // Re-parse operators
    strcpy(formula, rule->formula);
    for (int i = 0; formula[i]; i++) {
        if (formula[i] == '+' || formula[i] == '-' || 
            formula[i] == '*' || formula[i] == '/') {
            operators[op_count++] = formula[i];
        }
    }
    
    // Perform calculation (multiplication and division first)
    result = operands[0];
    for (int i = 0; i < op_count; i++) {
        switch (operators[i]) {
            case '+': result += operands[i + 1]; break;
            case '-': result -= operands[i + 1]; break;
            case '*': result *= operands[i + 1]; break;
            case '/': 
                if (operands[i + 1] != 0) result /= operands[i + 1];
                break;
        }
    }
    
    return result;
}

3. Main Control Loop

// Initialize calculation engine
int calc_engine_init(calc_engine_t* engine, const char* config_file) {
    // Allocate Modbus data cache
    engine->modbus_data = (float*)malloc(1000 * sizeof(float));
    if (!engine->modbus_data) {
        printf("Error: Memory allocation failed\n");
        return -1;
    }
    
    // Parse CSV configuration file
    if (parse_csv_rules(config_file, engine) != 0) {
        free(engine->modbus_data);
        return -1;
    }
    
    printf("Loaded %d calculation rules\n", engine->rule_count);
    return 0;
}

// Execute all calculation rules
void calc_engine_execute(calc_engine_t* engine) {
    for (int i = 0; i < engine->rule_count; i++) {
        calc_rule_t* rule = &engine->rules[i];
        float result = execute_rule(rule, engine->modbus_data);
        
        // Convert result based on data type
        switch (rule->data_type) {
            case REG_UINT16:
                engine->modbus_data[rule->output_reg - 40001] = (uint16_t)result;
                break;
            case REG_INT16:
                engine->modbus_data[rule->output_reg - 40001] = (int16_t)result;
                break;
            case REG_UINT32:
                engine->modbus_data[rule->output_reg - 40001] = (uint32_t)result;
                break;
            case REG_INT32:
                engine->modbus_data[rule->output_reg - 40001] = (int32_t)result;
                break;
            case REG_FLOAT:
            default:
                engine->modbus_data[rule->output_reg - 40001] = result;
                break;
        }
        
        printf("Rule %d: Result = %.2f -> Register %d\n", 
               rule->rule_id, result, rule->output_reg);
    }
}

// Clean up resources
void calc_engine_cleanup(calc_engine_t* engine) {
    if (engine->modbus_data) {
        free(engine->modbus_data);
        engine->modbus_data = NULL;
    }
}

Usage Example

#include "calc_engine.h"

int main() {
    calc_engine_t engine;
    
    // Initialize calculation engine
    if (calc_engine_init(&engine, "rules.csv") != 0) {
        return -1;
    }
    
    // Simulate Modbus data
    engine.modbus_data[0] = 10.0f;  // 40001
    engine.modbus_data[1] = 20.0f;  // 40002
    engine.modbus_data[2] = 5.0f;   // 40003
    engine.modbus_data[3] = 2.0f;   // 40004
    
    // Execute calculations
    calc_engine_execute(&engine);
    
    // Clean up resources
    calc_engine_cleanup(&engine);
    
    return 0;
}

Leave a Comment