From 46de782019e427489562df9d6ca4424456ac7559 Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Thu, 7 May 2026 10:56:21 +0300 Subject: [PATCH] Add readme, make some fixes --- Daemons/weatherdaemon_multimeteo/Readme.md | 334 +++++++++++++++++- .../weatherdaemon_multimeteo/example.config | 4 +- Daemons/weatherdaemon_multimeteo/fd.c | 7 +- .../weatherdaemon_multimeteo/mainweather.c | 2 + .../weatherdaemon_multimeteo/plugins/dummy.c | 6 +- Daemons/weatherdaemon_multimeteo/server.c | 13 +- .../weatherdaemon.files | 1 + Daemons/weatherdaemon_multimeteo/weathlib.c | 7 +- 8 files changed, 338 insertions(+), 36 deletions(-) diff --git a/Daemons/weatherdaemon_multimeteo/Readme.md b/Daemons/weatherdaemon_multimeteo/Readme.md index e18a340..8ea6560 100644 --- a/Daemons/weatherdaemon_multimeteo/Readme.md +++ b/Daemons/weatherdaemon_multimeteo/Readme.md @@ -1,22 +1,324 @@ -Weather daemon for several different weather stations -===================================================== +# superweatherdaemon Documentation -## Usage: +## Overview -``` -Usage: weatherdaemon [args] - Be careful: command line options have priority over config - Where args are: +**superweatherdaemon** is a weather monitoring daemon designed for astronomical observatories. It +collects data from multiple heterogeneous weather stations (meteostations) via a plugin +architecture, computes a unified weather status, and provides control interfaces over TCP and local +UNIX sockets. The daemon can issue warnings, change weather level, and even trigger a forced +shutdown of instruments (e.g., close dome) when conditions become dangerous. - -P, --pidfile=arg pidfile name (default: /tmp/weatherdaemon.pid) - -c, --conffile=arg configuration file name (consists all or a part of long-named parameters and their values (e.g. plugin=liboldweather.so) - -h, --help show this help - -l, --logfile=arg save logs to file (default: none) - -p, --plugin=arg add this weather plugin (may be a lot of) (can occur multiple times) - -v, --verb logfile verbocity level (each -v increased) - --port=arg network port to connect (default: 12345); hint: use "localhost:port" to make local net socket - --sockpath=arg UNIX socket path (starting from '\0' for anonimous) of command socket +The project is written in C, uses CMake as its build system, and relies on the +[usefull_macros](https://github.com/eddyem/snippets_library) library for utilities and socket +management. + +## Dependencies + +- **Build tools**: CMake >= 4.0, C compiler with C11 support, `pkg-config`. +- **Library**: `usefull_macros` >= 0.3.5 (`sl_*` functions for logging, sockets, command-line parsing, etc.). +- **Optional**: `net-snmp` (for the SNMP UPS plugin). + +On Gentoo/Calculate Linux, install the basic build dependencies: + +```bash +emerge dev-build/cmake dev-util/pkgconf +# usefull_macros must be installed from its own source; follow its documentation. +# For SNMP support: +emerge net-analyzer/net-snmp ``` +## Building and Installation -TODO: brief documentation will be here +1. Clone or download the source tree. +2. Create a build directory and run CMake: + + ```bash + mkdir build && cd build + cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local + ``` + +3. Customise enabled plugins with `-D` options (all are `ON` by default): + + ```bash + cmake .. -DDUMMY=OFF -DFDEXAMPLE=OFF -DHYDREON=ON -DBTAMETEO=ON ... + ``` + + Available plugin options: + - `DUMMY`  Dummy weather station for testing. + - `FDEXAMPLE`  Example file descriptor plugin. + - `HYDREON`  Hydreon RG-11 rain sensor. + - `BTAMETEO`  BTA 6-m telescope main meteostation (shared memory). + - `REINHARDT`  Old Reinhardt meteostation. + - `WXA100`  Vaisala WXA100 ultrasonic station. + - `SNMP`  UPS monitoring via SNMP. + - `LIGHTNING`  AS3935-based lightning sensor. + +4. Building: + + ```bash + make + su -c "make install" + ``` + + The main executable `superweatherdaemon` is installed into `bin`; the plugin shared libraries + (`lib*.so`) go to the library directory. + +## Configuration + +The daemon can be configured entirely via command-line options, a configuration file, or both. +Command-line options take precedence. + +### Command-line Options + +| Option | Description | +|--------|-------------| +| `-h`, `--help` | Show help message and exit. | +| `-c `, `--conffile ` | Use a configuration file. Point non-existant file to get help. | +| `-l `, `--logfile=` | Write logs to a file (default: none). | +| `-P `, `--pidfile=` | PID file (default `/tmp/superweatherdaemon.pid`). | +| `-p `, `--plugin=` | Add a weather plugin; can be repeated for different plugins. Format: `path:type:device` (see below). | +| `--port=` | Network port for clients (default `12345`). Use `localhost:port` for local access only. | +| `--sockpath=` | UNIX socket path (start with `@` for an abstract socket). | +| `-T `, `--pollt ` | Max polling interval in seconds (integer). | +| `-v`, `--verb` | Increase verbosity level (each `-v` adds 1). | + +### Plugin Specification + +A plugin is a shared library (`.so`) that provides a `sensor_init` function. It is loaded with the +`--plugin` option. + +Format: + +``` +--plugin=library:type:parameter +``` + +- `library`: path to the shared library, e.g. `libwxa100.so`. +- `type`: Connection type  `D` for serial device, `U` for UNIX socket, `N` for INET socket. +- `parameter`: device path and optional speed (`/dev/ttyS0:9600`), UNIX socket name, or `host:port` for INET. + +Examples: + +```bash +--plugin=libreinhardt.so:D:/dev/ttyS0 +--plugin=libwxa100.so:D:/dev/pl2303_0 +--plugin=libhydreon.so:D:/dev/ch340_0:1200 +--plugin=libbtameteo.so (no device, uses shared memory) +``` + +Multiple plugins are listed in order of **importance** (first ones are considered primary for +weather level calculation). + +### Configuration File + +The configuration file uses a simple `key = value` syntax, with `#` for comments. Example: + +```ini +# network port for clients +port = 4444 +logfile = /var/log/meteo/superweather.log +verbose = 2 +sockpath = "@weather" +pollt = 1 +reinit_delay = 10 + +# Weather thresholds +ahtung_delay = 1800 # in seconds +good_wind = 5.0 # m/s +bad_wind = 10.0 +terrible_wind = 15.0 +good_humidity = 65.0 # percents +bad_humidity = 87.0 +terrible_humidity = 94.0 +good_clouds = 2500.0 # for reinhardt sensor in its units +bad_clouds = 2000.0 +terrible_clouds = 500.0 +clouds_negflag = 1 # 1 means the higher the value the better +good_sky = -40.0 # sky minus ambient temperature, degC +bad_sky = -10.0 +terrible_sky = 0.0 +# plugins - most important first +plugin = libwxa100.so:D:/dev/pl2303_0 +plugin = libhydreon.so:D:/dev/ch340_0:1200 +plugin = libbtameteo.so +plugin = libreinhardt.so:D:/dev/ttyS0 +``` + +Run with: + +```bash +superweatherdaemon -c /etc/weather.conf +``` + +To run on system start you can use OpenRó `rc.local` mechanism. + +## Plugins + +Each weather station is implemented as a shared library exporting a single function: + +```c +int sensor_init(sensordata_t *s); +``` + +The `sensordata_t` structure contains all required callbacks, data pointers, and private fields. +See `weathlib.h` for the full definition. + +### Plugin Lifecycle + +1. **Loading**: The main daemon calls `s = sensor_new(...)` to create `sensordata_t` structure, + opens given library with `dlopen` and calls `sensor_init` on that new `s`. +2. **Initialisation**: + - Set `s->name`, `s->Nvalues`, `s->values` array. + - Configure the communication channel (file descriptor) using `getFD(s->path)`. + - Create a worker thread that periodically reads sensor data and updates `s->values`. + Don't forget to lock `s->valmutex` on any operation with `s->values`. +3. **Data delivery**: Each time new data is available, call `s->freshdatahandler(s)` + (this is set by the daemon to `dumpsensors`). The main daemon then merges the data into the + global weather evaluation. +4. **Shutdown**: The daemon calls `s->kill(s)`, which must join the thread, close the file + descriptor, and free resources. Default `common_kill` handles most of this; plugins can override it. + +If the plugin is disconnected for some reason (for example, the network connection is lost), the +daemon will try to reconnect every `reinit_delay` seconds. + +### Available Plugins + +| Library | Sensor | Type | +|---------|--------|------| +| `libwsdummy.so` | Dummy station  outputs random walk data around realistic values. | Test / Development | +| `libfdex.so` | Example of a filedescriptor based plugin. Prompts for commaseparated values. | Example | +| `libhydreon.so` | Hydreon RG-11 optical rain sensor. | Serial | +| `libbtameteo.so` | BTA 6-m telescope main meteostation (shared memory). | Shared Memory | +| `libreinhardt.so` | Old Reinhardt meteostation (serial, `?U` command). | Serial | +| `libwxa100.so` | Vaisala WXA100 ultrasonic meteostation (serial, `0R0` command). | Serial | +| `libsnmp.so` | UPS monitor via SNMP (requires net-snmp). | Network | +| `liblightning.so` | AS3935-based lightning sensor (serial). | Serial | + +### Writing a New Plugin + +Use the existing plugins as templates. A minimal plugin must: + +- Include `weathlib.h`. +- Define an array of `val_t` describing each measured quantity. +- Implement `sensor_init`: + - Allocate `s->values`, copy the template array. + - Set `s->Nvalues`, `s->name`. + - Open the device (use `getFD(s->path)` for serial/sockets) and set `s->fdes`. + If your plugin don't need file descriptor, you must set `s->fdes` to any non-negative value. + - Create a ring buffer if needed (`sl_RB_new`). + - Start the worker thread that reads data, updates values inside `pthread_mutex_lock(&s->valmutex)`, + and calls `s->freshdatahandler(s)` (outside mutex locked). + - Return `TRUE` on success, `FALSE` on failure (call `s->kill(s)` to clean up). +- The `weathlib.h` provides helper functions: `common_onrefresh`, `common_getval`, `common_kill`. + +## Weather Level Calculation + +The daemon combines data from all active plugins and continuously evaluates a global **weather +level**: + +- `0`  **GOOD**: observations can start safely. +- `1`  **BAD**: risky to start, but can continue. +- `2`  **TERRIBLE**: dome must close, instruments park. +- `3`  **PROHIBITED**: complete shutdown, power off equipment. + +### Criteria + +Each weather parameter (wind speed, humidity, clouds, sky temperature, lightning distance, +precipitation, etc.) has configurable thresholds: + +- `good`  below/above this (depending on sign) the condition is good. +- `bad`  above this the condition is bad. +- `terrible`  above this the condition is terrible. +- `prohibited`  (if defined) above this the condition goes directly to PROHIBITED. +- `negflag`  if `1`, a smaller value is worse (e.g., clouds). +- `shtdnflag`  if `1`, entering the terrible/prohibited range also sets the **FORCE SHUTDOWN** flag. + +### Special Flags + +- **FORCE SHUTDOWN**: Some parameters (e.g., lightning within <= 5km, UPS on battery) carry the + `IS_FORCEDSHTDN` meaning and have `shtdnflag = 1`. When their value exceeds the terrible threshold, + the forced shutdown flag is raised, which immediately sets the weather level to PROHIBITED and is + typically used to cut power. +- **Manual FORBID**: An operator can send a signal (`SIGUSR1`) or a socket command to forbid + observations, which also forces PROHIBITED. `SIGUSR2` or a socket command clears forbidden flag. + +### Hysteresis + +Once a bad/terrible state is reached, the level is not lowered until `ahtung_delay` seconds have +passed since the last serious event. This prevents rapid toggling. + +## Server Commands (Socket API) + +The daemon listens on two interfaces: + +1. **Network socket** (TCP, default port 12345)  read-only (in meaning they cannot change any + parameters) access for remote clients. +2. **Local UNIX socket** (abstract or filesystem, default `@weather`)  full control for local applications. + +Commands are sent as plain text strings, terminated by a newline. + +### Common (read-only) Commands + +| Command | Description | +|---------|-------------| +| `get` | Return all collected weather data (all stations). | +| `get=` | Return data from plugin ``. | +| `list` | List all loaded plugins with their names and value counts. | +| `time` | Return server UNIX time (float seconds). | +| `chklevel` | Show the `sense` (importance) level of every collected parameter. | +| `chklevel=` | Same for a specific plugin. | + +### Local-only (read-write) Commands + +| Command | Description | +|---------|-------------| +| `forbid` | Get current FORBID flag (0/1). | +| `forbid=<0/1>` | Set/clear manual FORBID. | +| `forceoff` | Get FORCE SHUTDOWN flag. | +| `forceoff=<0/1>` | Set/clear it manually. | +| `weathlevel` | Get current weather level (0-3 for GOOD-PROHIBITED). | +| `weathlevel=<0..3>` | Force weather level (use with caution). | +| `setlevel=:=,...` | Change the `sense` field of one or more sensor parameters. Example: `setlevel=1:WIND=3,HUMIDITY=3` disables wind and humidity from station 1. | +| `mute=` | Stop refreshing data from plugin `` (mute). | +| `unmute=` | Resume refreshing. | +| `ismuted=` | Return 1 if muted, 0 otherwise. | + +Reply format: each line is a FITS-like `KEY = value / comment` string; commands that set something +usually echo back the variable and its new value. For `get=` each `KEY` have a suffix in square +brackets  number of plugin, e.g. `WIND[1]= 10.1 / Wind speed, m/s`. + +## Signals + +| Signal | Effect | +|--------|--------| +| `SIGTERM`, `SIGINT`, `SIGQUIT` | Clean shutdown (removes PID file, kills plugins, destroys sockets). | +| `SIGHUP` | Ignored. | +| `SIGUSR1` | Set manual FORBID (weather level == PROHIBITED). | +| `SIGUSR2` | Clear manual FORBID. | +| `SIGPIPE` | Logged, used to detect network plugins disconnections. | + +## Files + +| File | Purpose | +|------|---------| +| `CMakeLists.txt` | Top-level build definition. | +| `cmdlnopts.c/.h` | Command-line and configuration file parsing. | +| `main.c` | Daemon entry point, signal handlers, forking. | +| `mainweather.c/.h` | Global weather evaluation, data collection, forced shutdown. | +| `sensors.c/.h` | Plugin management (load, unload, getters). | +| `server.c/.h` | TCP and UNIX socket servers. | +| `weathlib.c/.h` | Common plugin API, value definitions, helper functions. | +| `fd.c` | Function `getFD()` to open serial devices or sockets for plugins. | +| `example.config` | Sample configuration file. | +| `plugins/CMakeLists.txt` | Build file for all plugins. | +| `plugins/*.c` | Individual plugin source files. | + +## License + +The project is released under the **GNU General Public License v3.0** or later. See the headers in +the source files for the full legal text. + +--- + +*For further assistance or to report issues, please contact the maintainer: Edward V. Emelianov +.* diff --git a/Daemons/weatherdaemon_multimeteo/example.config b/Daemons/weatherdaemon_multimeteo/example.config index fb6e185..272fed0 100644 --- a/Daemons/weatherdaemon_multimeteo/example.config +++ b/Daemons/weatherdaemon_multimeteo/example.config @@ -12,8 +12,8 @@ reinit_delay = 10 # !!! Point plugins in order of meaning: the most important are first !!! # see help for plugins format -# this should be furst as almost a half of its sensors are broken -plugin = libreinhardt.so:D:/dev/ttyS0 plugin = libwxa100.so:D:/dev/pl2303_0 plugin = libhydreon.so:D:/dev/ch340_0:1200 plugin = libbtameteo.so +# this should be last as almost a half of its sensors are broken +plugin = libreinhardt.so:D:/dev/ttyS0 diff --git a/Daemons/weatherdaemon_multimeteo/fd.c b/Daemons/weatherdaemon_multimeteo/fd.c index 7e34af0..11c1a77 100644 --- a/Daemons/weatherdaemon_multimeteo/fd.c +++ b/Daemons/weatherdaemon_multimeteo/fd.c @@ -56,7 +56,12 @@ static int openserial(const char *path){ } DBG("Opened %s @ %d", str, speed); FREE(str); - return serial->comfd; + int comfd = serial->comfd; + FREE(serial->portname); + FREE(serial->buf); + FREE(serial->format); + FREE(serial); + return comfd; } /** diff --git a/Daemons/weatherdaemon_multimeteo/mainweather.c b/Daemons/weatherdaemon_multimeteo/mainweather.c index dbd432a..79791de 100644 --- a/Daemons/weatherdaemon_multimeteo/mainweather.c +++ b/Daemons/weatherdaemon_multimeteo/mainweather.c @@ -569,6 +569,7 @@ void refresh_sensval(sensordata_t *s){ // set/clear `forbid` flag (by signals USR1 and USR2) void forbid_observations(int f){ + pthread_mutex_lock(&datamutex); if(f) Forbidden = 1; else Forbidden = 0; int curt = (int) time(NULL); @@ -577,6 +578,7 @@ void forbid_observations(int f){ collected_data[NLASTAHTUNG].time = curt; sprintf(collected_data[NAHTUNGRSN].value.str, "FORBID"); collected_data[NAHTUNGRSN].time = curt; + pthread_mutex_unlock(&datamutex); DBG("Change FORBID status to %d", f); } diff --git a/Daemons/weatherdaemon_multimeteo/plugins/dummy.c b/Daemons/weatherdaemon_multimeteo/plugins/dummy.c index 796c7f1..5828f2b 100644 --- a/Daemons/weatherdaemon_multimeteo/plugins/dummy.c +++ b/Daemons/weatherdaemon_multimeteo/plugins/dummy.c @@ -55,11 +55,11 @@ static void *mainthread(void *s){ //if(!sensor->values[5].value.u && drand48() > 0.7) sensor->values[5].value.u = 1; time_t cur = time(NULL); for(int i = 0; i < NS-1; ++i) sensor->values[i].time = cur; - f = sensor->values[6].value.f - (drand48() - 0.52); + /*f = sensor->values[6].value.f - (drand48() - 0.52); if(f > 0. && f < 60){ sensor->values[6].value.f = f; sensor->values[6].time = cur; - } + }*/ pthread_mutex_unlock(&sensor->valmutex); //DBG("unlocked"); if(sensor->freshdatahandler) sensor->freshdatahandler(sensor); @@ -82,7 +82,7 @@ int sensor_init(sensordata_t *s){ s->values[3].value.f = 600.; s->values[4].value.f = 89.; s->values[5].value.u = 0; - s->values[6].value.f = 4.5; + //s->values[6].value.f = 4.5; if(pthread_create(&s->thread, NULL, mainthread, (void*)s)){ s->kill(s); return FALSE; diff --git a/Daemons/weatherdaemon_multimeteo/server.c b/Daemons/weatherdaemon_multimeteo/server.c index fe552bf..1f35db8 100644 --- a/Daemons/weatherdaemon_multimeteo/server.c +++ b/Daemons/weatherdaemon_multimeteo/server.c @@ -322,17 +322,7 @@ static void toomuch(int fd){ const char *m = "Try later: too much clients connected\n"; send(fd, m, sizeof(m)-1, MSG_NOSIGNAL); shutdown(fd, SHUT_WR); - DBG("shutdown, wait"); - double t0 = sl_dtime(); - uint8_t buf[8]; - while(sl_dtime() - t0 < 90.){ // change this value to smaller for real work - if(sl_canread(fd)){ - ssize_t got = read(fd, buf, 8); - DBG("Got=%zd", got); - if(got < 1) break; - } - } - DBG("Disc after %gs", sl_dtime() - t0); + DBG("shutdown"); LOGWARN("Client fd=%d tried to connect after MAX reached", fd); } // new connections handler (return FALSE to reject client) @@ -414,6 +404,7 @@ int start_servers(const char *netnode, const char *sockpath){ if(sensor_alive(s)) continue; // sensor isn't inited - try to do it DBG("sensor with path %s isn't inited, try", s->path); + s->kill(s); // clear resources if(s->init){ if(s->init(s)) LOGMSG("Sensor %s reinited @ %s", s->name, s->path); else DBG("Can't reinit"); diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.files b/Daemons/weatherdaemon_multimeteo/weatherdaemon.files index 4a33c63..8923168 100644 --- a/Daemons/weatherdaemon_multimeteo/weatherdaemon.files +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.files @@ -1,4 +1,5 @@ CMakeLists.txt +Readme.md cmdlnopts.c cmdlnopts.h fd.c diff --git a/Daemons/weatherdaemon_multimeteo/weathlib.c b/Daemons/weatherdaemon_multimeteo/weathlib.c index bd28bb9..3861c48 100644 --- a/Daemons/weatherdaemon_multimeteo/weathlib.c +++ b/Daemons/weatherdaemon_multimeteo/weathlib.c @@ -76,6 +76,10 @@ void common_kill(sensordata_t *s){ FNAME(); if(!s) return; if(s->fdes > -1){ // inited and maybe have opened file/socket + close(s->fdes); + s->fdes = -1; + DBG("FD closed"); + usleep(5000); if(pthread_equal(pthread_self(), s->thread)){ DBG("Don't cancel myself"); }else{ @@ -86,9 +90,6 @@ void common_kill(sensordata_t *s){ DBG("Done"); } } - close(s->fdes); - s->fdes = -1; - DBG("FD closed"); } DBG("Delete RB"); if(s->ringbuffer) sl_RB_delete(&s->ringbuffer);