Compare commits

..

4 Commits

Author SHA1 Message Date
fa305a7cc0 fix encoders 2026-03-11 10:05:02 +03:00
205a190820 some fixes, sill have troubles with dummy socket/fd 2026-03-06 12:20:10 +03:00
7c2aaf1cb0 add serial/socket plugin example 2026-02-27 18:31:22 +03:00
54778dcf9a add pre-pre-alpha 2026-02-27 12:05:40 +03:00
23 changed files with 1667 additions and 59 deletions

View File

@@ -20,6 +20,7 @@
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
@@ -64,14 +65,12 @@ void getXspeed(){
ls = LS_init(Conf.EncoderSpeedInterval / Conf.EncoderReqInterval);
if(!ls) return;
}
pthread_mutex_lock(&datamutex);
double dt = timediff0(&mountdata.encXposition.t);
double speed = LS_calc_slope(ls, mountdata.encXposition.val, dt);
if(fabs(speed) < 1.5 * Xlimits.max.speed){
mountdata.encXspeed.val = speed;
mountdata.encXspeed.t = mountdata.encXposition.t;
}
pthread_mutex_unlock(&datamutex);
//DBG("Xspeed=%g", mountdata.encXspeed.val);
}
void getYspeed(){
@@ -80,14 +79,12 @@ void getYspeed(){
ls = LS_init(Conf.EncoderSpeedInterval / Conf.EncoderReqInterval);
if(!ls) return;
}
pthread_mutex_lock(&datamutex);
double dt = timediff0(&mountdata.encYposition.t);
double speed = LS_calc_slope(ls, mountdata.encYposition.val, dt);
if(fabs(speed) < 1.5 * Ylimits.max.speed){
mountdata.encYspeed.val = speed;
mountdata.encYspeed.t = mountdata.encYposition.t;
}
pthread_mutex_unlock(&datamutex);
}
/**
@@ -136,13 +133,12 @@ static void parse_encbuf(uint8_t databuf[ENC_DATALEN], struct timespec *t){
DBG("Got positions X/Y= %.6g / %.6g", mountdata.encXposition.val, mountdata.encYposition.val);
mountdata.encXposition.t = *t;
mountdata.encYposition.t = *t;
pthread_mutex_unlock(&datamutex);
//if(t - lastXenc.t > Conf.EncoderSpeedInterval) getXspeed();
//if(t - lastYenc.t > Conf.EncoderSpeedInterval) getYspeed();
getXspeed(); getYspeed();
pthread_mutex_unlock(&datamutex);
//DBG("time = %zd+%zd/1e6, X=%g deg, Y=%g deg", tv->tv_sec, tv->tv_usec, mountdata.encposition.X*180./M_PI, mountdata.encposition.Y*180./M_PI);
}
#if 0
/**
* @brief getencval - get uint64_t data from encoder
* @param fd - encoder fd
@@ -211,6 +207,8 @@ static int getencval(int fd, double *val, struct timespec *t){
if(t){ if(!curtime(t)){ DBG("Can't get time"); return 0; }}
return got;
}
#endif
// try to read 1 byte from encoder; return -1 if nothing to read or -2 if device seems to be disconnected
static int getencbyte(){
if(encfd[0] < 0) return -1;
@@ -322,65 +320,130 @@ static void *encoderthread1(void _U_ *u){
return NULL;
}
#define XYBUFSZ (128)
typedef struct{
char buf[XYBUFSZ+1];
int len;
} buf_t;
// write to buffer next data portion; return FALSE in case of error
static int readstrings(buf_t *buf, int fd){
FNAME();
if(!buf){DBG("Empty buffer"); return FALSE;}
int L = XYBUFSZ - buf->len;
if(L == 0){
DBG("buffer overfull: %d!", buf->len);
char *lastn = strrchr(buf->buf, '\n');
if(lastn){
fprintf(stderr, "BUFOVR: _%s_", buf->buf);
++lastn;
buf->len = XYBUFSZ - (lastn - buf->buf);
DBG("Memmove %d", buf->len);
memmove(lastn, buf->buf, buf->len);
buf->buf[buf->len] = 0;
}else buf->len = 0;
L = XYBUFSZ - buf->len;
}
int got = read(fd, &buf->buf[buf->len], L);
if(got < 0){
DBG("read()");
return FALSE;
}else if(got == 0){ DBG("NO data"); return TRUE; }
buf->len += got;
buf->buf[buf->len] = 0;
DBG("buf[%d]: %s", buf->len, buf->buf);
return TRUE;
}
// return TRUE if got, FALSE if no data found
static int getdata(buf_t *buf, long *out){
if(!buf || buf->len < 1) return FALSE;
// read record between last '\n' and previous (or start of string)
char *last = &buf->buf[buf->len - 1];
//DBG("buf: _%s_", buf->buf);
if(*last != '\n') return FALSE;
*last = 0;
//DBG("buf: _%s_", buf->buf);
char *prev = strrchr(buf->buf, '\n');
if(!prev) prev = buf->buf;
else{
fprintf(stderr, "MORETHANONE: _%s_", buf->buf);
++prev; // after last '\n'
}
if(out) *out = atol(prev);
// clear buffer
buf->len = 0;
return TRUE;
}
// try to write '\n' asking new data portion; return FALSE if failed
static int asknext(int fd){
FNAME();
if(fd < 0) return FALSE;
int i = 0;
for(; i < 5; ++i){
int l = write(fd, "\n", 1);
//DBG("l=%d", l);
if(1 == l) return TRUE;
usleep(100);
}
DBG("5 tries... failed!");
return FALSE;
}
// main encoder thread for separate encoders as USB devices /dev/encoder_X0 and /dev/encoder_Y0
static void *encoderthread2(void _U_ *u){
if(Conf.SepEncoder != 2) return NULL;
DBG("Thread started");
struct pollfd pfds[2];
pfds[0].fd = encfd[0]; pfds[0].events = POLLIN;
pfds[1].fd = encfd[1]; pfds[1].events = POLLIN;
double t0[2], tstart;
buf_t strbuf[2];
long msrlast[2]; // last encoder data
double mtlast[2]; // last measurement time
asknext(encfd[0]); asknext(encfd[1]);
t0[0] = t0[1] = tstart = timefromstart();
int errctr = 0;
double t0 = timefromstart();
const char *req = "\n";
int need2ask = 0; // need or not to ask encoder for new data
while(encfd[0] > -1 && encfd[1] > -1 && errctr < MAX_ERR_CTR){
struct timespec t;
if(need2ask){
//DBG("ASK for new data, tfs=%.6g", timefromstart());
if(1 != write(encfd[0], req, 1)) { ++errctr; continue; }
else if(1 != write(encfd[1], req, 1)) { ++errctr; continue; }
//DBG("OK");
need2ask = 0;
usleep(100);
do{ // main cycle
if(poll(pfds, 2, 0) < 0){
DBG("poll()");
break;
}
if(!curtime(&t)){
DBG("Where is time?");
continue;
}
double v;
if(getencval(encfd[0], &v, &t)){
pthread_mutex_lock(&datamutex);
mountdata.encXposition.val = Xenc2rad(v);
mountdata.encXposition.t = t;
pthread_mutex_unlock(&datamutex);
//DBG("MDXpos: %.10g/%g (xez=%d, xesr=%.10g), tfs=%.6g", v, mountdata.encXposition.val, X_ENC_ZERO, X_ENC_STEPSPERREV, timefromstart());
getXspeed();
if(getencval(encfd[1], &v, &t)){
pthread_mutex_lock(&datamutex);
mountdata.encYposition.val = Yenc2rad(v);
mountdata.encYposition.t = t;
pthread_mutex_unlock(&datamutex);
//DBG("MDYpos: %.10g/%g (yez=%d, yesr=%.10g)", v, mountdata.encYposition.val, Y_ENC_ZERO, Y_ENC_STEPSPERREV);
getYspeed();
errctr = 0;
need2ask = 0;
//DBG("tgot=%.6g", timefromstart());
while(timefromstart() - t0 < Conf.EncoderReqInterval){ usleep(50); }
t0 = timefromstart();
}else{
DBG("NO Y DATA!!!");
/*if(need2ask) ++errctr;
else need2ask = 1;
continue;*/
++errctr;
need2ask = 1;
int got = 0;
for(int i = 0; i < 2; ++i){
if(pfds[i].revents && POLLIN){
if(!readstrings(&strbuf[i], encfd[i])){
++errctr;
break;
}
}
double curt = timefromstart();
if(getdata(&strbuf[i], &msrlast[i])) mtlast[i] = curt;
if(curt - t0[i] >= Conf.EncoderReqInterval){ // get last records
if(curt - mtlast[i] < 1.5*Conf.EncoderReqInterval){
pthread_mutex_lock(&datamutex);
if(i == 0){
mountdata.encXposition.val = Xenc2rad((double)msrlast[i]);
curtime(&mountdata.encXposition.t);
getXspeed();
}else{
mountdata.encYposition.val = Yenc2rad((double)msrlast[i]);
curtime(&mountdata.encYposition.t);
getYspeed();
}
pthread_mutex_unlock(&datamutex);
}
if(!asknext(encfd[i])){
++errctr;
break;
}
t0[i] = (curt - t0[i] < 2.*Conf.EncoderReqInterval) ? t0[i] + Conf.EncoderReqInterval : curt;
++got;
}
}else{ // no data - ask for new
//DBG("Need new, tfs=%.6g", timefromstart());
/*if(need2ask) ++errctr;
else need2ask = 1;
continue;*/
++errctr;
need2ask = 1;
}
}
if(got == 2) errctr = 0;
}while(encfd[0] > -1 && encfd[1] > -1 && errctr < MAX_ERR_CTR);
DBG("\n\nEXIT: ERRCTR=%d", errctr);
for(int i = 0; i < 2; ++i){
if(encfd[i] > -1){
@@ -463,8 +526,8 @@ static void *mountthread(void _U_ *u){
}else mountdata.millis = oldmillis;
chkModStopped(&Xprev, c.X, &xcnt, &mountdata.Xstate);
chkModStopped(&Yprev, c.Y, &ycnt, &mountdata.Ystate);
pthread_mutex_unlock(&datamutex);
getXspeed(); getYspeed();
pthread_mutex_unlock(&datamutex);
while(timefromstart() - t0 < Conf.EncoderReqInterval) usleep(50);
t0 = timefromstart();
}

View File

@@ -0,0 +1,69 @@
cmake_minimum_required(VERSION 4.0)
set(PROJ weatherdaemon)
set(PROJLIB senslib)
set(MAJOR_VERSION "0")
set(MID_VERSION "0")
set(MINOR_VERSION "1")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SOURCES)
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)
option(FDEXAMPLE "Example of file descriptor plugin" ON)
# default flags
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -W -Wextra -pedantic-errors -fPIC")
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.5)
#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()
# static lib for sensors
set(LIBSRC "weathlib.c")
set(LIBHEADER "weathlib.h")
add_library(${PROJLIB} STATIC ${LIBSRC})
set_target_properties(${PROJLIB} PROPERTIES VERSION ${VERSION})
# exe & deps files
add_executable(${PROJ} ${SOURCES})
target_link_libraries(${PROJ} ${${PROJ}_LIBRARIES} ${PROJLIB} -lm ${CMAKE_DL_LIBS})
target_include_directories(${PROJ} PUBLIC ${${PROJ}_INCLUDE_DIRS} .)
target_link_directories(${PROJ} PUBLIC ${${PROJ}_LIBRARY_DIRS} )
include(GNUInstallDirs)
# Installation of the program
install(TARGETS ${PROJ} DESTINATION "bin")
add_subdirectory("plugins")

