diff --git a/Daemons/weatherdaemon_multimeteo/CMakeLists.txt b/Daemons/weatherdaemon_multimeteo/CMakeLists.txt new file mode 100644 index 0000000..2b1211c --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.20) +set(PROJ weatherdaemon) +set(MAJOR_VERSION "0") +set(MID_VERSION "0") +set(MINOR_VERSION "1") + +aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SOURCES) +#set(SOURCES main.c cmdlnopts.c sensors.c) + +set(VERSION "${MAJOR_VERSION}.${MID_VERSION}.${MINOR_VERSION}") +project(${PROJ} VERSION ${VERSION} LANGUAGES C) +message("VER: ${VERSION}") + +# list of options +option(DEBUG "Compile in debug mode" OFF) +option(DUMMY "Dummy device plugin" ON) + + +# default flags +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -W -Wextra -fno-builtin-strlen") + +message("Install dir prefix: ${CMAKE_INSTALL_PREFIX}") + +add_definitions(-D_XOPEN_SOURCE=1234 -D_DEFAULT_SOURCE -D_GNU_SOURCE + -DPACKAGE_VERSION=\"${VERSION}\" -DMINOR_VERSION=\"${MINOR_VERSION}\" -DMID_VERSION=\"${MID_VERSION}\" + -DMAJOR_VERSION=\"${MAJOR_VESION}\") + +set(CMAKE_COLOR_MAKEFILE ON) + +# cmake -DDEBUG=on -> debugging +if(DEBUG) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Og -g3 -ggdb -Werror") + add_definitions(-DEBUG) + set(CMAKE_BUILD_TYPE DEBUG) + set(CMAKE_VERBOSE_MAKEFILE "ON") +else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -march=native -fdata-sections -ffunction-sections -flto=auto") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections -flto=auto") + set(CMAKE_BUILD_TYPE RELEASE) +endif() + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") +find_package(PkgConfig REQUIRED) +pkg_check_modules(${PROJ} REQUIRED usefull_macros>=0.3.2) + +#include(FindOpenMP) +#if(OPENMP_FOUND) +# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") +# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_EXE_LINKER_FLAGS}") +# add_definitions(-DOMP_FOUND) +#endif() + +# exe & lib files +add_executable(${PROJ} ${SOURCES}) +target_link_libraries(${PROJ} ${${PROJ}_LIBRARIES} -lm ${CMAKE_DL_LIBS}) +target_include_directories(${PROJ} PUBLIC ${${PROJ}_INCLUDE_DIRS} .) +target_link_directories(${PROJ} PUBLIC ${${PROJ}_LIBRARY_DIRS} ) +set_target_properties(${PROJLIB} PROPERTIES VERSION ${VERSION}) + +include(GNUInstallDirs) +# Installation of the program +install(TARGETS ${PROJ} DESTINATION "bin") + +add_subdirectory("plugins") diff --git a/Daemons/weatherdaemon_multimeteo/Readme.md b/Daemons/weatherdaemon_multimeteo/Readme.md new file mode 100644 index 0000000..e18a340 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/Readme.md @@ -0,0 +1,22 @@ +Weather daemon for several different weather stations +===================================================== + +## Usage: + +``` +Usage: weatherdaemon [args] + Be careful: command line options have priority over config + Where args are: + + -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 +``` + + +TODO: brief documentation will be here diff --git a/Daemons/weatherdaemon_multimeteo/cmdlnopts.c b/Daemons/weatherdaemon_multimeteo/cmdlnopts.c new file mode 100644 index 0000000..6173662 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/cmdlnopts.c @@ -0,0 +1,163 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include "cmdlnopts.h" + +/* + * here are global parameters initialisation + */ +int help; + +// default values for Gdefault & help +#define DEFAULT_PORT "12345" +#define DEFAULT_PID "/tmp/weatherdaemon.pid" + +// DEFAULTS +// default global parameters +static glob_pars defpars = { + .port = DEFAULT_PORT, + .logfile = NULL, + .verb = 0, + .pidfile = DEFAULT_PID +}; +// default config: all values should be wrong or empty to understand than user change them +static glob_pars defconf = { + .verb = -1, +}; + +static glob_pars G; + +/* + * Define command line options by filling structure: + * name has_arg flag val type argptr help +*/ +#define COMMON_OPTS \ + {"port", NEED_ARG, NULL, 0, arg_string, APTR(&G.port), "network port to connect (default: " DEFAULT_PORT "); hint: use \"localhost:port\" to make local net socket"}, \ + {"logfile", NEED_ARG, NULL, 'l', arg_string, APTR(&G.logfile), "save logs to file (default: none)"}, \ + {"pidfile", NEED_ARG, NULL, 'P', arg_string, APTR(&G.pidfile), "pidfile name (default: " DEFAULT_PID ")"}, \ + {"sockpath",NEED_ARG, NULL, 0, arg_string, APTR(&G.sockname), "UNIX socket path (starting from '\\0' for anonimous) of command socket"}, \ + {"plugin", MULT_PAR, NULL, 'p', arg_string, APTR(&G.plugins), "add this weather plugin (may be a lot of)"}, + +sl_option_t cmdlnopts[] = { + {"help", NO_ARGS, NULL, 'h', arg_int, APTR(&help), "show this help"}, + {"conffile",NEED_ARG, NULL, 'c', arg_string, APTR(&G.conffile), "configuration file name (consists all or a part of long-named parameters and their values (e.g. plugin=liboldweather.so)"}, + {"verb", NO_ARGS, NULL, 'v', arg_none, APTR(&G.verb), "logfile verbocity level (each -v increased)"}, \ + COMMON_OPTS + end_option +}; + +sl_option_t confopts[] = { + {"verbose", NEED_ARG, NULL, 'v', arg_int, APTR(&G.verb), "logfile verbocity level"}, \ + COMMON_OPTS + end_option +}; + +static int sortstrings(const void *v1, const void *v2){ + const char **s1 = (const char **)v1, **s2 = (const char **)v2; + return strcmp(*s1, *s2); +} + +// compare plugins from configuration and command line; add to command line plugins all new +static void compplugins(glob_pars *cmdline, glob_pars *conf){ + if(!cmdline) return; + char **p; + int nconf = 0; + if(conf){ + p = conf->plugins; + if(p && *p) while(*p++) ++nconf; + } + int ncmd = 0; + p = cmdline->plugins; + if(p && *p) while(*p++) ++ncmd; + DBG("Got %d plugins in conf and %d in cmdline", nconf, ncmd); + // compare plugins and rebuild new list + int newsize = ncmd + nconf; + if(newsize == 0) return; // no plugins in both + char **newarray = MALLOC(char*, newsize + 1); // +1 for ending NULL + for(int i = 0; i < ncmd; ++i){ newarray[i] = cmdline->plugins[i]; } + FREE(cmdline->plugins); + if(conf){ + for(int i = 0; i < nconf; ++i){ newarray[i+ncmd] = conf->plugins[i]; } + FREE(conf->plugins); + } + qsort(newarray, newsize, sizeof(char*), sortstrings); + DBG("NOW together:"); p = newarray; while(*p) printf("\t%s\n", *p++); + p = newarray; + int nondobuleidx = 0; + for(int i = 0; i < newsize;){ + int j = i + 1; + for(; j < newsize; ++j){ + if(strcmp(newarray[i], newarray[j])) break; + FREE(newarray[j]); + } + if(nondobuleidx != i){ + newarray[nondobuleidx] = newarray[i]; + newarray[i] = NULL; + } + ++nondobuleidx; + i = j; + } + DBG("Result:"); p = newarray; while(*p) printf("\t%s\n", *p++); + cmdline->plugins = newarray; + cmdline->nplugins = nondobuleidx; +} + +/** + * Parse command line options and return dynamically allocated structure + * to global parameters + * @param argc - copy of argc from main + * @param argv - copy of argv from main + * @return allocated structure with global parameters + */ +glob_pars *parse_args(int argc, char **argv){ + G = defpars; // copy defaults + // format of help: "Usage: progname [args]\n" + sl_helpstring("Usage: %s [args]\n\t" COLOR_RED "Be careful: command line options have priority over config" COLOR_OLD "\n\tWhere args are:\n"); + // parse arguments + sl_parseargs(&argc, &argv, cmdlnopts); + DBG("verb: %d", G.verb); + if(help) sl_showhelp(-1, cmdlnopts); + if(argc > 0){ + WARNX("You give %d unused parameters:", argc); + while(argc) printf("\t%s\n", argv[--argc]); + } + DBG("PARS: \n-------------\n%s-------------\n\n", sl_print_opts(cmdlnopts, 1)); + if(G.conffile){ // read conffile and fix parameters (cmdline args are in advantage) + glob_pars oldpars = G; // save cmdline opts + G = defconf; + if(!sl_conf_readopts(oldpars.conffile, confopts)) ERRX("Can't get options from %s", G.conffile); + DBG("CONF: \n-------------\n%s-------------\n\n", sl_print_opts(confopts, 1)); + if((0 == strcmp(oldpars.port, DEFAULT_PORT)) && G.port) oldpars.port = G.port; + if(!oldpars.logfile && G.logfile) oldpars.logfile = G.logfile; + if(!oldpars.verb && G.verb > -1) oldpars.verb = G.verb; + if((0 == strcmp(oldpars.pidfile, DEFAULT_PID)) && G.pidfile) oldpars.pidfile = G.pidfile; + if(!oldpars.sockname && G.sockname) oldpars.sockname = G.sockname; + // now check plugins + compplugins(&oldpars, &G); + G = oldpars; + }else compplugins(&G, NULL); + DBG("RESULT: \n-------------\n%s-------------\n\n", sl_print_opts(cmdlnopts, 1)); + DBG("Nplugins = %d", G.nplugins); + return &G; +} + diff --git a/Daemons/weatherdaemon_multimeteo/cmdlnopts.h b/Daemons/weatherdaemon_multimeteo/cmdlnopts.h new file mode 100644 index 0000000..3fcdca8 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/cmdlnopts.h @@ -0,0 +1,36 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +/* + * here are some typedef's for global data + */ +typedef struct{ + char *sockname; // UNIX socket name for internal connections (commands etc) + char *port; // port for external clients + char *logfile; // logfile name + int verb; // verbocity level + char *pidfile; // pidfile name + char **plugins; // all plugins connected + int nplugins; // amount of plugins + char *conffile; // configuration file used instead of long command line +} glob_pars; + + +glob_pars *parse_args(int argc, char **argv); diff --git a/Daemons/weatherdaemon_multimeteo/main.c b/Daemons/weatherdaemon_multimeteo/main.c new file mode 100644 index 0000000..023e524 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/main.c @@ -0,0 +1,92 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include // wait +#include //prctl +#include + +#include "cmdlnopts.h" +#include "sensors.h" +#include "server.h" + +static pid_t childpid = 0; + +void signals(int signo){ + if(childpid){ + LOGERR("Killed with status %d", signo); + closeplugins(); + kill_servers(); + usleep(1000); // let child close everything before dead + }else{ + LOGERR("Main process exits with status %d", signo); + } + exit(signo); +} + +extern const char *__progname; + +int main(int argc, char **argv){ + glob_pars *GP = NULL; + sl_init(); + signal(SIGTERM, signals); // kill (-15) - quit + signal(SIGHUP, SIG_IGN); // hup - ignore + signal(SIGINT, signals); // ctrl+C - quit + signal(SIGQUIT, signals); // ctrl+\ - quit + signal(SIGTSTP, SIG_IGN); // ignore ctrl+Z + GP = parse_args(argc, argv); + if(!GP) ERRX("Error parsing args"); + if(!GP->sockname) ERRX("Point command socket name"); + if(GP->logfile){ + sl_loglevel_e lvl = LOGLEVEL_ERR + GP->verb; + if(lvl >= LOGLEVEL_AMOUNT) lvl = LOGLEVEL_AMOUNT - 1; + DBG("Loglevel: %d", lvl); + if(!OPENLOG(GP->logfile, lvl, 1)) ERRX("Can't open log file"); + LOGMSG("Started"); + } + int nopened = openplugins(GP->plugins, GP->nplugins); + if(nopened < 1){ + LOGERR("No plugins found; exit!"); + ERRX("Can't find any sensor plugin"); + } + if(GP->nplugins && GP->nplugins != nopened) LOGWARN("Work without some plugins"); + #ifndef EBUG + sl_check4running((char*)__progname, GP->pidfile); + while(1){ // guard for dead processes + childpid = fork(); + if(childpid){ + LOGDBG("create child with PID %d\n", childpid); + DBG("Created child with PID %d\n", childpid); + wait(NULL); + WARNX("Child %d died\n", childpid); + LOGWARN("Child %d died\n", childpid); + sleep(1); + }else{ + prctl(PR_SET_PDEATHSIG, SIGTERM); // send SIGTERM to child when parent dies + break; // go out to normal functional + } + } + #endif + if(!start_servers(GP->port, GP->sockname)) ERRX("Can't run server's threads"); + while(1); + //WARNX("TEST ends"); + //signals(0); + return 0; // never reached +} diff --git a/Daemons/weatherdaemon_multimeteo/plugins/CMakeLists.txt b/Daemons/weatherdaemon_multimeteo/plugins/CMakeLists.txt new file mode 100644 index 0000000..72e2e24 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/plugins/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.20) + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(PLUGINS REQUIRED usefull_macros) + +include_directories(${PLUGINS_INCLUDE_DIRS} ..) +link_directories(${PLUGINSLIBRARY_DIRS}) +link_libraries(${$PLUGINS_LIBRARIES} -fPIC) + +set(LIBS "") + +if(DUMMY) + add_library(wsdummy SHARED dummy.c) + list(APPEND LIBS wsdummy) +endif(DUMMY) + +install(TARGETS ${LIBS} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/Daemons/weatherdaemon_multimeteo/plugins/dummy.c b/Daemons/weatherdaemon_multimeteo/plugins/dummy.c new file mode 100644 index 0000000..c819294 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/plugins/dummy.c @@ -0,0 +1,113 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "weathlib.h" + +#define NS (6) + +extern sensordata_t sensor; + +static void (*freshdatahandler)(const struct sensordata_t* const) = NULL; // do nothing with fresh data +static pthread_t thread; + +static val_t values[NS] = { // fields `name` and `comment` have no sense until value meaning is `IS_OTHER` + {.name = "WIND", .comment = "wind speed, m/s", .sense = VAL_OBLIGATORY, .type = VALT_FLOAT, .meaning = IS_WIND}, + {.name = "WINDDIR", .comment = "wind direction azimuth (from south over west), deg", .sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_WINDDIR}, + {.name = "EXTTEMP", .comment = "external temperature, degC", .sense = VAL_OBLIGATORY, .type = VALT_FLOAT, .meaning = IS_AMB_TEMP}, + {.name = "PRESSURE", .comment = "atmospheric pressure, hPa", .sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_PRESSURE}, + {.name = "HUMIDITY", .comment = "air relative humidity, %%", .sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_HUMIDITY}, + {.name = "PRECIP", .comment = "precipitations flag (0 - no, 1 - yes)", .sense = VAL_OBLIGATORY, .type = VALT_UINT, .meaning = IS_PRECIP}, +}; + +void *mainthread(void _U_ *U){ + FNAME(); + double t0 = sl_dtime(); + while(1){ + float f = values[0].value.f + (drand48() - 0.5) / 2.; + if(f >= 0.) values[0].value.f = f; + f = values[1].value.f + (drand48() - 0.5) * 4.; + if(f > 160. && f < 200.) values[1].value.f = f; + f = values[2].value.f + (drand48() - 0.5) / 20.; + if(f > 13. && f < 21.) values[2].value.f = f; + f = values[3].value.f + (drand48() - 0.5) / 100.; + if(f > 585. && f < 615.) values[3].value.f = f; + f = values[4].value.f + (drand48() - 0.5) / 10.; + if(f > 60. && f <= 100.) values[4].value.f = f; + values[5].value.u = (f > 98.) ? 1 : 0; + time_t cur = time(NULL); + for(int i = 0; i < NS; ++i) values[i].time = cur; + if(freshdatahandler) freshdatahandler(&sensor); + while(sl_dtime() - t0 < 1.) usleep(500); + t0 = sl_dtime(); + } + return NULL; +} + +static int init(int N){ + FNAME(); + values[0].value.f = 1.; + values[1].value.f = 180.; + values[2].value.f = 17.; + values[3].value.f = 600.; + values[4].value.f = 80.; + values[5].value.u = 0; + if(pthread_create(&thread, NULL, mainthread, NULL)) return 0; + sensor.PluginNo = N; + return NS; +} + +static int onrefresh(void (*handler)(const struct sensordata_t* const)){ + FNAME(); + if(!handler) return FALSE; + freshdatahandler = handler; + return TRUE; +} + +static void die(){ + FNAME(); + if(0 == pthread_kill(thread, 9)){ + DBG("Killed, join"); + pthread_join(thread, NULL); + DBG("Done"); + } +} + +/** + * @brief getval - value's getter + * @param o (o) - value + * @param N - it's index + * @return FALSE if failed + */ +static int getval(val_t *o, int N){ + if(N < 0 || N >= NS) return FALSE; + if(o) *o = values[N]; + return TRUE; +} + +sensordata_t sensor = { + .name = "Dummy weatherstation", + .Nvalues = NS, + .init = init, + .onrefresh = onrefresh, + .get_value = getval, + .die = die, +}; diff --git a/Daemons/weatherdaemon_multimeteo/sensors.c b/Daemons/weatherdaemon_multimeteo/sensors.c new file mode 100644 index 0000000..3d264dd --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/sensors.c @@ -0,0 +1,203 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "sensors.h" + +#define WARNXL(...) do{ LOGWARN(__VA_ARGS__); WARNX(__VA_ARGS__); } while(0) +#define WARNL(...) do{ LOGWARN(__VA_ARGS__); WARN(__VA_ARGS__); } while(0) +#define ERRXL(...) do{ LOGERR(__VA_ARGS__); ERRX(__VA_ARGS__); } while(0) +#define ERRL(...) do{ LOGERR(__VA_ARGS__); ERR(__VA_ARGS__); } while(0) + +static int nplugins = 0; +static sensordata_t **allplugins = NULL; + +int get_nplugins(){ + return nplugins; +} + +/** + * @brief get_plugin - get copy of opened plugin + * @param o (o) - plugin with given index + * @param N - index in `allplugins` + * @return TRUE if OK + */ +int get_plugin(sensordata_t *o, int N){ + if(!o || N < 0 || N >= nplugins) return FALSE; + *o = *allplugins[N]; + return TRUE; +} + +void *open_plugin(const char *name){ + DBG("try to open lib %s", name); + void* dlh = dlopen(name, RTLD_NOLOAD); // library may be already opened + if(!dlh){ + DBG("Not loaded - load"); + dlh = dlopen(name, RTLD_NOW); + } + if(!dlh){ + char *e = dlerror(); + WARNXL("Can't find plugin! %s", (e) ? e : ""); + return NULL; + } + return dlh; +} + +#ifdef EBUG +// in release this function can be used for meteo logging +static void dumpsensors(const struct sensordata_t* const station){ + FNAME(); + if(!station || !station->get_value || station->Nvalues < 1) return; + char buf[FULL_LEN+1]; + int N = (nplugins > 1) ? station->PluginNo : -1; + for(int i = 0; i < station->Nvalues; ++i){ + val_t v; + if(!station->get_value(&v, i)) continue; + if(0 < format_sensval(&v, buf, FULL_LEN+1, N)){ + printf("%s\n", buf); + } + } +} +#endif + +/** + * @brief openplugins - open sensors' plugin and init it + * @param paths - paths to plugins + * @param N - amount of plugins + * @return amount of opened and inited plugins + * This function should be runned only once at start + */ +int openplugins(char **paths, int N){ + if(!paths || !*paths || N < 1) return 0; + if(allplugins || nplugins){ + WARNXL("Plugins already opened"); return 0; + } + allplugins = MALLOC(sensordata_t*, N); + green("Try to open plugins:\n"); + for(int i = 0; i < N; ++i){ + printf("\tplugin[%d]=%s\n", i, paths[i]); + void* dlh = open_plugin(paths[i]); + if(!dlh) continue; + DBG("OPENED"); + void *s = dlsym(dlh, "sensor"); + if(s){ + sensordata_t *S = (sensordata_t*) s; + if(!S->get_value) WARNXL("Sensor library %s have no values' getter!", paths[i]); + if(!S->init){ + WARNXL("Sensor library %s have no init funtion"); + continue; + } + int ns = S->init(nplugins); + if(ns < 1) WARNXL("Can't init plugin %s", paths[i]); + else{ +#ifdef EBUG + if(!S->onrefresh(dumpsensors)) WARNXL("Can't init refresh funtion"); +#endif + LOGMSG("Plugin %s nave %d sensors", paths[i], ns); + allplugins[nplugins++] = S; + } + }else WARNXL("Can't find field `sensor` in plugin %s: %s", paths[i], dlerror()); + } + return nplugins; +} + +/** + * @brief closeplugins - call `die` function for all sensors + * This function should be runned at exit + */ +void closeplugins(){ + if(!allplugins || nplugins < 1) return; + for(int i = 0; i < nplugins; ++i){ + if(allplugins[i]->die) allplugins[i]->die(); + } + FREE(allplugins); + nplugins = 0; +} + +/** + * @brief format_sensval - snprintf sensor's value into buffer + * @param v - value to get + * @param buf - buffer + * @param buflen - full length of `buf` + * @param Np - if Np>-1, show it as plugin number (added to field name in square brackets, like WIND[1]); + * @return amount of symbols printed or -1 if error + */ +int format_sensval(const val_t *v, char *buf, int buflen, int Np){ + --buflen; // for trailing zero + if(!v || !buf || buflen < FULL_LEN) return -1; + char strval[VAL_LEN+1]; + switch(v->type){ + case VALT_UINT: snprintf(strval, VAL_LEN, "%u", v->value.u); break; + case VALT_INT: snprintf(strval, VAL_LEN, "%d", v->value.i); break; + case VALT_FLOAT: snprintf(strval, VAL_LEN, "%g", v->value.f); break; + default: sprintf(strval, "'ERROR'"); + } + const char* const NM[] = { // names of standard fields + [IS_WIND] = "WIND", + [IS_WINDDIR] = "WINDDIR", + [IS_HUMIDITY] = "HUMIDITY", + [IS_AMB_TEMP] = "EXTTEMP", + [IS_INNER_TEMP] = "INTTEMP", + [IS_HW_TEMP] = "HWTEMP", // mirror? + [IS_PRESSURE] = "PRESSURE", + [IS_PRECIP] = "PRECIP", + [IS_PRECIP_LEVEL]="PRECIPLV", + [IS_MIST] = "MIST", + [IS_CLOUDS] = "CLOUDS", + [IS_SKYTEMP] = "SKYTEMP" + }; + const char* const CMT[] = { // comments for standard fields + [IS_WIND] = "Wind, m/s", + [IS_WINDDIR] = "Wind direction, degr (CW from north to FROM)", + [IS_HUMIDITY] = "Humidity, percent", + [IS_AMB_TEMP] = "Ambient temperature, degC", + [IS_INNER_TEMP] = "In-dome temperature, degC", + [IS_HW_TEMP] = "Hardware (mirror?) termperature, degC", + [IS_PRESSURE] = "Atmospheric pressure, mmHg", + [IS_PRECIP] = "Precipitation (1 - yes, 0 - no)", + [IS_PRECIP_LEVEL]="Precipitation level (mm)", + [IS_MIST] = "Mist (1 - yes, 0 - no)", + [IS_CLOUDS] = "Integral clouds value (bigger - better)", + [IS_SKYTEMP] = "Mean sky temperatyre" + }; + const char *name = NULL, *comment = NULL; + int idx = v->meaning; + if(idx < IS_OTHER){ + name = NM[idx]; + comment = CMT[idx]; + }else{ + name = v->name; + comment = v->comment; + } + int got; + if(Np > -1) got = snprintf(buf, buflen, "%s[%d]=%s / %s", name, Np, strval, comment); + else got = snprintf(buf, buflen, "%s=%s / %s", name, strval, comment); + return got; +} + +// the same for measurement time formatting +int format_msrmttm(time_t t, char *buf, int buflen){ + --buflen; // for trailing zero + if(!buf || buflen < FULL_LEN) return -1; + char cmt[COMMENT_LEN+1]; + struct tm *T = localtime(&t); + strftime(cmt, COMMENT_LEN, "%F %T", T); + return snprintf(buf, buflen, "TMEAS=%zd / Last measurement time: %s", t, cmt); +} diff --git a/Daemons/weatherdaemon_multimeteo/sensors.h b/Daemons/weatherdaemon_multimeteo/sensors.h new file mode 100644 index 0000000..20894c2 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/sensors.h @@ -0,0 +1,28 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "weathlib.h" + +int openplugins(char **paths, int N); +void closeplugins(); +int get_plugin(sensordata_t *o, int N); +int get_nplugins(); +int format_sensval(const val_t *v, char *buf, int buflen, int Np); +int format_msrmttm(time_t t, char *buf, int buflen); diff --git a/Daemons/weatherdaemon_multimeteo/server.c b/Daemons/weatherdaemon_multimeteo/server.c new file mode 100644 index 0000000..05a01ad --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/server.c @@ -0,0 +1,229 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "sensors.h" +#include "server.h" + +// if measurement time oldest than now minus `oldest_interval`, we think measurement are too old +static time_t oldest_interval = 60; + +// server's sockets: net and local (UNIX) +static sl_sock_t *netsocket = NULL, *localsocket; +//static pthread_t netthread, locthread; + +// show user current time +static sl_sock_hresult_e timehandler(sl_sock_t *client, _U_ sl_sock_hitem_t *item, _U_ const char *req){ + if(!client) return RESULT_FAIL; + char buf[32]; + snprintf(buf, 31, "UNIXT=%.3f\n", sl_dtime()); + sl_sock_sendstrmessage(client, buf); + return RESULT_SILENCE; +} + +// show all connected libraries +static sl_sock_hresult_e listhandler(sl_sock_t *client, _U_ sl_sock_hitem_t *item, _U_ const char *req){ + if(!client) return RESULT_FAIL; + char buf[128]; + int N = get_nplugins(); + if(N < 1) return RESULT_FAIL; + sensordata_t d; + for(int i = 0; i < N; ++i){ + if(!get_plugin(&d, i)) continue; + snprintf(buf, 127, "PLUGIN[%d]=%s\nNVALUES[%d]=%d\n", i, d.name, i, d.Nvalues); + sl_sock_sendstrmessage(client, buf); + } + return RESULT_SILENCE; +} + +/** + * @brief showdata - send to user meteodata + * @param client - client data + * @param N - index of station + * @param showidx - == TRUE to show index in square brackets + */ +static void showdata(sl_sock_t *client, int N, int showidx){ + char buf[FULL_LEN+1]; + val_t v; + sensordata_t s; + if(!get_plugin(&s, N) || (s.Nvalues < 1)){ + snprintf(buf, FULL_LEN, "Can't get plugin[%d]\n", N); + sl_sock_sendstrmessage(client, buf); + return; + } + if(!showidx || get_nplugins() == 1) N = -1; // only one -> don't show indexes + time_t oldest = time(NULL) - oldest_interval; + uint64_t Tsum = 0; int nsum = 0; + for(int i = 0; i < s.Nvalues; ++i){ + if(!s.get_value(&v, i)) continue; + if(v.time < oldest) continue; + if(1 > format_sensval(&v, buf, FULL_LEN+1, N)) continue; + DBG("formatted: '%s'", buf); + sl_sock_sendstrmessage(client, buf); + sl_sock_sendbyte(client, '\n'); + ++nsum; Tsum += v.time; + } + oldest = (time_t)(Tsum / nsum); + if(0 < format_msrmttm(oldest, buf, FULL_LEN+1)){ // send mean measuring time + DBG("Formatted time: '%s'", buf); + sl_sock_sendstrmessage(client, buf); + sl_sock_sendbyte(client, '\n'); + } +} + +// get meteo data +static sl_sock_hresult_e gethandler(sl_sock_t *client, _U_ sl_sock_hitem_t *item, const char *req){ + if(!client) return RESULT_FAIL; + int N = get_nplugins(); + if(N < 1) return RESULT_FAIL; + if(!req) for(int i = 0; i < N; ++i) showdata(client, i, TRUE); + else{ + int n; + if(!sl_str2i(&n, req) || n < 0 || n >= N) return RESULT_BADVAL; + showdata(client, n, FALSE); + } + return RESULT_SILENCE; +} + +// graceful closing socket: let client know that he's told to fuck off +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); + LOGWARN("Client fd=%d tried to connect after MAX reached", fd); +} +// new connections handler (return FALSE to reject client) +static int connected(sl_sock_t *c){ + if(c->type == SOCKT_UNIX) LOGMSG("New local client fd=%d connected", c->fd); + else LOGMSG("New client fd=%d, IP=%s connected", c->fd, c->IP); + return TRUE; +} +// disconnected handler +static void disconnected(sl_sock_t *c){ + if(c->type == SOCKT_UNIX) LOGMSG("Disconnected local client fd=%d", c->fd); + else LOGMSG("Disconnected client fd=%d, IP=%s", c->fd, c->IP); +} +static sl_sock_hresult_e defhandler(struct sl_sock *s, const char *str){ + if(!s || !str) return RESULT_FAIL; + sl_sock_sendstrmessage(s, "You entered wrong command:\n```\n"); + sl_sock_sendstrmessage(s, str); + sl_sock_sendstrmessage(s, "\n```\nTry \"help\"\n"); + return RESULT_SILENCE; +} + +// handlers for network and local (UNIX) sockets +static sl_sock_hitem_t nethandlers[] = { // net - only getters and client-only setters + {gethandler, "get", "get all meteo or only for given plugin number", NULL}, + {listhandler, "list", "show all opened plugins", NULL}, + {timehandler, "time", "get server's UNIX time", NULL}, + {NULL, NULL, NULL, NULL} +}; +static sl_sock_hitem_t localhandlers[] = { // local - full amount of setters/getters + {gethandler, "get", "get all meteo or only for given plugin number", NULL}, + {listhandler, "list", "show all opened plugins", NULL}, + {timehandler, "time", "get server's UNIX time", NULL}, + {NULL, NULL, NULL, NULL} +}; + +#if 0 +// common parsers for both net and local sockets +static void *cmdparser(void *U){ + if(!U) return NULL; + sl_sock_t *s = (sl_sock_t*) U; + while(s && s->connected){ + if(!s->rthread){ + LOGERR("Server's handlers' thread is dead"); + break; + } + } + LOGDBG("cmdparser(): exit"); + return NULL; +} +#endif + +int start_servers(const char *netnode, const char *sockpath){ + if(!netnode || !sockpath){ + LOGERR("start_servers(): need arguments"); + return FALSE; + } + netsocket = sl_sock_run_server(SOCKT_NET, netnode, BUFSIZ, nethandlers); + if(!netsocket){ + LOGERR("start_servers(): can't run network socket"); + return FALSE; + } + localsocket = sl_sock_run_server(SOCKT_UNIX, sockpath, BUFSIZ, localhandlers); + if(!localsocket){ + LOGERR("start_servers(): can't run local socket"); + return FALSE; + } + sl_sock_changemaxclients(netsocket, MAX_CLIENTS); + sl_sock_changemaxclients(localsocket, 1); + sl_sock_maxclhandler(netsocket, toomuch); + sl_sock_maxclhandler(localsocket, toomuch); + sl_sock_connhandler(netsocket, connected); + sl_sock_connhandler(localsocket, connected); + sl_sock_dischandler(netsocket, disconnected); + sl_sock_dischandler(localsocket, disconnected); + sl_sock_defmsghandler(netsocket, defhandler); + sl_sock_defmsghandler(localsocket, defhandler); +#if 0 + if(pthread_create(&netthread, NULL, cmdparser, (void*)netsocket)){ + LOGERR("Can't run server's net thread"); + goto errs; + } + if(pthread_create(&locthread, NULL, cmdparser, (void*)localsocket)){ + LOGERR("Can't run server's local thread"); + goto errs; + } +#endif + return TRUE; +#if 0 +errs: + sl_sock_delete(&localsocket); + sl_sock_delete(&netsocket); + return FALSE; +#endif +} + +void kill_servers(){ + //pthread_cancel(locthread); + //pthread_cancel(netthread); + //LOGMSG("Server threads canceled"); + //usleep(500); + sl_sock_delete(&localsocket); + sl_sock_delete(&netsocket); + LOGMSG("Server sockets destroyed"); + //usleep(500); + //pthread_kill(locthread, 9); + //pthread_kill(netthread, 9); + //LOGMSG("Server threads killed"); +} diff --git a/Daemons/weatherdaemon_multimeteo/server.h b/Daemons/weatherdaemon_multimeteo/server.h new file mode 100644 index 0000000..25a25b8 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/server.h @@ -0,0 +1,25 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +// maximal amount of simultaneous clients connected +#define MAX_CLIENTS (30) + +int start_servers(const char *netnode, const char *sockpath); +void kill_servers(); diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.cflags b/Daemons/weatherdaemon_multimeteo/weatherdaemon.cflags new file mode 100644 index 0000000..68d5165 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.cflags @@ -0,0 +1 @@ +-std=c17 \ No newline at end of file diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.config b/Daemons/weatherdaemon_multimeteo/weatherdaemon.config new file mode 100644 index 0000000..63b475c --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.config @@ -0,0 +1,4 @@ +#define EBUG 1 +#define _GNU_SOURCE +#define _XOPEN_SOURCE 1111 +#define _POSIX_C_SOURCE 200000 diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.creator b/Daemons/weatherdaemon_multimeteo/weatherdaemon.creator new file mode 100644 index 0000000..e94cbbd --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.creator @@ -0,0 +1 @@ +[General] diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.cxxflags b/Daemons/weatherdaemon_multimeteo/weatherdaemon.cxxflags new file mode 100644 index 0000000..6435dfc --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.cxxflags @@ -0,0 +1 @@ +-std=c++17 \ No newline at end of file diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.files b/Daemons/weatherdaemon_multimeteo/weatherdaemon.files new file mode 100644 index 0000000..fce6b66 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.files @@ -0,0 +1,9 @@ +cmdlnopts.c +cmdlnopts.h +main.c +plugins/dummy.c +sensors.c +sensors.h +server.c +server.h +weathlib.h diff --git a/Daemons/weatherdaemon_multimeteo/weatherdaemon.includes b/Daemons/weatherdaemon_multimeteo/weatherdaemon.includes new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weatherdaemon.includes @@ -0,0 +1 @@ +. diff --git a/Daemons/weatherdaemon_multimeteo/weathlib.h b/Daemons/weatherdaemon_multimeteo/weathlib.h new file mode 100644 index 0000000..0d76b57 --- /dev/null +++ b/Daemons/weatherdaemon_multimeteo/weathlib.h @@ -0,0 +1,92 @@ +/* + * This file is part of the weatherdaemon project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +// length (in symbols) of key, value and comment +#define KEY_LEN (8) +#define VAL_LEN (31) +#define COMMENT_LEN (63) +// maximal full length of "KEY=val / comment" (as for sfitsio) +#define FULL_LEN (81) +// name of meteo-plugin +#define NAME_LEN (31) + +// importance of values +typedef enum{ + VAL_OBLIGATORY, // can't be omitted + VAL_RECOMMENDED, // recommended to show + VAL_UNNECESSARY, // may be shown by user request + VAL_BROKEN // sensor is broken, omit it +} valsense_t; + +// meaning of values +typedef enum{ + IS_WIND, // wind, m/s + IS_WINDDIR, // wind direction, degr + IS_HUMIDITY, // humidity, percent + IS_AMB_TEMP, // ambient temperature, degC + IS_INNER_TEMP, // in-dome temperature, degC + IS_HW_TEMP, // hardware (?) termperature, degC + IS_PRESSURE, // atmospheric pressure, mmHg + IS_PRECIP, // precipitation (1 - yes, 0 - no) + IS_PRECIP_LEVEL, // precipitation level (mm) + IS_MIST, // mist (1 - yes, 0 - no) + IS_CLOUDS, // integral clouds value (bigger - better) + IS_SKYTEMP, // mean sky temperatyre + IS_OTHER // something other - read "name" and "comment" +} valmeaning_t; + +typedef union{ + uint32_t u; + int32_t i; + float f; +} num_t; + +// type of value +typedef enum{ + VALT_UINT, + VALT_INT, + VALT_FLOAT, + //VALT_STRING, +} valtype_t; + +// value +typedef struct{ + char name[KEY_LEN+1]; // max VAL_LEN symbols FITS header keyword name + char comment[COMMENT_LEN+1];// max COMMENT_LEN symbols of comment to FITS header + valsense_t sense; // importance + valtype_t type; // type of given value + valmeaning_t meaning; // what type of sensor is it + num_t value; // value itself + time_t time; // last changing time +} val_t; + +// all sensor's data +typedef struct sensordata_t{ + char name[NAME_LEN+1]; // max 31 symbol of sensor's name (e.g. "rain sensor") + int Nvalues; // amount of values + int PluginNo; // plugin number in array (if several) + int (*init)(int); // init meteostation with given PluginNo; return amount of parameters found + int (*onrefresh)(void (*handler)(const struct sensordata_t* const)); // handler of new data; return TRUE if OK + int (*get_value)(val_t *, int); // getter of Nth value + void (*die)(); // close everything and die +} sensordata_t;