diff --git a/Daemons/weather_logger/Readme.md b/Daemons/weather_logger/Readme.md new file mode 100644 index 0000000..62ff138 --- /dev/null +++ b/Daemons/weather_logger/Readme.md @@ -0,0 +1,127 @@ +# meteologger + +**meteologger** is a client that collects meteorological data from the +[`weatherdaemon_multimeteo`](https://github.com/eddyem/small_tel/tree/master/Daemons/weatherdaemon_multimeteo) +daemon. It connects to the daemon over a TCP or UNIX socket, periodically requests data from all +available weather stations, and saves the received values into separate files (one per station). + +The project is written in C and uses the +[`usefull_macros`](https://github.com/eddyem/snippets_library) library for socket handling, +logging, argument parsing, and helper macros. + +## Repository + +Source code is located in the [small_tel](https://github.com/eddyem/small_tel) repository under +`Daemons/weather_logger`. + +### Dependencies + +- Linux (uses Linux‑specific calls: `prctl`, `fork`, `waitpid`, signals) +- CMake ≥ 4.0 +- C compiler (GCC, Clang) +- `usefull_macros` library ≥ 0.3.5 (must be installed and discoverable via `pkg-config`) + +### Building + +```bash +git clone --depth=1 https://github.com/eddyem/small_tel.git +cd small_tel/Daemons/weather_logger +mkdir build && cd build +cmake .. # or -DDEBUG=on for debug build +make +``` + +The executable `meteologger` will be placed in `build/` (or `bin/` after installation). + +### Installation + +```bash +su -c "make install" +``` + +By default the program is installed into `bin` (usually `/usr/local/bin`). + +## Usage + +``` +meteologger -n -o [options] +``` + +### Required arguments + +| Option | Long form | Description | +|--------|----------------|-------------| +| `-n` | `--node` | Node to connect to: `:` (e.g., `127.0.0.1:5555`) or a UNIX socket path when `-u` is given. | +| `-o` | `--output` | Directory where database files will be stored (must exist and be writable). | + +### Optional arguments + +| Option | Long form | Default | Description | +|--------|----------------|-----------------------------|-------------| +| `-h` | `--help` | - | Show help and exit. | +| `-u` | `--isunix` | - | Use a UNIX socket instead of TCP. | +| `-l` | `--logfile` | (none) | File to write logs | +| `-p` | `--pidfile` | `/tmp/meteologger.pid` | File where the process PID is written. | +| `-v` | `--verbose` | 0 | Increase logging verbosity (each `-v` raises the level). | +| `-i` | `--interval` | `0.5` | Request interval in seconds. Allowed range: `[0.2, 900]`. | +| `-t` | `--timeout` | `1.0` | Network timeout for server responses (seconds). Allowed range: `[0.1, 30]`. | + +### Examples + +1. Connect to a local daemon on port 5555 and store data in `/var/log/weather`: + ```bash + meteologger -n 127.0.0.1:5555 -o /var/log/weather + ``` + +2. Use a UNIX socket `/tmp/weather.sock`, write PID to `/run/meteologger.pid`: + ```bash + meteologger -n /tmp/weather.sock -o /data/weather -u -p /run/meteologger.pid + ``` + +3. Enable verbose logging to a file: + ```bash + meteologger -n localhost:5555 -o /srv/weather -vv -l /var/log/meteologger.log + ``` + +## Output file format + +Files are stored in the directory given by `-o` and are named `weatherXX.log`, where `XX` is a +two‑digit station number (from `00` to `99`). If a file already exists for a station (matching the +station name in the header comment), new data is appended. + +File structure: + +``` +# +# Station #, format: KEYWORD[level],... +# TIMESTAMP, [], [], ... +, , , ... +``` + +Example: + +``` +# WXA100-06 ultrasonic meteostation @ D:/dev/pl2303_0 +# Station #0, format: KEYWORD[level],... +# TIMESTAMP, WIND[1], WINDDIR[2], HUMIDITY[1], EXTTEMP[1], PRESSURE[1], PRECIP[3], PRECIPLV[3], PRECRATE[3] +1779369905, 0.50, 120.70, 76.90, 8.40, 590.00, 1, 374.70, 0.00 +1779369907, 0.60, 128.20, 76.90, 8.40, 590.00, 1, 374.70, 0.00 +1779369909, 0.70, 130.30, 76.90, 8.40, 590.00, 1, 374.70, 0.00 +... +``` + +- First line: comment with station name. +- Second line: station number and format description. +- Third line: column headers: `TIMESTAMP` followed by keys with their levels. +- Following lines: data – Unix timestamp (integer) and values (floating‑point, integer or string). + +----- + +## License + +Copyright © 2026 Edward V. Emelianov. +**GNU General Public License v3.0** or later. + +See the `LICENSE` file in repository's root directory or [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/) +for details. + diff --git a/Daemons/weather_logger/main.c b/Daemons/weather_logger/main.c index 42abc4e..b19aad3 100644 --- a/Daemons/weather_logger/main.c +++ b/Daemons/weather_logger/main.c @@ -79,6 +79,15 @@ int main(int argc, char **argv){ signal(SIGUSR1, signals); // reload DB #ifndef EBUG if(sl_daemonize()) ERRX("Can't daemonize"); +#endif + if(G->logfile){ + sl_loglevel_e lvl = LOGLEVEL_ERR + G->verb; + if(lvl >= LOGLEVEL_AMOUNT) lvl = LOGLEVEL_AMOUNT - 1; + DBG("Loglevel: %d", lvl); + OPENLOG(G->logfile, lvl, 1); + } + LOGMSG("Started"); +#ifndef EBUG catchsig = INT_MAX; // now `signals` won't run exit() while(catchsig == INT_MAX){ // guard for dead processes childpid = fork(); @@ -114,8 +123,10 @@ int main(int argc, char **argv){ }else{ pid_t self = getpid(); prepare_and_run(G); - if(catchsig == INT_MAX) LOGERR("Child process %d died", self); - else LOGERR("Child process %d exits with status %d", self, catchsig); + if(catchsig == INT_MAX){ + LOGERR("Child process %d died", self); + catchsig = 0; // like normal exit + }else LOGERR("Child process %d exits with status %d", self, catchsig); ; // cleanup child here #ifdef EBUG if(G->pidfile) unlink(G->pidfile); // unlink PID-file in debug-mode diff --git a/Daemons/weather_logger/meteologger.files b/Daemons/weather_logger/meteologger.files index 441f13d..b25df0c 100644 --- a/Daemons/weather_logger/meteologger.files +++ b/Daemons/weather_logger/meteologger.files @@ -1,4 +1,5 @@ CMakeLists.txt +Readme.md main.c parseargs.c parseargs.h diff --git a/Daemons/weather_logger/parseargs.c b/Daemons/weather_logger/parseargs.c index b75aeac..ea08156 100644 --- a/Daemons/weather_logger/parseargs.c +++ b/Daemons/weather_logger/parseargs.c @@ -61,7 +61,7 @@ glob_pars *parseargs(int *argc, char ***argv){ // remove trailing '/' int eol = strlen(G.bddir) - 1; DBG("eol=%d", eol); - while(eol > 0){ + while(eol > 0){ // don't remove leading slash in case of "/" DBG("before: %s", G.bddir); if(G.bddir[eol] == '/') G.bddir[eol] = 0; else break; @@ -73,10 +73,7 @@ glob_pars *parseargs(int *argc, char ***argv){ if(G.net_timeout < MIN_NET_TMOUT || G.net_timeout > MAX_NET_TMOUT) ERRX("Wrong network timeout %g, should be in [%g, %g]", G.net_timeout, MIN_NET_TMOUT, MAX_NET_TMOUT); if(G.logfile){ - sl_loglevel_e lvl = LOGLEVEL_ERR + G.verb; - if(lvl >= LOGLEVEL_AMOUNT) lvl = LOGLEVEL_AMOUNT - 1; - DBG("Loglevel: %d", lvl); - if(!OPENLOG(G.logfile, lvl, 1)) ERRX("Can't open log file %s", G.logfile); + if(*G.logfile != '/') ERRX("Logging file path should be absolute!"); } return &G; } diff --git a/Daemons/weather_logger/server.c b/Daemons/weather_logger/server.c index 423baae..6698828 100644 --- a/Daemons/weather_logger/server.c +++ b/Daemons/weather_logger/server.c @@ -128,7 +128,15 @@ void set_nettimeout(double dt){ */ static bool write2fd(senslog_t *sensor, const char *str, size_t len){ if(!sensor || !str || len < 1) return false; - if(write(sensor->fd, str, len) != (ssize_t)len){ + const char *ptr = str; + size_t written = 0; + while(len){ + ssize_t l = write(sensor->fd, ptr, len); + if(l < 0) break; + len -= l; + ptr += l; + } + if(written != len){ LOGERR("Can't write '%s' to file %s: %s", str, sensor->path, strerror(errno)); WARNX("Can't write data to file"); close(sensor->fd); @@ -405,8 +413,10 @@ static bool analyse_list(sl_sock_t *sock){ if(Nsensors <= idx){ int oldN = Nsensors; Nsensors = idx + 1; + senslog_t *old_sensors = sensors; sensors = realloc(sensors, Nsensors * sizeof(senslog_t)); if(!sensors){ + sensors = old_sensors; // restore old pointer instead of NULL sensors_delete(); LOGERR("analyse_list(): error in realloc()"); WARNX("analyse_list(): error in realloc()"); @@ -545,8 +555,11 @@ static bool poll_server_answers(sl_sock_t *sock){ // now throw data to disk DBG("throw '%s' with length %d to disk", sensor->buf, sensor->buflen); allOK = write2fd(sensor, sensor->buf, sensor->buflen); - write2fd(sensor, "\n", 1); - sensor->lasttimestamp = ts; + if(allOK){ + write2fd(sensor, "\n", 1); + fsync(sensor->fd); // sync last full record + sensor->lasttimestamp = ts; + } DBG("Data for %d is %swritten to disk", sensor->idx, allOK ? "" : "not "); } } @@ -596,8 +609,22 @@ static bool poll_server_answers(sl_sock_t *sock){ } } } - sensor->buflen += snprintf(sensor->buf + sensor->buflen, BUFSIZ-sensor->buflen, "%s%s", - value, sensor->nvalues-1 != idx ? ", " : ""); + int avail = BUFSIZ - sensor->buflen; + if(avail <= 0){ + LOGWARN("Sensor %d: writing buffer overfull", sensor->idx); + WARNX("Overfull; clear all"); + sensor->lastvalidx = -1; + sensor->buflen = 0; + return false; + } + int n = snprintf(sensor->buf + sensor->buflen, avail, "%s%s", + value, sensor->nvalues-1 != idx ? ", " : ""); + if(n >= avail){ + // Data would be truncated  handle error or discard + n = avail - 1; + LOGWARN("Sensor %d: writing buffer truncated", sensor->idx); + } + sensor->buflen += n; sensor->lastvalidx = idx; DBG("Now buf[%d]=%s", sensor->idx, sensor->buf); return true;