View File

@@ -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

View File

@@ -0,0 +1,164 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <usefull_macros.h>
#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); FORMAT: \"dlpath:l:dev\", where `dlpath` - path of plugin library; `l` - 'D' for device, 'U' for UNIX-socket or 'N' for INET socket; dev - path to device and speed (like /dev/ttyS0:9600), UNIX socket name or host:port for INET"}, \
{"pollt", NEED_ARG, NULL, 'T', arg_int, APTR(&G.pollt), "set maximal polling interval (seconds, integer)"},
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:D:/dev/ttyS0:115200)"},
{"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;
}

View File

@@ -0,0 +1,37 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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
int pollt; // sensors maximal polling interval
} glob_pars;
glob_pars *parse_args(int argc, char **argv);

View File

@@ -0,0 +1,152 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2026 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
// try to open device or socket that user pointed for plugin
// WARNING!!! The `getFD` function intended for single use for each plugin!
// WARNING!!! If you will try to close some plugins in running mode and open others, it
// WARNING!!! would cause to a memory leak!
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/un.h> // unix socket
#include <usefull_macros.h>
#include "fd.h"
/**
* @brief openserial - try to open serial device
* @param path - path to device and speed, colon-separated (without given speed assume 9600)
* @return -1 if failed or opened FD
* WARNING!!! Memory leakage danger. Don't call this function too much times!
*/
static int openserial(char *path){
FNAME();
int speed = 9600; // default speed
char *colon = strchr(path, ':');
if(colon){
*colon++ = 0;
if(!sl_str2i(&speed, colon)){
WARNX("Wrong speed settings: '%s'", colon);
return -1;
}
}
sl_tty_t *serial = sl_tty_new(path, speed, BUFSIZ);
if(!serial || !sl_tty_open(serial, TRUE)){
WARN("Can't open %s @ speed %d", path, speed);
return -1;
}
return serial->comfd;
}
static char *convunsname(const char *path){
char *apath = MALLOC(char, 106);
if(*path == 0 || *path == '@'){
DBG("convert name starting from 0 or @");
apath[0] = 0;
strncpy(apath+1, path+1, 104);
}else if(strncmp("\\0", path, 2) == 0){
DBG("convert name starting from \\0");
apath[0] = 0;
strncpy(apath+1, path+2, 104);
}else strncpy(apath, path, 105);
return apath;
}
/**
* @brief opensocket - try to open socket
* @param sock - UNIX socket path or hostname:port for INET socket
* @param type - UNIX or INET
* @return -1 if failed or opened FD
*/
static int opensocket(char *path, sl_socktype_e type){
FNAME();
DBG("path: '%s'", path);
int sock = -1;
struct addrinfo ai = {0}, *res = &ai;
struct sockaddr_un unaddr = {0};
char *node = path, *service = NULL;
ai.ai_socktype = 0; // try to get socket type from `getaddrinfo`
switch(type){
case SOCKT_UNIX:
{
char *str = convunsname(path);
if(!str) return -1;
unaddr.sun_family = AF_UNIX;
ai.ai_addr = (struct sockaddr*) &unaddr;
ai.ai_addrlen = sizeof(unaddr);
memcpy(unaddr.sun_path, str, 106);
FREE(str);
ai.ai_family = AF_UNIX;
}
break;
case SOCKT_NET:
case SOCKT_NETLOCAL:
ai.ai_family = AF_INET;
char *delim = strchr(path, ':');
if(delim){
*delim = 0;
service = delim+1;
if(delim == path) node = NULL; // only port
}
DBG("node: '%s', service: '%s'", node, service);
int e = getaddrinfo(node, service, &ai, &res);
if(e){
WARNX("getaddrinfo(): %s", gai_strerror(e));
return -1;
}
for(struct addrinfo *p = res; p; p = p->ai_next){
if((sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) continue;
DBG("Try proto %d, type %d", p->ai_protocol, p->ai_socktype);
if(connect(sock, p->ai_addr, p->ai_addrlen) == -1){
WARN("connect()");
close(sock); sock = -1;
} else break;
}
break;
default: // never reached
WARNX("Unsupported socket type %d", type);
return -1;
}
DBG("FD: %d", sock);
return sock;
}
/**
* @brief getFD - try to open given device/socket
* @param path - rest of string for --plugin= (e.g. "N:host.com:12345")
* WARNING!!! Contents of `path` would be modified in this function!
* @return opened file descriptor or -1 in case of error
*/
int getFD(char *path){
if(!path || !*path) return -1;
char type = *path;
path += 2;
switch(type){
case 'D': // serial device
return openserial(path);
case 'N': // INET socket
return opensocket(path, SOCKT_NET);
case 'U': // UNIX socket
return opensocket(path, SOCKT_UNIX);
}
WARNX("Wrong plugin format: '%c', should be 'D', 'N' or 'U'", type);
return -1;
}

View File

@@ -0,0 +1,21 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2026 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
int getFD(char *path);

View File

@@ -0,0 +1,102 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h> // wait
#include <sys/prctl.h> //prctl
#include <usefull_macros.h>
#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);
}
static void getpipe(int _U_ signo){
WARNX("Get sigpipe!");
// TODO: check all sensors for disconnected one
signal(SIGPIPE, getpipe);
}
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
signal(SIGPIPE, getpipe); // socket disconnected
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");
}
if(GP->pollt > 0){
if(!set_pollT((time_t)GP->pollt)) ERRX("Can't set polling time to %d seconds", GP->pollt);
}
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
}

