diff --git a/modbus_params/Makefile b/modbus_params/Makefile index bbd8f39..448c3d8 100644 --- a/modbus_params/Makefile +++ b/modbus_params/Makefile @@ -1,5 +1,5 @@ # run `make DEF=...` to add extra defines -PROGRAM := modbus_par +PROGRAM := modbus_params LDFLAGS := -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,--discard-all LDFLAGS += -lusefull_macros -lmodbus SRCS := $(wildcard *.c) diff --git a/modbus_params/Readme b/modbus_params/Readme deleted file mode 100644 index 54076c2..0000000 --- a/modbus_params/Readme +++ /dev/null @@ -1,21 +0,0 @@ - -modbus_par: reading/writing/dumping modbus registers using `dictionary` - - - -D, --dictionary=arg file with dictionary (format: code register value writeable) - -N, --node=arg node "IP", or path (could be "\0path" for anonymous UNIX-socket) - -O, --outdic=arg output dictionary for full device dump by input dictionary registers - -R, --readc registers (by keycodes, checked by dictionary) to read; multiply parameter - -U, --unixsock UNIX socket instead of INET - -W, --writec write new value to register by keycode (format: keycode=val); multiply parameter - -a, --alias=arg file with aliases in format 'name : command to run' - -b, --baudrate=arg modbus baudrate (default: 9600) - -d, --device=arg modbus device (default: /dev/ttyUSB0) - -h, --help show this help - -k, --dumpkey dump entry with this keycode; multiply parameter - -o, --outfile=arg file with parameter's dump - -r, --readr registers (by address) to read; multiply parameter - -s, --slave=arg slave ID (default: 1) - -t, --dumptime=arg dumping time interval (seconds, default: 0.1) - -v, --verbose verbose level (each -v adds 1) - -w, --writer write new value to register (format: reg=val); multiply parameter diff --git a/modbus_params/Readme.md b/modbus_params/Readme.md new file mode 100644 index 0000000..558c3df --- /dev/null +++ b/modbus_params/Readme.md @@ -0,0 +1,275 @@ +# modbus_params + +**modbus_params** is a command-line tool and network server for interacting with Modbus RTU +devices. It uses a human‑readable **dictionary** that maps register addresses to mnemonic codes, +supports periodic dumping of selected parameters to a TSV file, and provides a TCP/UNIX socket +interface for remote access. + +The project is hosted at: +[https://github.com/eddyem/eddys_snippets/tree/master/modbus_params](https://github.com/eddyem/eddys_snippets/tree/master/modbus_params) + +## Features + +- Read and write Modbus holding registers by **address** or **mnemonic code**. +- Define a dictionary (`code → register`) with optional comments and read‑only flags. +- Dump selected registers periodically to a TSV file. +- Start a **TCP or UNIX socket server** that accepts commands to: + - Read/write registers (by code or address). + - List dictionary entries or aliases. + - Start/stop and rename the dump file. +- Define **aliases** – macros that expand to other commands or expressions. +- Verbose logging with configurable level. + +## Dependencies + +- [libmodbus](https://libmodbus.org/) (Modbus RTU backend) +- [usefull_macros](https://github.com/eddyem/snippets_library) (argument parsing, socket helpers, + logging macros) +- POSIX threads (pthread) +- Standard C library + +## Building + +Clone the repository together with the required submodule: + +```bash +git clone --depth=1 https://github.com/eddyem/eddys_snippets.git +cd eddys_snippets/modbus_params +``` + +Then compile with `make`: + +```bash +make +``` + +The resulting executable is named `modbus_params`. You can copy this file wherever you want, e.g. + +```bash +su -c "cp modbus_params /usr/local/bin +``` + +## Command‑line usage + +``` +modbus_params [options] +``` + +### General options + +| Option | Argument | Description | +|--------|-------------------|-------------| +| `-h`, `--help` | – | Show help and exit | +| `-v`, `--verbose` | – | Increase verbosity (can be used multiple times) | +| `-D`, `--dictionary` | *file* | Dictionary file (format: `code register value readonly`). *Required for most operations.* | +| `-a`, `--alias` | *file* | Aliases file (format: `name : command`). Optional. | +| `-s`, `--slave` | *ID* | Modbus slave ID (default: 1) | +| `-d`, `--device` | *path* | Serial device (default: `/dev/ttyUSB0`) | +| `-b`, `--baudrate` | *rate* | Baud rate (default: 9600) | + +### Read/write operations + +| Option | Description | +|--------|-------------| +| `-r`, `--readr` *addr* | Read register by address (multiply: `-r 100 -r 200`) | +| `-R`, `--readc` *code* | Read register by dictionary code (multiply) | +| `-w`, `--writer` *addr=val* | Write value to register by address (multiply) | +| `-W`, `--writec` *code=val* | Write value to register by dictionary code (multiply) | +| `-O`, `--outdic` *file* | Read **all** registers listed in the dictionary and save them (with current values) into a new dictionary file | + +### Dumping (periodic TSV output) + +| Option | Description | +|--------|-------------| +| `-k`, `--dumpkey` *code* | Add a dictionary key to the dump list (multiply) | +| `-o`, `--outfile` *file* | TSV file for the dump | +| `-t`, `--dumptime` *seconds* | Dump interval (default: 0.1 s) | + +### Server mode + +| Option | Description | +|--------|-------------| +| `-N`, `--node` *spec* | Start a server. For TCP: IP address or hostname with port after ':' (e.g. `:1212` or `localhost:1212`). For UNIX socket: a path (e.g. `/tmp/mb_sock`). | +| `-U`, `--unixsock` | Use UNIX domain socket instead of TCP (requires `-N` with a path) | + +If `-N` is given, the program runs as a server **after** executing any immediate read/write/dump +operations. The server stays alive until interrupted (Ctrl+C). + +## File formats + +### Dictionary file + +Each line defines one register. Empty lines and text after `#` are ignored. +Format: + +``` + +``` + +- `code` – mnemonic string (no spaces). +- `register` – 16‑bit Modbus address (decimal). +- `value` – initial/default value (16‑bit, not used by the tool except for documentation). +- `readonly` – `0` (writable) or `1` (read‑only). + +Example (from `dictionary.dic`): + +``` +F00.00 61440 1 0 # Motor management (V/F) +F00.01 61441 0 0 # Selecting the START command task +... +A02.01 41473 0 1 # Output frequency (read-only) +``` + +### Aliases file + +Aliases provide a way to define shortcuts or composite commands. +Format: + +``` +name : expression # optional comment +``` + +The `expression` is a string that will be interpreted as a command (register read/write, another +alias, or a built‑in server command). Aliases are resolved recursively. + +Example: + +``` +stop : F00.00=0 # stop motor +start : F00.00=1 # start motor +freq : F00.11 # read current frequency +``` + +When the server receives `stop`, it writes `0` to register mapped to `F00.00`. + +### Dump file (TSV) + +When dumping is active, the tool writes a tab-separated file with a header line: + +``` +# time,s ... +``` + +Each subsequent line contains a timestamp (seconds since dump start) followed by the current value +of each registered key. Unreadable registers are shown as `----`. + +Example: +``` +# time,s F00.00 F00.11 + 0.000 1 5000 + 0.102 1 5000 +``` + +## Server protocol + +The server listens for plain text commands, terminated by newline (`\n`). +Each command is a key‑value pair: `key[=value]`. + +- **Getter** – just the key → returns current value. +- **Setter** – `key=value` → writes the value (if the register is writable). + +### Built‑in keys (handlers) + +| Key | Description | +|-----|-------------| +| `list` | As a getter: prints the whole dictionary (one line per entry). As a setter (e.g. `list=F00.01`): prints details of the given code or register address. | +| `alias` | Getter: prints all aliases. Setter (e.g. `alias=myalias`): prints that specific alias. | +| `newdump` | Getter: returns current dump file name. Setter (e.g. `newdump=/path/file.dump`): closes current dump file (if any), opens a new one and starts dumping. | +| `clodump` | Stops the dump thread and closes the dump file. No value expected. | + +### Default handler (register access) + +If the key is **not** a built‑in handler, the program tries to interpret it as: + +- A **register address** (decimal integer) → find the corresponding dictionary entry. +- A **dictionary code** → find the entry. +- An **alias** → recursively expand and execute. + +Then: + +- **Getter** (only key) → reads the register and replies `key=value`. +- **Setter** (`key=value`) → writes the value to the register (if not read‑only) and replies `OK`. + +### Examples (using `netcat`, `telnet` or `socat`) + +**TCP server** (assuming `-N :5020`): + +``` +$ nc localhost 5020 +F00.00 +F00.00=1 +F00.00=0 +OK +list=F00.01 +F00.01 61441 0 0 # Selecting the START command task +newdump=/tmp/motor.dump +OK +clodump +OK +``` + +**UNIX socket**: + +``` +$ socat - UNIX-CONNECT:/tmp/mb_sock +F00.11 +F00.11=5000 +``` + +## Examples + +### 1. Simple read by address + +```bash +modbus_params -D dictionary.txt -s 1 -d /dev/ttyUSB0 -b 9600 -r 61440 +``` + +### 2. Write a value using a code + +```bash +modbus_params -D dictionary.txt -W "F00.00=1" +``` + +### 3. Dump two registers every 0.5 seconds to `data.tsv` + +```bash +modbus_params -D dictionary.txt -k F00.00 -k F00.11 -o data.tsv -t 0.5 +``` + +(No server started – the program will dump until interrupted.) + +### 4. Start a local server with dumping already active + +```bash +modbus_params -D dictionary.txt -k F00.00 -o /tmp/dump.csv -t 1 -N localhost:5020 +``` + +Now you can connect to port 5020 and use `newdump` to change the output file, or `clodump` to stop. + +### 5. Read all dictionary registers and save current values + +```bash +modbus_params -D dictionary.dic -O current_state.dic +``` + +## Signals + +- `SIGINT` (Ctrl+C), `SIGTERM`, `SIGQUIT` – gracefully close Modbus, dump files, and exit. +- `SIGHUP`, `SIGTSTP` – ignored (no action). + +## Notes + +- The dictionary is **shared** between the main program and the server thread. Do not modify the + dictionary file while the program is running. +- The dump thread uses a separate timer; if a read operation hangs, the dump may stall. +- When using UNIX sockets, the path can be prefixed with `\0` or `@` (for abstract sockets). + The program accepts plain paths for filesystem sockets. +- The `usefull_macros` library provides the `sl_dtime()` high‑resolution timer and thread‑safe +logging macros. + +## License + +Copyright 2022 Edward V. Emelianov . +GNU General Public License v3 or later. + +Full text of the GPLv3 is available at . diff --git a/modbus_params/dictionary.c b/modbus_params/dictionary.c index 464d704..77af3e5 100644 --- a/modbus_params/dictionary.c +++ b/modbus_params/dictionary.c @@ -45,7 +45,7 @@ static char *dumpname = NULL; // it's name static dicentry_t *dumppars = NULL; // array with parameters to dump static int dumpsize = 0; // it's size static double dumpTime = 0.1; // period to dump -static atomic_int stopdump = FALSE, isstopped = TRUE; // flags +static atomic_int stopdump = 0, isstopped = 1; // flags /* aliases */ // list of aliases sorted by name @@ -121,12 +121,13 @@ int opendict(const char *dic){ // DBG("Got line: '%s %" PRIu16 " %" PRIu16 " %" PRIu8, curentry.code, curentry.reg, curentry.value, curentry.readonly); if(++dictsize >= dicsz){ dicsz += 50; - dictionary = realloc(dictionary, sizeof(dicentry_t) * dicsz); - if(!dictionary){ + dicentry_t *newdic = realloc(dictionary, sizeof(dicentry_t) * dicsz); + if(!newdic){ WARN("Can't allocate memory for dictionary"); retcode = FALSE; goto ret; } + dictionary = newdic; } dicentry_t *entry = &dictionary[dictsize-1]; entry->code = strdup(curentry.code); @@ -228,13 +229,20 @@ int setdumppars(char **pars){ if(cursz <= N){ cursz += 50; DBG("realloc list to %d", cursz); - dumppars = realloc(dumppars, sizeof(dicentry_t) * (cursz)); - DBG("zero mem"); - bzero(&dumppars[N], sizeof(dicentry_t)*(cursz-N)); + dicentry_t *newpars = realloc(dumppars, sizeof(dicentry_t) * (cursz)); + if(!newpars){ + WARN("realloc()"); cursz -= 50; + }else{ + dumppars = newpars; + DBG("zero mem"); + bzero(&dumppars[N], sizeof(dicentry_t)*(cursz-N)); + } } FREE(dumppars[N].code); + FREE(dumppars[N].help); dumppars[N] = *e; dumppars[N].code = strdup(e->code); + if(e->help) dumppars[N].help = strdup(e->help); DBG("Add %s", e->code); ++N; ++p; } @@ -269,10 +277,11 @@ int opendumpfile(const char *name){ char *getdumpname(){ return dumpname;} void closedumpfile(){ - if(dumpfile && !isstopped){ - if(!isstopped){ - stopdump = TRUE; - while(!isstopped); + int stpd = atomic_load(&isstopped); + if(dumpfile && !stpd){ + if(!stpd){ + atomic_store(&stopdump, 1); + while(!atomic_load(&isstopped)); } fclose(dumpfile); FREE(dumpname); @@ -280,12 +289,12 @@ void closedumpfile(){ } static void *dumpthread(void *p){ - isstopped = FALSE; - stopdump = FALSE; + atomic_store(&isstopped, 0); + atomic_store(&stopdump, 0); double dT = *(double*)p; DBG("Dump thread started. Period: %gs", dT); double startT = sl_dtime(); - while(!stopdump){ + while(!atomic_load(&isstopped)){ double t0 = sl_dtime(); fprintf(dumpfile, "%10.3f ", t0 - startT); for(int i = 0; i < dumpsize; ++i){ @@ -295,7 +304,7 @@ static void *dumpthread(void *p){ fprintf(dumpfile, "\n"); while(sl_dtime() - t0 < dT) usleep(100); } - isstopped = TRUE; + atomic_store(&isstopped, 1); return NULL; } @@ -431,12 +440,13 @@ int openaliases(const char *filename){ } if(++aliasessize >= asz){ asz += 50; - aliases = realloc(aliases, sizeof(alias_t) * asz); - if(!aliases){ + alias_t *newaliases = realloc(aliases, sizeof(alias_t) * asz); + if(!newaliases){ WARNX("Can't allocate memory for aliases list"); retcode = FALSE; goto ret; } + aliases = newaliases; } alias_t *cur = &aliases[aliasessize - 1]; cur->name = strdup(name); diff --git a/modbus_params/modbus_param.files b/modbus_params/modbus_param.files index b011bac..1d20b0e 100644 --- a/modbus_params/modbus_param.files +++ b/modbus_params/modbus_param.files @@ -1,3 +1,4 @@ +Readme.md dictionary.c dictionary.h main.c