RS485-MODBUS-MQTT-DASHBOARD

Aus TippvomTibb
Zur Navigation springen Zur Suche springen

Allgemeines

Eigentlich sollte die Seite RS485->MODBUS->MQTT->DASHBOARD heiszen, MediaWiki laesst aber kein Sonderzeichen im Titel zu. Der eigentliche Titel soll auf die Kette des Datenflusses hindeuten der auf dieser Seite beschrieben wird.

FHEM, HA und weitere sind zwar nette Helfer der Smarthomer, aber sobald es komplexer wird sind gerade die zu grafischen Zentralen eher ein Nachteil. HA mit seinen YAMLs geht noch gerade so, FHEM mit seiner einzigen Confdatei ist des Admins Liebling, aber wenn das System zu komplex wird (> 100 KNX/GA, > 50 Zigbee, > 20 MODBUS, ...) verliert man sich schnell im Config-Dschungel.

Kein Beispiel ist so schlecht, dass es nicht doch fuer etwas gut ist. Z.B. die FOX Integration in HA funktioniert zwar auf den ersten Blick, eine Erweiterung oder gar Anpassung ist aufgrund der katastrophalen Softwarerealisierung vergebene Liebesmuehe. Nicht besser haben es die Programmierer von mbmd [1] gemacht. Ein paar wenige Registeradressen im Programmcode in eine Liste einprogrammiert. Grrrrrr!!!

Eine Tibb Grundregel lautet:

Baue jedes Stueck Software so, dass es als Modul wiederverwendet werden kann.

Dagegen hat man bei der FOX-Integration verstoszen und somit habe ich dieser Loesung zwar nicht geloescht, aber sie wird auf Dauer ersetzt werden.

Grundsaetze

Die Grundsaetze gewinnen natuerlich mehr an Bedeutung je groeszer die Anlage wird.

Zentralisiere dein Smarthome nie auf eine Hardware oder eine Software!!!

Sowohl bei Hardware, als auch bei Software ist Redundanz oberstes Gebot.

Die Komponenten meines Smarthomes sind zudem alle USV-gesichert. Die USV-Anlage (Notstrom) ist eine 1-phasige Victron Energy Anlage (10 kWh). Es ist eine Netzersatzanlage (NEA) und dient der Sicherheitsstromversorgung. Ich kann jedem der ein Haus neu baut nur dringend ans Herz legen eine solche Ersatzinstallation mit zu planen.

Binde nie eine Smarthome-Komponente direkt an einen vermeintlichen Alleskoenner!!!

Das bedeutet zum Beispiel KNX nicht direkt an FHEM ode HA, sondern erst an KNXD und von dort aus an die Smarthomezentrale (SHZ).

Der groeszte Unfug sind "intelligente" (Touch-)Display. Ganz schlimm wenn sie auch nur mit proprietaerer Software zu programmieren sind.

Niemalsnie verwende Funk wo auch ein Kabel geht.

(TODO)

Datenfluss

Aus den Grundsaetzen leitet sich die folgende Datenkette indirekt ab.

Geraet (MODBUS) -> MODBUS Scanner/Dispatcher -> MQTT (Standalone) -> SHZ und/oder Web Signature Display/Board

Geraete

Also
  • Smartmeter
  • Wechselrichter
  • Wallboxen
  • Klimageraete
  • Sensoren

...

und ueber MODBUS RTU oder MODBUS TCP kontaktierbar.

Mittlerweile bin ich von den RS485-ETH-Adaptern nicht mehr so ueberzeugt. Die Konfiguration ist extrem holprig und es beleibt immer so ein Gefuehl der Unsicherheit. Da der MODBUS Scanner/Dispatcher/Daemon, wie auch immer man ihn nennen mag, ja eh auf einem PC/Mikrocontroller laufen muss, kann man auch gleich RS485 per USB oder Modul anbinden.

MODBUS Scanner/Dispatcher/Daemon

Die Software ist in C realisiert und die einfache Protierung auf einen Mikrocontroller habe ich mir damit offen gehalten.