View File

@@ -0,0 +1,24 @@
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(${PLUGINS_LIBRARY_DIRS} ..)
link_libraries(${$PLUGINS_LIBRARIES} ${PROJLIB} -fPIC)
set(LIBS "")
if(DUMMY)
add_library(wsdummy SHARED dummy.c)
list(APPEND LIBS wsdummy)
endif()
if(FDEXAMPLE)
add_library(fdex SHARED fdexample.c)
list(APPEND LIBS fdex)
endif()
install(TARGETS ${LIBS} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})

View File

@@ -0,0 +1,95 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
// dummy meteostation sending data each `tpoll` seconds
#include "weathlib.h"
#define NS (6)
extern sensordata_t sensor;
static const val_t values[NS] = { // fields `name` and `comment` have no sense until value meaning is `IS_OTHER`
{.sense = VAL_OBLIGATORY, .type = VALT_FLOAT, .meaning = IS_WIND},
{.sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_WINDDIR},
{.sense = VAL_OBLIGATORY, .type = VALT_FLOAT, .meaning = IS_AMB_TEMP},
{.sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_PRESSURE},
{.sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_HUMIDITY},
{.sense = VAL_OBLIGATORY, .type = VALT_UINT, .meaning = IS_PRECIP},
};
static void *mainthread(void _U_ *U){
FNAME();
double t0 = sl_dtime();
while(1){
float f = sensor.values[0].value.f + (drand48() - 0.5) / 2.;
if(f >= 0.) sensor.values[0].value.f = f;
f = sensor.values[1].value.f + (drand48() - 0.5) * 4.;
if(f > 160. && f < 200.) sensor.values[1].value.f = f;
f = sensor.values[2].value.f + (drand48() - 0.5) / 20.;
if(f > 13. && f < 21.) sensor.values[2].value.f = f;
f = sensor.values[3].value.f + (drand48() - 0.5) / 100.;
if(f > 585. && f < 615.) sensor.values[3].value.f = f;
f = sensor.values[4].value.f + (drand48() - 0.5) / 10.;
if(f > 60. && f <= 100.) sensor.values[4].value.f = f;
sensor.values[5].value.u = (f > 98.) ? 1 : 0;
time_t cur = time(NULL);
for(int i = 0; i < NS; ++i) sensor.values[i].time = cur;
if(sensor.freshdatahandler) sensor.freshdatahandler(&sensor);
while(sl_dtime() - t0 < sensor.tpoll) usleep(500);
t0 = sl_dtime();
}
return NULL;
}
static int init(struct sensordata_t* s, int N, time_t pollt, int _U_ fd){
FNAME();
if(pthread_create(&s->thread, NULL, mainthread, NULL)) return 0;
if(pollt) s->tpoll = pollt;
s->values = MALLOC(val_t, NS);
for(int i = 0; i < NS; ++i) s->values[i] = values[i];
sensor.values[0].value.f = 1.;
sensor.values[1].value.f = 180.;
sensor.values[2].value.f = 17.;
sensor.values[3].value.f = 600.;
sensor.values[4].value.f = 80.;
sensor.values[5].value.u = 0;
sensor.PluginNo = N;
return NS;
}
/**
* @brief getval - value's getter
* @param o (o) - value
* @param N - it's index
* @return FALSE if failed
*/
static int getval(struct sensordata_t* s, val_t *o, int N){
if(N < 0 || N >= NS) return FALSE;
if(o) *o = s->values[N];
return TRUE;
}
sensordata_t sensor = {
.name = "Dummy weatherstation",
.Nvalues = NS,
.init = init,
.onrefresh = common_onrefresh,
.get_value = getval,
.kill = common_kill,
};

