mirror of
https://github.com/eddyem/eddys_snippets.git
synced 2026-06-21 10:56:20 +03:00
add documentation, make some fixes
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# run `make DEF=...` to add extra defines
|
# 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 := -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,--discard-all
|
||||||
LDFLAGS += -lusefull_macros -lmodbus
|
LDFLAGS += -lusefull_macros -lmodbus
|
||||||
SRCS := $(wildcard *.c)
|
SRCS := $(wildcard *.c)
|
||||||
|
|||||||
@@ -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
|
|
||||||
275
modbus_params/Readme.md
Normal file
275
modbus_params/Readme.md
Normal file
@@ -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> <register> <value> <readonly>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `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 <code1> <code2> ...
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <edward.emelianoff@gmail.com>.
|
||||||
|
GNU General Public License v3 or later.
|
||||||
|
|
||||||
|
Full text of the GPLv3 is available at <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||||
@@ -45,7 +45,7 @@ static char *dumpname = NULL; // it's name
|
|||||||
static dicentry_t *dumppars = NULL; // array with parameters to dump
|
static dicentry_t *dumppars = NULL; // array with parameters to dump
|
||||||
static int dumpsize = 0; // it's size
|
static int dumpsize = 0; // it's size
|
||||||
static double dumpTime = 0.1; // period to dump
|
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 */
|
/* aliases */
|
||||||
// list of aliases sorted by name
|
// 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);
|
// DBG("Got line: '%s %" PRIu16 " %" PRIu16 " %" PRIu8, curentry.code, curentry.reg, curentry.value, curentry.readonly);
|
||||||
if(++dictsize >= dicsz){
|
if(++dictsize >= dicsz){
|
||||||
dicsz += 50;
|
dicsz += 50;
|
||||||
dictionary = realloc(dictionary, sizeof(dicentry_t) * dicsz);
|
dicentry_t *newdic = realloc(dictionary, sizeof(dicentry_t) * dicsz);
|
||||||
if(!dictionary){
|
if(!newdic){
|
||||||
WARN("Can't allocate memory for dictionary");
|
WARN("Can't allocate memory for dictionary");
|
||||||
retcode = FALSE;
|
retcode = FALSE;
|
||||||
goto ret;
|
goto ret;
|
||||||
}
|
}
|
||||||
|
dictionary = newdic;
|
||||||
}
|
}
|
||||||
dicentry_t *entry = &dictionary[dictsize-1];
|
dicentry_t *entry = &dictionary[dictsize-1];
|
||||||
entry->code = strdup(curentry.code);
|
entry->code = strdup(curentry.code);
|
||||||
@@ -228,13 +229,20 @@ int setdumppars(char **pars){
|
|||||||
if(cursz <= N){
|
if(cursz <= N){
|
||||||
cursz += 50;
|
cursz += 50;
|
||||||
DBG("realloc list to %d", cursz);
|
DBG("realloc list to %d", cursz);
|
||||||
dumppars = realloc(dumppars, sizeof(dicentry_t) * (cursz));
|
dicentry_t *newpars = realloc(dumppars, sizeof(dicentry_t) * (cursz));
|
||||||
DBG("zero mem");
|
if(!newpars){
|
||||||
bzero(&dumppars[N], sizeof(dicentry_t)*(cursz-N));
|
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].code);
|
||||||
|
FREE(dumppars[N].help);
|
||||||
dumppars[N] = *e;
|
dumppars[N] = *e;
|
||||||
dumppars[N].code = strdup(e->code);
|
dumppars[N].code = strdup(e->code);
|
||||||
|
if(e->help) dumppars[N].help = strdup(e->help);
|
||||||
DBG("Add %s", e->code);
|
DBG("Add %s", e->code);
|
||||||
++N; ++p;
|
++N; ++p;
|
||||||
}
|
}
|
||||||
@@ -269,10 +277,11 @@ int opendumpfile(const char *name){
|
|||||||
char *getdumpname(){ return dumpname;}
|
char *getdumpname(){ return dumpname;}
|
||||||
|
|
||||||
void closedumpfile(){
|
void closedumpfile(){
|
||||||
if(dumpfile && !isstopped){
|
int stpd = atomic_load(&isstopped);
|
||||||
if(!isstopped){
|
if(dumpfile && !stpd){
|
||||||
stopdump = TRUE;
|
if(!stpd){
|
||||||
while(!isstopped);
|
atomic_store(&stopdump, 1);
|
||||||
|
while(!atomic_load(&isstopped));
|
||||||
}
|
}
|
||||||
fclose(dumpfile);
|
fclose(dumpfile);
|
||||||
FREE(dumpname);
|
FREE(dumpname);
|
||||||
@@ -280,12 +289,12 @@ void closedumpfile(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void *dumpthread(void *p){
|
static void *dumpthread(void *p){
|
||||||
isstopped = FALSE;
|
atomic_store(&isstopped, 0);
|
||||||
stopdump = FALSE;
|
atomic_store(&stopdump, 0);
|
||||||
double dT = *(double*)p;
|
double dT = *(double*)p;
|
||||||
DBG("Dump thread started. Period: %gs", dT);
|
DBG("Dump thread started. Period: %gs", dT);
|
||||||
double startT = sl_dtime();
|
double startT = sl_dtime();
|
||||||
while(!stopdump){
|
while(!atomic_load(&isstopped)){
|
||||||
double t0 = sl_dtime();
|
double t0 = sl_dtime();
|
||||||
fprintf(dumpfile, "%10.3f ", t0 - startT);
|
fprintf(dumpfile, "%10.3f ", t0 - startT);
|
||||||
for(int i = 0; i < dumpsize; ++i){
|
for(int i = 0; i < dumpsize; ++i){
|
||||||
@@ -295,7 +304,7 @@ static void *dumpthread(void *p){
|
|||||||
fprintf(dumpfile, "\n");
|
fprintf(dumpfile, "\n");
|
||||||
while(sl_dtime() - t0 < dT) usleep(100);
|
while(sl_dtime() - t0 < dT) usleep(100);
|
||||||
}
|
}
|
||||||
isstopped = TRUE;
|
atomic_store(&isstopped, 1);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,12 +440,13 @@ int openaliases(const char *filename){
|
|||||||
}
|
}
|
||||||
if(++aliasessize >= asz){
|
if(++aliasessize >= asz){
|
||||||
asz += 50;
|
asz += 50;
|
||||||
aliases = realloc(aliases, sizeof(alias_t) * asz);
|
alias_t *newaliases = realloc(aliases, sizeof(alias_t) * asz);
|
||||||
if(!aliases){
|
if(!newaliases){
|
||||||
WARNX("Can't allocate memory for aliases list");
|
WARNX("Can't allocate memory for aliases list");
|
||||||
retcode = FALSE;
|
retcode = FALSE;
|
||||||
goto ret;
|
goto ret;
|
||||||
}
|
}
|
||||||
|
aliases = newaliases;
|
||||||
}
|
}
|
||||||
alias_t *cur = &aliases[aliasessize - 1];
|
alias_t *cur = &aliases[aliasessize - 1];
|
||||||
cur->name = strdup(name);
|
cur->name = strdup(name);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
Readme.md
|
||||||
dictionary.c
|
dictionary.c
|
||||||
dictionary.h
|
dictionary.h
|
||||||
main.c
|
main.c
|
||||||
|
|||||||
Reference in New Issue
Block a user