MODBUS Scanner
/*
 * modbus_ncurses_scan_json.c
 *
 * Modbus/TCP Registerscanner mit ncurses-Matrixdarstellung
 * Liest zusammenhaengende Register in Bloecken von jeweils bis zu 10 Werten.
 * Optional mit JSON-Registerdatei:
 *   [
 *     {"adresse":0,"einheit":"V","bezeichnung":"L1-N","beschreibung":"","datentyp":"integer","koordinaten":"1,1"}
 *   ]
 *
 * Aufruf:
 *   ./modbusscan <ip> <port> <unitid> <fc> <start> <end> [interval_ms] [jsonfile]
 *                 [mqtt_host] [mqtt_port] [mqtt_topic] [mqtt_user] [mqtt_password]
 *
 * Beispiele:
 *   ./modbusscan 192.168.178.68 502 1 3 1 100
 *   ./modbusscan 192.168.178.68 502 1 3 1 100 1000 em24_register.json
 *   ./modbusscan 192.168.178.68 502 1 3 1 100 0 em24_register.json
 *
 * Tasten:
 *   q = Ende
 *   h = Hex-Anzeige
 *   d = Dezimal-Anzeige
 *   t = Toggle Hex/Dezimal
 *
 * Kompilieren:
 *   gcc modbus_ncurses_scan_json_mqtt.c -o modbusscan -lncurses -lmosquitto
 */

#include <arpa/inet.h>
#include <errno.h>
#include <ncurses.h>
#include <mosquitto.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <ctype.h>

#define TIMEOUT_SEC 1
#define LABEL_LEN 32
#define UNIT_LEN 16
#define TYPE_LEN 32
#define DESC_LEN 128
#define MODBUS_BLOCK_SIZE 10
#define MQTT_PAYLOAD_INITIAL_SIZE 4096

typedef struct {
    int ok;
    uint16_t value;
    uint8_t exception_code;
} ScanResult;

typedef struct {
    int present;
    int address;
    char unit[UNIT_LEN];
    char label[LABEL_LEN];
    char description[DESC_LEN];
    int factor;
    char datatype[TYPE_LEN];
    int has_coordinates;
    int coord_x;
    int coord_y;
} RegisterInfo;

static uint16_t transaction_id = 1;
static int display_hex = 1;

static int connect_tcp(const char *ip, int port) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) return -1;

    struct timeval tv;
    tv.tv_sec = TIMEOUT_SEC;
    tv.tv_usec = 0;

    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));

    addr.sin_family = AF_INET;
    addr.sin_port = htons((uint16_t)port);

    if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
        close(sock);
        return -1;
    }

    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        close(sock);
        return -1;
    }

    return sock;
}

static int read_exact(int sock, uint8_t *buf, int len) {
    int total = 0;

    while (total < len) {
        int n = recv(sock, buf + total, len - total, 0);
        if (n <= 0) return -1;
        total += n;
    }

    return total;
}

static int read_register_block(
    const char *ip,
    int port,
    int unit_id,
    int function_code,
    int start_address,
    int quantity,
    ScanResult *results
) {
    if (!results || quantity < 1 || quantity > 125) return -1;

    for (int i = 0; i < quantity; i++) {
        results[i].ok = 0;
        results[i].value = 0;
        results[i].exception_code = 0;
    }

    int sock = connect_tcp(ip, port);
    if (sock < 0) return -1;

    uint8_t req[12];
    uint16_t tid = transaction_id++;

    req[0] = (uint8_t)(tid >> 8);
    req[1] = (uint8_t)(tid & 0xff);
    req[2] = 0x00;
    req[3] = 0x00;
    req[4] = 0x00;
    req[5] = 0x06;
    req[6] = (uint8_t)(unit_id & 0xff);
    req[7] = (uint8_t)(function_code & 0xff);
    req[8] = (uint8_t)(start_address >> 8);
    req[9] = (uint8_t)(start_address & 0xff);
    req[10] = (uint8_t)(quantity >> 8);
    req[11] = (uint8_t)(quantity & 0xff);

    if (send(sock, req, sizeof(req), 0) != (ssize_t)sizeof(req)) {
        close(sock);
        return -1;
    }

    uint8_t mbap[7];
    if (read_exact(sock, mbap, 7) != 7) {
        close(sock);
        return -1;
    }

    uint16_t response_tid = ((uint16_t)mbap[0] << 8) | mbap[1];
    uint16_t protocol_id = ((uint16_t)mbap[2] << 8) | mbap[3];
    uint16_t length = ((uint16_t)mbap[4] << 8) | mbap[5];

    if (response_tid != tid || protocol_id != 0 || length < 2 || length > 260) {
        close(sock);
        return -1;
    }

    int pdu_len = (int)length - 1;
    uint8_t pdu[260];

    if (read_exact(sock, pdu, pdu_len) != pdu_len) {
        close(sock);
        return -1;
    }

    close(sock);

    if (pdu[0] == (uint8_t)(function_code | 0x80)) {
        uint8_t exception = pdu_len > 1 ? pdu[1] : 0xff;
        for (int i = 0; i < quantity; i++) {
            results[i].exception_code = exception;
        }
        return 0;
    }

    int expected_bytes = quantity * 2;
    if (pdu[0] != (uint8_t)function_code ||
        pdu_len != expected_bytes + 2 ||
        pdu[1] != expected_bytes) {
        return -1;
    }

    for (int i = 0; i < quantity; i++) {
        int offset = 2 + i * 2;
        results[i].value = ((uint16_t)pdu[offset] << 8) | pdu[offset + 1];
        results[i].ok = 1;
    }

    return quantity;
}