View File

@@ -0,0 +1,151 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2026 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <time.h>
#include "weathlib.h"
// dummy example of file descriptors usage
#define NS (4)
extern sensordata_t sensor;
static const val_t values[NS] = { // fields `name` and `comment` have no sense until value meaning is `IS_OTHER`
{.sense = VAL_OBLIGATORY, .type = VALT_FLOAT, .meaning = IS_WIND},
{.sense = VAL_OBLIGATORY, .type = VALT_FLOAT, .meaning = IS_AMB_TEMP},
{.sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_PRESSURE},
{.sense = VAL_RECOMMENDED, .type = VALT_FLOAT, .meaning = IS_HUMIDITY},
};
static int format_values(char *buf){
int gotvals = 0;
char *token = strtok(buf, ",");
time_t tnow = time(NULL);
while(token && gotvals < NS){
double v;
DBG("TOKEN: %s", token);
if(sl_str2d(&v, token)){
DBG("next value: %g", v);
sensor.values[gotvals].value.f = (float) v;
sensor.values[gotvals].time = tnow;
++gotvals;
}
token = strtok(NULL, ",");
}
DBG("GOT: %d", gotvals);
return gotvals;
}
static ssize_t writedata(int fd, const char *str, size_t size){
ssize_t sent = 0;
do{
DBG("try to write %zd bytes", size);
int canwrite = sl_canwrite(fd);
if(canwrite < 0){
WARNX("Disconnected?!");
return -1;
}else if(canwrite){
ssize_t r = write(fd, str+sent, size);
if(r < 0){
sent = -1;
WARNX("Disconnected??");
break;
}else{
sent += r;
size -= r;
}
DBG("sent %zd bytes; total send %zd, leave %zd", r, sent, size);
}
}while(size);
return sent;
}
static void *mainthread(void _U_ *U){
FNAME();
time_t task = 0;
const char begging[] = "Enter comma-separated data: wind, exttemp, pressure, humidity\n";
char buf[128];
while(sensor.fdes > -1){
time_t tnow = time(NULL);
int canread = sl_canread(sensor.fdes);
if(canread < 0){
WARNX("Disconnected fd %d", sensor.fdes);
break;
}else if(canread == 1){
ssize_t got = read(sensor.fdes, buf, 128);
if(got > 0){
sl_RB_write(sensor.ringbuffer, (uint8_t*)buf, got);
}else if(got < 0){
DBG("Disconnected?");
break;
}
}
if(sl_RB_readline(sensor.ringbuffer, buf, 127) > 0){
if(NS == format_values(buf) && sensor.freshdatahandler)
sensor.freshdatahandler(&sensor);
}
if(sensor.tpoll){
if(tnow >= task){
DBG("write %s", begging);
ssize_t got = writedata(sensor.fdes, begging, sizeof(begging)-1);
if(got > 0) task = tnow + sensor.tpoll;
else if(got < 0){
close(sensor.fdes);
sensor.fdes = -1;
}
}
}
}
DBG("OOOOps!");
return NULL;
}
static int init(struct sensordata_t *s, int N, time_t pollt, int fd){
FNAME();
if(!s) return -1;
s->fdes = fd;
if(s->fdes < 0) return -1;
sensor.PluginNo = N;
if(pollt) s->tpoll = pollt;
if(pthread_create(&s->thread, NULL, mainthread, NULL)) return -1;
s->values = MALLOC(val_t, NS);
// don't use memcpy, as `values` could be aligned
for(int i = 0; i < NS; ++i) s->values[i] = values[i];
if(!(s->ringbuffer = sl_RB_new(BUFSIZ))){
WARNX("Can't init ringbuffer!");
return -1;
}
return NS;
}
static int getval(struct sensordata_t *s, val_t *o, int N){
if(!s || N < 0 || N >= NS) return FALSE;
if(o) *o = s->values[N];
return TRUE;
}
sensordata_t sensor = {
.name = "Dummy socket or serial device weatherstation",
.Nvalues = NS,
.init = init,
.onrefresh = common_onrefresh,
.get_value = getval,
.kill = common_kill,
};

