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;
}