static char *read_text_file(const char *path) {
    FILE *f = fopen(path, "rb");
    if (!f) return NULL;

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    rewind(f);

    if (size <= 0) {
        fclose(f);
        return NULL;
    }

    char *buf = calloc((size_t)size + 1, 1);
    if (!buf) {
        fclose(f);
        return NULL;
    }

    fread(buf, 1, (size_t)size, f);
    fclose(f);
    return buf;
}

static char *find_object_end(char *start) {
    int depth = 0;
    int in_string = 0;
    int escape = 0;

    for (char *p = start; *p; p++) {
        if (escape) {
            escape = 0;
            continue;
        }
        if (*p == '\\') {
            escape = 1;
            continue;
        }
        if (*p == '"') {
            in_string = !in_string;
            continue;
        }
        if (!in_string) {
            if (*p == '{') depth++;
            if (*p == '}') {
                depth--;
                if (depth == 0) return p;
            }
        }
    }
    return NULL;
}

static int json_get_int(char *obj_start, char *obj_end, const char *key, int *out) {
    char pattern[64];
    snprintf(pattern, sizeof(pattern), "\"%s\"", key);

    char *p = obj_start;
    while ((p = strstr(p, pattern)) && p < obj_end) {
        p += strlen(pattern);
        while (p < obj_end && isspace((unsigned char)*p)) p++;
        if (p >= obj_end || *p != ':') continue;
        p++;
        while (p < obj_end && isspace((unsigned char)*p)) p++;
        *out = atoi(p);
        return 1;
    }
    return 0;
}

static int json_get_string(char *obj_start, char *obj_end, const char *key, char *out, size_t out_size) {
    char pattern[64];
    snprintf(pattern, sizeof(pattern), "\"%s\"", key);

    char *p = obj_start;
    while ((p = strstr(p, pattern)) && p < obj_end) {
        p += strlen(pattern);
        while (p < obj_end && isspace((unsigned char)*p)) p++;
        if (p >= obj_end || *p != ':') continue;
        p++;
        while (p < obj_end && isspace((unsigned char)*p)) p++;
        if (p >= obj_end || *p != '"') return 0;
        p++;

        size_t len = 0;
        int escape = 0;
        while (p < obj_end && *p) {
            if (escape) {
                if (len + 1 < out_size) out[len++] = *p;
                escape = 0;
            } else if (*p == '\\') {
                escape = 1;
            } else if (*p == '"') {
                break;
            } else {
                if (len + 1 < out_size) out[len++] = *p;
            }
            p++;
        }
        out[len] = '\0';
        return 1;
    }
    return 0;
}


static int parse_coordinates(const char *text, int *x, int *y) {
    if (!text || !x || !y) return 0;

    char *endptr;
    long parsed_x = strtol(text, &endptr, 10);
    if (endptr == text) return 0;

    while (isspace((unsigned char)*endptr)) endptr++;
    if (*endptr != ',') return 0;
    endptr++;

    while (isspace((unsigned char)*endptr)) endptr++;
    char *y_start = endptr;
    long parsed_y = strtol(y_start, &endptr, 10);
    if (endptr == y_start) return 0;

    while (isspace((unsigned char)*endptr)) endptr++;
    if (*endptr != '\0') return 0;

    if (parsed_x < 0 || parsed_y < 0 || parsed_x > 10000 || parsed_y > 10000) {
        return 0;
    }

    *x = (int)parsed_x;
    *y = (int)parsed_y;
    return 1;
}