View File

@@ -0,0 +1,226 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <dlfcn.h>
#include <string.h>
#include <usefull_macros.h>
#include "fd.h"
#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)
// poll each `poll_interval` seconds
static time_t poll_interval = 15;
static int nplugins = 0;
static sensordata_t **allplugins = NULL;
int get_nplugins(){
return nplugins;
}
// set polling interval
int set_pollT(time_t t){
if(t == 0 || t > MAX_POLLT) return FALSE;
poll_interval = t;
return TRUE;
}
/**
* @brief get_plugin - get link to opened plugin
* @param o (o) - plugin with given index
* @param N - index in `allplugins`
* @return NULL if failed or pointer
*/
sensordata_t *get_plugin(int N){
if(N < 0 || N >= nplugins) return NULL;
return allplugins[N];
}
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(struct sensordata_t* station){
FNAME();
if(!station || !station->get_value || station->Nvalues < 1) return;
char buf[FULL_LEN+1];
uint64_t Tsum = 0; int nsum = 0;
int N = (nplugins > 1) ? station->PluginNo : -1;
for(int i = 0; i < station->Nvalues; ++i){
val_t v;
if(!station->get_value(station, &v, i)) continue;
if(0 < format_sensval(&v, buf, FULL_LEN+1, N)){
printf("%s\n", buf);
++nsum; Tsum += v.time;
}
}
time_t last = (time_t)(Tsum / nsum);
if(0 < format_msrmttm(last, buf, FULL_LEN+1)){
printf("%s\n\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){
char buf[PATH_MAX+1];
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]);
snprintf(buf, PATH_MAX, "%s", paths[i]);
char *colon = strchr(buf, ':');
if(colon) *colon++ = 0;
void* dlh = open_plugin(buf);
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 fd = -1;
if(colon) fd = getFD(colon);
int ns = S->init(S, nplugins, poll_interval, fd); // here nplugins is index in array
if(ns < 1) WARNXL("Can't init plugin %s", paths[i]);
else{
#ifdef EBUG
if(!S->onrefresh(S, 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]->kill) allplugins[i]->kill(allplugins[i]);
}
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);
}

