#include "modbus_points.h" #include #include #include #include /* ---------- local helpers ---------- */ static void trim_whitespace(char *s) { char *start; char *end; if (s == NULL || *s == '\0') return; start = s; while (*start != '\0' && isspace((unsigned char)*start)) start++; if (start != s) memmove(s, start, strlen(start) + 1U); if (*s == '\0') return; end = s + strlen(s) - 1; while (end >= s && isspace((unsigned char)*end)) { *end = '\0'; end--; } } static bool str_equals(const char *a, const char *b) { if (a == NULL || b == NULL) return false; return strcmp(a, b) == 0; } static modbus_point_type_t parse_point_type(const char *s) { if (str_equals(s, "Coil")) return MB_POINT_COIL; if (str_equals(s, "Discrete_Input")) return MB_POINT_DISCRETE_INPUT; if (str_equals(s, "Input_Register")) return MB_POINT_INPUT_REGISTER; if (str_equals(s, "Holding_Register")) return MB_POINT_HOLDING_REGISTER; return MB_POINT_INVALID; } static modbus_data_type_t parse_data_type(const char *s) { if (str_equals(s, "bool")) return MB_DATA_BOOL; if (str_equals(s, "uint16")) return MB_DATA_UINT16; if (str_equals(s, "uint32")) return MB_DATA_UINT32; if (str_equals(s, "float")) return MB_DATA_FLOAT; if (str_equals(s, "double")) return MB_DATA_DOUBLE; return MB_DATA_INVALID; } static pics_data_type_t parse_pics_data_type(const char *s) { if (str_equals(s, "Boolean")) return PICS_DATA_BOOLEAN; if (str_equals(s, "Integer")) return PICS_DATA_INTEGER; if (str_equals(s, "Float")) return PICS_DATA_FLOAT; if (str_equals(s, "String")) return PICS_DATA_STRING; if (s == NULL || *s == '\0') return PICS_DATA_NONE; return PICS_DATA_INVALID; } static bool parse_u32(const char *s, uint32_t *value) { char *endptr; unsigned long v; if (s == NULL || value == NULL || *s == '\0') return false; v = strtoul(s, &endptr, 10); if (*endptr != '\0') return false; *value = (uint32_t)v; return true; } static modbus_point_type_t infer_point_type_from_address(uint32_t raw_address) { uint32_t family_digit; if (raw_address == 0U) return MB_POINT_INVALID; if (raw_address < 10000U) return MB_POINT_COIL; if (raw_address >= 100000U) family_digit = raw_address / 100000U; else family_digit = raw_address / 10000U; switch (family_digit) { case 1U: return MB_POINT_DISCRETE_INPUT; case 3U: return MB_POINT_INPUT_REGISTER; case 4U: return MB_POINT_HOLDING_REGISTER; default: return MB_POINT_INVALID; } } static bool normalize_address(modbus_point_type_t type, uint32_t raw_address, uint16_t *offset) { uint32_t index_1_based; uint32_t off; uint32_t family_digit = 0U; if (offset == NULL || raw_address == 0U) return false; if (raw_address >= 100000U) { family_digit = raw_address / 100000U; index_1_based = raw_address % 100000U; } else if (raw_address >= 10000U) { family_digit = raw_address / 10000U; index_1_based = raw_address % 10000U; } else { family_digit = 0U; index_1_based = raw_address; } if (index_1_based == 0U) return false; switch (type) { case MB_POINT_COIL: if (family_digit != 0U) return false; off = index_1_based - 1U; break; case MB_POINT_DISCRETE_INPUT: if (family_digit != 1U) return false; off = index_1_based - 1U; break; case MB_POINT_INPUT_REGISTER: if (family_digit != 3U) return false; off = index_1_based - 1U; break; case MB_POINT_HOLDING_REGISTER: if (family_digit != 4U) return false; off = index_1_based - 1U; break; default: return false; } if (off > 0xFFFFU) return false; *offset = (uint16_t)off; return true; } static bool validate_type_and_datatype(modbus_point_type_t type, modbus_data_type_t data_type) { switch (type) { case MB_POINT_COIL: case MB_POINT_DISCRETE_INPUT: return data_type == MB_DATA_BOOL; case MB_POINT_INPUT_REGISTER: case MB_POINT_HOLDING_REGISTER: return data_type == MB_DATA_UINT16 || data_type == MB_DATA_UINT32 || data_type == MB_DATA_FLOAT || data_type == MB_DATA_DOUBLE; default: return false; } } static uint8_t data_type_reg_span(modbus_data_type_t data_type) { switch (data_type) { case MB_DATA_BOOL: case MB_DATA_UINT16: return 1U; case MB_DATA_UINT32: case MB_DATA_FLOAT: return 2U; case MB_DATA_DOUBLE: return 4U; default: return 0U; } } static bool db_has_duplicate_type_offset(const modbus_db_t *db, modbus_point_type_t type, uint16_t offset) { size_t i; if (db == NULL) return false; for (i = 0; i < db->count; i++) { if (db->points[i].type == type && db->points[i].offset == offset) return true; } return false; } static bool ranges_overlap(uint16_t start_a, uint8_t span_a, uint16_t start_b, uint8_t span_b) { uint32_t end_a; uint32_t end_b; if (span_a == 0U || span_b == 0U) return false; end_a = (uint32_t)start_a + (uint32_t)span_a - 1U; end_b = (uint32_t)start_b + (uint32_t)span_b - 1U; return !((end_a < start_b) || (end_b < start_a)); } const modbus_point_t *modbus_points_find_by_pics_offset(const modbus_db_t *db, modbus_point_type_t type, uint16_t offset) { size_t i; if (db == NULL) return NULL; for (i = 0; i < db->count; i++) { if (db->points[i].has_pics_address && db->points[i].pics_type == type && db->points[i].pics_offset == offset) return &db->points[i]; } return NULL; } const modbus_point_t *modbus_points_find_by_pics_range(const modbus_db_t *db, modbus_point_type_t type, uint16_t offset) { size_t i; if (db == NULL) return NULL; for (i = 0; i < db->count; i++) { const modbus_point_t *pt = &db->points[i]; uint32_t end_offset; if (!pt->has_pics_address || pt->pics_reg_span == 0U) continue; if (pt->pics_type != type) continue; end_offset = (uint32_t)pt->pics_offset + (uint32_t)pt->pics_reg_span - 1U; if ((uint32_t)offset >= (uint32_t)pt->pics_offset && (uint32_t)offset <= end_offset) return pt; } return NULL; } static bool db_has_duplicate_pics_range(const modbus_db_t *db, modbus_point_type_t pics_type, uint16_t pics_offset, uint8_t pics_reg_span) { size_t i; if (db == NULL || pics_reg_span == 0U) return false; for (i = 0; i < db->count; i++) { const modbus_point_t *pt = &db->points[i]; if (!pt->has_pics_address || pt->pics_reg_span == 0U) continue; if (pt->pics_type != pics_type) continue; if (ranges_overlap(pt->pics_offset, pt->pics_reg_span, pics_offset, pics_reg_span)) return true; } return false; } static bool validate_control_mapping(const modbus_point_t *pt) { if (pt == NULL) return false; if (!pt->has_control_address) return true; if (!modbus_points_is_writable_type(pt->control_type)) return false; switch (pt->type) { case MB_POINT_DISCRETE_INPUT: return pt->control_type == MB_POINT_COIL; case MB_POINT_INPUT_REGISTER: return pt->control_type == MB_POINT_HOLDING_REGISTER; case MB_POINT_COIL: case MB_POINT_HOLDING_REGISTER: return false; default: return false; } } static bool append_helper_points(modbus_db_t *db, const modbus_point_t *base_pt) { uint8_t i; if (db == NULL || base_pt == NULL) return false; if (base_pt->reg_span <= 1U) return true; for (i = 1U; i < base_pt->reg_span; i++) { modbus_point_t helper = *base_pt; helper.raw_address = base_pt->raw_address + (uint32_t)i; helper.offset = (uint16_t)(base_pt->offset + i); helper.data_type = MB_DATA_UINT16; helper.reg_span = 1U; helper.is_helper = true; helper.has_control_address = false; helper.control_raw_address = 0U; helper.control_type = MB_POINT_INVALID; helper.control_offset = 0U; helper.has_pics_address = false; helper.pics_raw_address = 0U; helper.pics_type = MB_POINT_INVALID; helper.pics_offset = 0U; helper.pics_data_type = PICS_DATA_NONE; helper.pics_reg_span = 0U; memset(helper.pics_words, 0, sizeof(helper.pics_words)); helper.bit_value = 0U; helper.reg_value = 0U; if (db->count >= MODBUS_MAX_POINTS) return false; if (db_has_duplicate_type_offset(db, helper.type, helper.offset)) return false; db->points[db->count++] = helper; } return true; } static bool parse_csv_line(const char *line_in, modbus_point_t *pt) { char line[MODBUS_MAX_CSV_LINE_LEN]; char *fields[8]; size_t field_count = 0; char *p; uint32_t raw_address; if (line_in == NULL || pt == NULL) return false; strncpy(line, line_in, sizeof(line) - 1U); line[sizeof(line) - 1U] = '\0'; p = line; fields[field_count++] = p; while (*p != '\0' && field_count < 8U) { if (*p == ',') { *p = '\0'; fields[field_count++] = p + 1; } p++; } if (field_count != 8U) return false; for (size_t i = 0; i < field_count; i++) trim_whitespace(fields[i]); memset(pt, 0, sizeof(*pt)); pt->control_type = MB_POINT_INVALID; pt->bit_value = 0U; pt->reg_value = 0U; pt->type = parse_point_type(fields[0]); if (pt->type == MB_POINT_INVALID) return false; if (!parse_u32(fields[1], &raw_address)) return false; pt->raw_address = raw_address; if (!normalize_address(pt->type, pt->raw_address, &pt->offset)) return false; pt->data_type = parse_data_type(fields[3]); if (pt->data_type == MB_DATA_INVALID) return false; if (!validate_type_and_datatype(pt->type, pt->data_type)) return false; pt->reg_span = data_type_reg_span(pt->data_type); pt->is_helper = false; if (pt->reg_span == 0U) return false; if (fields[4][0] != '\0') { uint32_t control_raw; if (!parse_u32(fields[4], &control_raw)) return false; pt->has_control_address = true; pt->control_raw_address = control_raw; pt->control_type = infer_point_type_from_address(control_raw); if (pt->control_type == MB_POINT_INVALID) return false; if (!normalize_address(pt->control_type, pt->control_raw_address, &pt->control_offset)) return false; if (!validate_control_mapping(pt)) return false; } else { pt->has_control_address = false; pt->control_raw_address = 0U; pt->control_type = MB_POINT_INVALID; pt->control_offset = 0U; } if (fields[5][0] != '\0') { uint32_t pics_raw; if (!parse_u32(fields[5], &pics_raw)) return false; pt->has_pics_address = true; pt->pics_raw_address = pics_raw; pt->pics_type = infer_point_type_from_address(pics_raw); if (pt->pics_type == MB_POINT_INVALID) return false; if (!normalize_address(pt->pics_type, pt->pics_raw_address, &pt->pics_offset)) return false; } else { pt->has_pics_address = false; pt->pics_raw_address = 0U; pt->pics_type = MB_POINT_INVALID; pt->pics_offset = 0U; } pt->pics_data_type = parse_pics_data_type(fields[6]); if (pt->pics_data_type == PICS_DATA_INVALID) return false; if (pt->pics_data_type == PICS_DATA_STRING) return false; switch (pt->pics_data_type) { case PICS_DATA_BOOLEAN: pt->pics_reg_span = 1U; break; case PICS_DATA_INTEGER: pt->pics_reg_span = 2U; break; case PICS_DATA_FLOAT: pt->pics_reg_span = 4U; break; case PICS_DATA_NONE: pt->pics_reg_span = 0U; break; default: pt->pics_reg_span = 0U; break; } if (pt->has_pics_address && pt->pics_data_type == PICS_DATA_NONE) return false; if (!pt->has_pics_address && pt->pics_data_type != PICS_DATA_NONE) return false; if (pt->has_pics_address) { switch (pt->pics_data_type) { case PICS_DATA_INTEGER: case PICS_DATA_FLOAT: if (pt->pics_type != MB_POINT_HOLDING_REGISTER) return false; break; case PICS_DATA_BOOLEAN: if (pt->pics_type != MB_POINT_COIL && pt->pics_type != MB_POINT_HOLDING_REGISTER) return false; break; default: break; } } return true; } /* ---------- public API ---------- */ void modbus_points_init(modbus_db_t *db) { modbus_points_reset(db); } void modbus_points_reset(modbus_db_t *db) { if (db == NULL) return; memset(db, 0, sizeof(*db)); } bool modbus_points_load(modbus_db_t *db, const char *filename) { FILE *fp; char line[MODBUS_MAX_CSV_LINE_LEN]; size_t line_num = 0; if (db == NULL || filename == NULL) return false; fp = fopen(filename, "r"); if (fp == NULL) { printf("CSV ERROR: failed to open file: %s\n", filename); return false; } modbus_points_reset(db); while (fgets(line, sizeof(line), fp) != NULL) { modbus_point_t pt; char *newline; line_num++; newline = strchr(line, '\n'); if (newline != NULL) *newline = '\0'; newline = strchr(line, '\r'); if (newline != NULL) *newline = '\0'; trim_whitespace(line); if (line[0] == '\0') continue; if (line_num == 1U) continue; { char temp_line[MODBUS_MAX_CSV_LINE_LEN]; char *fields[8]; size_t field_count = 0; char *p; bool all_fields_empty = true; size_t i; strncpy(temp_line, line, sizeof(temp_line) - 1U); temp_line[sizeof(temp_line) - 1U] = '\0'; p = temp_line; fields[field_count++] = p; while (*p != '\0' && field_count < 8U) { if (*p == ',') { *p = '\0'; fields[field_count++] = p + 1; } p++; } if (field_count == 8U) { for (i = 0; i < field_count; i++) { trim_whitespace(fields[i]); if (fields[i][0] != '\0') { all_fields_empty = false; break; } } if (all_fields_empty) continue; } } if (db->count >= MODBUS_MAX_POINTS) { printf("CSV ERROR line %u: exceeded MODBUS_MAX_POINTS (%u)\n", (unsigned)line_num, (unsigned)MODBUS_MAX_POINTS); fclose(fp); return false; } if (!parse_csv_line(line, &pt)) { printf("CSV ERROR line %u: parse failed: %s\n", (unsigned)line_num, line); fclose(fp); return false; } if (db_has_duplicate_type_offset(db, pt.type, pt.offset)) { printf("CSV ERROR line %u: duplicate Modbus address raw=%lu offset=%u\n", (unsigned)line_num, (unsigned long)pt.raw_address, (unsigned)pt.offset); fclose(fp); return false; } if (pt.has_pics_address && db_has_duplicate_pics_range(db, pt.pics_type, pt.pics_offset, pt.pics_reg_span)) { printf("CSV ERROR line %u: overlapping PICS range pics_raw=%lu pics_offset=%u span=%u\n", (unsigned)line_num, (unsigned long)pt.pics_raw_address, (unsigned)pt.pics_offset, (unsigned)pt.pics_reg_span); fclose(fp); return false; } db->points[db->count++] = pt; if (!append_helper_points(db, &pt)) { printf("CSV ERROR line %u: failed to append helper points\n", (unsigned)line_num); fclose(fp); return false; } } fclose(fp); return true; } const modbus_point_t *modbus_points_find_by_type_offset(const modbus_db_t *db, modbus_point_type_t type, uint16_t offset) { size_t i; if (db == NULL) return NULL; for (i = 0; i < db->count; i++) { if (db->points[i].type == type && db->points[i].offset == offset) return &db->points[i]; } return NULL; } modbus_mem_type_t modbus_points_type_to_mem(modbus_point_type_t type) { switch (type) { case MB_POINT_COIL: return MB_MEM_COIL; case MB_POINT_DISCRETE_INPUT: return MB_MEM_DISCRETE_INPUT; case MB_POINT_INPUT_REGISTER: return MB_MEM_INPUT_REGISTER; case MB_POINT_HOLDING_REGISTER: return MB_MEM_HOLDING_REGISTER; default: return MB_MEM_COIL; } } bool modbus_points_is_bit_type(modbus_point_type_t type) { return (type == MB_POINT_COIL || type == MB_POINT_DISCRETE_INPUT); } bool modbus_points_is_reg_type(modbus_point_type_t type) { return (type == MB_POINT_INPUT_REGISTER || type == MB_POINT_HOLDING_REGISTER); } bool modbus_points_is_writable_type(modbus_point_type_t type) { return (type == MB_POINT_COIL || type == MB_POINT_HOLDING_REGISTER); }