static RegisterInfo *load_register_json(const char *path, int start, int end) {
    int count = end - start + 1;
    RegisterInfo *infos = calloc((size_t)count, sizeof(RegisterInfo));
    if (!infos) return NULL;

    if (!path || strlen(path) == 0) return infos;

    char *json = read_text_file(path);
    if (!json) return infos;

    char *p = json;
    while ((p = strchr(p, '{')) != NULL) {
        char *end_obj = find_object_end(p);
        if (!end_obj) break;

        int addr = -1;
        if (json_get_int(p, end_obj, "adresse", &addr)) {
            if (addr >= start && addr <= end) {
                RegisterInfo *ri = &infos[addr - start];
                ri->present = 1;
                ri->address = addr;
                ri->factor = 10;
                json_get_string(p, end_obj, "einheit", ri->unit, sizeof(ri->unit));
                json_get_string(p, end_obj, "bezeichnung", ri->label, sizeof(ri->label));
                json_get_string(p, end_obj, "beschreibung", ri->description, sizeof(ri->description));
                json_get_int(p, end_obj, "faktor", &ri->factor);
                json_get_string(p, end_obj, "datentyp", ri->datatype, sizeof(ri->datatype));

                char coordinates[64];
                if (json_get_string(p, end_obj, "koordinaten", coordinates, sizeof(coordinates)) &&
                    parse_coordinates(coordinates, &ri->coord_x, &ri->coord_y)) {
                    ri->has_coordinates = 1;
                }
            }
        }

        p = end_obj + 1;
    }

    free(json);
    return infos;
}

static void sleep_ms(int ms) {
    usleep((useconds_t)ms * 1000);
}

static int is_signed_power_value(const RegisterInfo *info) {
    if (!info || !info->present) return 0;
    return strcmp(info->unit, "W") == 0 || strcmp(info->unit, "var") == 0;
}

static double numeric_value(const ScanResult *res, const RegisterInfo *info) {
    int factor = 10;
    if (info && info->present && info->factor != 0) factor = info->factor;

    int raw_value;
    if (is_signed_power_value(info)) {
        raw_value = (int16_t)res->value;
    } else {
        raw_value = (int)res->value;
    }
    return (double)raw_value / (double)factor;
}

static void format_value(
    char *out,
    size_t out_size,
    const ScanResult *res,
    const RegisterInfo *info
) {
    if (!res->ok) {
        snprintf(out, out_size, "NN");
        return;
    }

    if (display_hex) {
        snprintf(out, out_size, "%04X", res->value);
    } else {
        int factor = 10;
        if (info && info->present && info->factor != 0) factor = info->factor;
        double value = numeric_value(res, info);

        if (factor == 1) snprintf(out, out_size, "%.0f", value);
        else snprintf(out, out_size, "%.6g", value);
    }
}

static int append_text(char **buf, size_t *capacity, size_t *length, const char *text) {
    size_t needed = strlen(text);
    if (*length + needed + 1 > *capacity) {
        size_t new_capacity = *capacity;
        while (*length + needed + 1 > new_capacity) new_capacity *= 2;
        char *new_buf = realloc(*buf, new_capacity);
        if (!new_buf) return 0;
        *buf = new_buf;
        *capacity = new_capacity;
    }
    memcpy(*buf + *length, text, needed + 1);
    *length += needed;
    return 1;
}

static int append_json_string(char **buf, size_t *capacity, size_t *length, const char *text) {
    if (!append_text(buf, capacity, length, "\"")) return 0;
    for (const unsigned char *p = (const unsigned char *)text; *p; p++) {
        char escaped[8];
        if (*p == '\"' || *p == '\\') snprintf(escaped, sizeof(escaped), "\\%c", *p);
        else if (*p == '\n') snprintf(escaped, sizeof(escaped), "\\n");
        else if (*p == '\r') snprintf(escaped, sizeof(escaped), "\\r");
        else if (*p == '\t') snprintf(escaped, sizeof(escaped), "\\t");
        else if (*p < 0x20) snprintf(escaped, sizeof(escaped), "\\u%04x", *p);
        else { escaped[0] = (char)*p; escaped[1] = '\0'; }
        if (!append_text(buf, capacity, length, escaped)) return 0;
    }
    return append_text(buf, capacity, length, "\"");
}