View File

@@ -0,0 +1,32 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "weathlib.h"
// don't allow to set polling time more than 10 minutes
#define MAX_POLLT (600)
int openplugins(char **paths, int N);
void closeplugins();
sensordata_t *get_plugin(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);
int set_pollT(time_t t);

View File

@@ -0,0 +1,229 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <signal.h>
#include <string.h>
#include <usefull_macros.h>
#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[256];
int N = get_nplugins();
if(N < 1) return RESULT_FAIL;
sensordata_t *d = NULL;
for(int i = 0; i < N; ++i){
if(!(d = get_plugin(i))) continue;
snprintf(buf, 255, "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 = NULL;
if(!(s = get_plugin(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(s, &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");
}

View File

@@ -0,0 +1,25 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
// maximal amount of simultaneous clients connected
#define MAX_CLIENTS (30)
int start_servers(const char *netnode, const char *sockpath);
void kill_servers();

View File

@@ -0,0 +1 @@
-std=c17

View File

@@ -0,0 +1,4 @@
#define EBUG 1
#define _GNU_SOURCE
#define _XOPEN_SOURCE 1111
#define _POSIX_C_SOURCE 200000

View File

@@ -0,0 +1 @@
[General]

View File

@@ -0,0 +1 @@
-std=c++17

View File

@@ -0,0 +1,14 @@
CMakeLists.txt
cmdlnopts.c
cmdlnopts.h
fd.c
fd.h
main.c
plugins/dummy.c
plugins/fdexample.c
sensors.c
sensors.h
server.c
server.h
weathlib.c
weathlib.h

View File

@@ -0,0 +1 @@
.

View File

@@ -0,0 +1,66 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2026 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
// Some common functions and handlers for sensors
#include "weathlib.h"
/**
* @brief sensor_alive - test if sensor's thread isn't dead
* @param s - sensor
* @return FALSE if thread is dead
*/
int sensor_alive(sensordata_t *s){
if(!s) return FALSE;
if(pthread_kill(s->thread, 0)) return FALSE;
return TRUE;
}
/**
* @brief common_onrefresh - common `change onrefresh handler`
* @param s - sensor
* @return FALSE if failed
*/
int common_onrefresh(sensordata_t *s, void (*handler)(sensordata_t *)){
FNAME();
if(!s || !handler) return FALSE;
s->freshdatahandler = handler;
return TRUE;
}
/**
* @brief common_kill - common `die` function
* @param s - sensor
*/
void common_kill(sensordata_t *s){
FNAME();
if(!s) return;
if(0 == pthread_kill(s->thread, -9)){
DBG("%s main thread killed, join", s->name);
pthread_join(s->thread, NULL);
DBG("Done");
}
DBG("Delete RB");
sl_RB_delete(&s->ringbuffer);
if(s->fdes > -1){
close(s->fdes);
DBG("FD closed");
}
FREE(s->values);
DBG("Sensor %s killed", s->name);
}

View File

@@ -0,0 +1,108 @@
/*
* This file is part of the weatherdaemon project.
* Copyright 2025 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <pthread.h>
#include <signal.h> // pthread_kill
#include <stdint.h>
#include <time.h>
#include <usefull_macros.h>
// 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 (127)
// 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
// all functions have `this` as first arg
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)(struct sensordata_t*, int, time_t, int); // init meteostation with given PluginNo, poll_interval and fd; return amount of parameters found or -1 if error
int (*onrefresh)(struct sensordata_t*, void (*handler)(struct sensordata_t*)); // handler of new data; return TRUE if OK
int (*get_value)(struct sensordata_t*, val_t*, int); // getter of Nth value
void (*kill)(struct sensordata_t*); // close everything and remove sensor
// private members:
val_t *values; // array of values
pthread_t thread; // main thread
void (*freshdatahandler)(struct sensordata_t*); // handler of fresh data
int fdes; // file descriptor of device/socket
sl_ringbuffer_t *ringbuffer; // ringbuffer for device reading
time_t tpoll; // forced polling time for sensor
} sensordata_t;
// library functions and other
int common_onrefresh(sensordata_t*, void (*handler)(sensordata_t*));
void common_kill(sensordata_t *s);
int sensor_alive(sensordata_t *s);