static char *build_mqtt_json(
    int cycle,
    int unit_id,
    int function_code,
    int start,
    int end,
    const ScanResult *results,
    const RegisterInfo *infos
) {
    size_t capacity = MQTT_PAYLOAD_INITIAL_SIZE;
    size_t length = 0;
    char *json = calloc(capacity, 1);
    if (!json) return NULL;

    char tmp[256];
    snprintf(tmp, sizeof(tmp),
             "{\"cycle\":%d,\"unit_id\":%d,\"function_code\":%d,\"values\":[",
             cycle, unit_id, function_code);
    if (!append_text(&json, &capacity, &length, tmp)) goto fail;

    int first = 1;
    int count = end - start + 1;
    for (int i = 0; i < count; i++) {
        if (!infos || !infos[i].present) continue;
        if (!first && !append_text(&json, &capacity, &length, ",")) goto fail;
        first = 0;

        snprintf(tmp, sizeof(tmp), "{\"address\":%d,\"name\":", start + i);
        if (!append_text(&json, &capacity, &length, tmp)) goto fail;
        if (!append_json_string(&json, &capacity, &length, infos[i].label)) goto fail;
        if (!append_text(&json, &capacity, &length, ",\"value\":")) goto fail;

        if (results[i].ok) {
            snprintf(tmp, sizeof(tmp), "%.10g", numeric_value(&results[i], &infos[i]));
            if (!append_text(&json, &capacity, &length, tmp)) goto fail;
        } else {
            if (!append_text(&json, &capacity, &length, "null")) goto fail;
        }

        if (!append_text(&json, &capacity, &length, ",\"unit\":")) goto fail;
        if (!append_json_string(&json, &capacity, &length, infos[i].unit)) goto fail;
        snprintf(tmp, sizeof(tmp), ",\"valid\":%s,\"raw_hex\":\"%04X\"}",
                 results[i].ok ? "true" : "false", results[i].value);
        if (!append_text(&json, &capacity, &length, tmp)) goto fail;
    }

    if (!append_text(&json, &capacity, &length, "]}")) goto fail;
    return json;

fail:
    free(json);
    return NULL;
}

static int mqtt_publish_cycle(
    struct mosquitto *mqtt,
    const char *topic,
    int cycle,
    int unit_id,
    int function_code,
    int start,
    int end,
    const ScanResult *results,
    const RegisterInfo *infos
) {
    if (!mqtt || !topic) return MOSQ_ERR_INVAL;
    char *payload = build_mqtt_json(cycle, unit_id, function_code, start, end, results, infos);
    if (!payload) return MOSQ_ERR_NOMEM;

    int rc = mosquitto_publish(mqtt, NULL, topic, (int)strlen(payload), payload, 0, false);
    if (rc == MOSQ_ERR_SUCCESS) rc = mosquitto_loop(mqtt, 100, 1);
    free(payload);
    return rc;
}

static void draw_matrix(
    const char *ip,
    int port,
    int unit_id,
    int function_code,
    int start,
    int end,
    int interval_ms,
    ScanResult *results,
    RegisterInfo *infos,
    int cycle,
    const char *jsonfile,
    const char *mqtt_topic,
    const char *mqtt_status
) {
    clear();

    int rows, cols;
    getmaxyx(stdscr, rows, cols);

    mvprintw(0, 0, "Modbus/TCP Scanner  IP=%s  Port=%d  Unit=%d  FC=%d  Range=%d-%d",
             ip, port, unit_id, function_code, start, end);

    mvprintw(1, 0, "Anzeige=%s  Zyklus=%d  Intervall=%s  JSON=%s",
             display_hex ? "HEX" : "DEZ",
             cycle,
             interval_ms > 0 ? "aktiv" : "einmalig",
             jsonfile ? jsonfile : "-");

    mvprintw(2, 0, "Tasten: q=Ende h=HEX d=DEZ t=Umschalten  MQTT=%s (%s)",
             mqtt_topic ? mqtt_topic : "aus", mqtt_status ? mqtt_status : "-");
    mvhline(3, 0, '-', cols);

    const int first_data_row = 4;
    const int cell_width = 30;
    int count = end - start + 1;

    for (int i = 0; i < count; i++) {
        if (!infos || !infos[i].present || !infos[i].has_coordinates) {
            continue;
        }

        int x = infos[i].coord_x;
        int y = infos[i].coord_y;

        /* Die Koordinate 0,0 unterdrueckt nur die Darstellung. */
        if (x == 0 && y == 0) {
            continue;
        }

        /* Alle anderen Koordinaten muessen 1-basiert sein. */
        if (x < 1 || y < 1) {
            continue;
        }

        int r = first_data_row + (y - 1);
        int c = (x - 1) * cell_width;

        /* Eintraege ausserhalb des sichtbaren Fensters nicht zeichnen. */
        if (r < first_data_row || r >= rows - 2 || c < 0 || c >= cols) {
            continue;
        }

        char value[16];
        format_value(value, sizeof(value), &results[i], infos ? &infos[i] : NULL);

        char label[LABEL_LEN];
        if (infos[i].label[0] != '\0') {
            snprintf(label, sizeof(label), "%s", infos[i].label);
        } else {
            snprintf(label, sizeof(label), "-");
        }
        label[10] = '\0';

        int available = cols - c;
        if (available > 0) {
            mvprintw(r, c, "%04d %-12s %s %s",
                     start + i, label, value, infos[i].unit);
        }
    }

    mvhline(rows - 2, 0, '-', cols);
    mvprintw(rows - 1, 0,
             "Raster: x=Spalte, y=Zeile, 1,1=erste Datenzelle, 0,0=ausgeblendet");

    refresh();
}

static void handle_key(int ch, int *running) {
    if (ch == 'q' || ch == 'Q') {
        *running = 0;
    } else if (ch == 'h' || ch == 'H') {
        display_hex = 1;
    } else if (ch == 'd' || ch == 'D') {
        display_hex = 0;
    } else if (ch == 't' || ch == 'T') {
        display_hex = !display_hex;
    }
}

int main(int argc, char **argv) {
    if (argc < 7) {
        fprintf(stderr,
            "Aufruf:\n"
            "  %s <ip> <port> <unitid> <functioncode> <startadresse> <endadresse>"
            " [interval_ms] [jsonfile] [mqtt_host] [mqtt_port] [mqtt_topic]"
            " [mqtt_user] [mqtt_password]\n\n"
            "Ohne MQTT:\n"
            "  %s 192.168.178.68 502 1 3 0 100 1000 em24_register.json\n\n"
            "Mit MQTT:\n"
            "  %s 192.168.178.68 502 1 3 0 100 1000 em24_register.json"
            " 192.168.178.10 1883 em24/state\n\n"
            "Mit MQTT-Anmeldung:\n"
            "  %s 192.168.178.68 502 1 3 0 100 1000 em24_register.json"
            " 192.168.178.10 1883 em24/state benutzer passwort\n",
            argv[0], argv[0], argv[0], argv[0]);
        return 1;
    }

    const char *ip = argv[1];
    int port = atoi(argv[2]);
    int unit_id = atoi(argv[3]);
    int function_code = atoi(argv[4]);
    int start = atoi(argv[5]);
    int end = atoi(argv[6]);
    int interval_ms = argc >= 8 ? atoi(argv[7]) : 0;
    const char *jsonfile = argc >= 9 ? argv[8] : NULL;
    const char *mqtt_host = argc >= 10 ? argv[9] : NULL;
    int mqtt_port = argc >= 11 ? atoi(argv[10]) : 1883;
    const char *mqtt_topic = argc >= 12 ? argv[11] : NULL;
    const char *mqtt_user = argc >= 13 ? argv[12] : NULL;
    const char *mqtt_password = argc >= 14 ? argv[13] : NULL;
    int mqtt_enabled = mqtt_host && mqtt_topic && mqtt_host[0] && mqtt_topic[0];

    if (function_code != 3 && function_code != 4) {
        fprintf(stderr, "Aktuell werden nur Function Code 3 und 4 unterstuetzt.\n");
        return 1;
    }
    if (start < 0 || end < start || end > 65535) {
        fprintf(stderr, "Ungueltiger Adressbereich.\n");
        return 1;
    }
    if (mqtt_enabled && (mqtt_port < 1 || mqtt_port > 65535)) {
        fprintf(stderr, "Ungueltiger MQTT-Port.\n");
        return 1;
    }

    int count = end - start + 1;
    ScanResult *results = calloc((size_t)count, sizeof(ScanResult));
    if (!results) {
        perror("calloc");
        return 1;
    }

    RegisterInfo *infos = load_register_json(jsonfile, start, end);
    if (!infos) {
        free(results);
        fprintf(stderr, "Konnte Registerinfos nicht anlegen.\n");
        return 1;
    }

    struct mosquitto *mqtt = NULL;
    char mqtt_status[96] = "aus";
    if (mqtt_enabled) {
        mosquitto_lib_init();
        char client_id[96];
        snprintf(client_id, sizeof(client_id), "modbusscan-%ld", (long)getpid());
        mqtt = mosquitto_new(client_id, true, NULL);
        if (!mqtt) {
            snprintf(mqtt_status, sizeof(mqtt_status), "Initialisierung fehlgeschlagen");
        } else {
            if (mqtt_user && mqtt_user[0]) {
                int auth_rc = mosquitto_username_pw_set(mqtt, mqtt_user, mqtt_password);
                if (auth_rc != MOSQ_ERR_SUCCESS) {
                    snprintf(mqtt_status, sizeof(mqtt_status), "Login-Konfiguration: %s",
                             mosquitto_strerror(auth_rc));
                }
            }
            int rc = mosquitto_connect(mqtt, mqtt_host, mqtt_port, 30);
            if (rc == MOSQ_ERR_SUCCESS) snprintf(mqtt_status, sizeof(mqtt_status), "verbunden");
            else snprintf(mqtt_status, sizeof(mqtt_status), "Verbindung: %s", mosquitto_strerror(rc));
        }
    }

    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    nodelay(stdscr, TRUE);
    curs_set(0);

    int cycle = 0;
    int running = 1;

    while (running) {
        cycle++;

        for (int i = 0; i < count; i += MODBUS_BLOCK_SIZE) {
            int ch = getch();
            handle_key(ch, &running);
            if (!running) break;

            int block_count = count - i;
            if (block_count > MODBUS_BLOCK_SIZE) block_count = MODBUS_BLOCK_SIZE;

            read_register_block(ip, port, unit_id, function_code,
                                start + i, block_count, &results[i]);

            draw_matrix(ip, port, unit_id, function_code, start, end,
                        interval_ms, results, infos, cycle, jsonfile,
                        mqtt_enabled ? mqtt_topic : NULL, mqtt_status);
        }

        if (running && mqtt_enabled && mqtt) {
            int rc = mqtt_publish_cycle(mqtt, mqtt_topic, cycle, unit_id,
                                        function_code, start, end, results, infos);
            if (rc == MOSQ_ERR_NO_CONN || rc == MOSQ_ERR_CONN_LOST) {
                rc = mosquitto_reconnect(mqtt);
                if (rc == MOSQ_ERR_SUCCESS) {
                    rc = mqtt_publish_cycle(mqtt, mqtt_topic, cycle, unit_id,
                                            function_code, start, end, results, infos);
                }
            }
            if (rc == MOSQ_ERR_SUCCESS) snprintf(mqtt_status, sizeof(mqtt_status), "Zyklus %d gesendet", cycle);
            else snprintf(mqtt_status, sizeof(mqtt_status), "Fehler: %s", mosquitto_strerror(rc));
        }

        draw_matrix(ip, port, unit_id, function_code, start, end,
                    interval_ms, results, infos, cycle, jsonfile,
                    mqtt_enabled ? mqtt_topic : NULL, mqtt_status);

        if (interval_ms <= 0) {
            while (running) {
                int ch = getch();
                handle_key(ch, &running);
                sleep_ms(50);
            }
        } else {
            int waited = 0;
            while (running && waited < interval_ms) {
                int ch = getch();
                handle_key(ch, &running);
                if (mqtt) mosquitto_loop(mqtt, 0, 1);
                sleep_ms(50);
                waited += 50;
            }
        }
    }

    endwin();
    if (mqtt) {
        mosquitto_disconnect(mqtt);
        mosquitto_destroy(mqtt);
    }
    if (mqtt_enabled) mosquitto_lib_cleanup();
    free(infos);
    free(results);
    return 0;
}

In diesem Programm ist kein MODBUS-Register hardcoded, sondern wird aus einer json Datei eingelesen, die man schnell und einfach pflegen kann ud sich sogar durch ChatGPT erstellen oder ergaenzen lassen kann.