From 1a3dba4676c62520bbcd71b9236a18597fbb8464 Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Mon, 26 May 2025 17:00:27 +0300 Subject: [PATCH] add simplest web interface --- modbus_relay/web_management/index.html | 96 +++++ .../web_management/relay_daemon/Makefile | 59 +++ .../web_management/relay_daemon/cmdlnopts.c | 83 ++++ .../web_management/relay_daemon/cmdlnopts.h | 42 +++ .../web_management/relay_daemon/main.c | 134 +++++++ .../web_management/relay_daemon/modbus_relay | Bin 0 -> 26648 bytes .../relay_daemon/modbus_relay.cflags | 1 + .../relay_daemon/modbus_relay.config | 3 + .../relay_daemon/modbus_relay.creator | 1 + .../relay_daemon/modbus_relay.creator.user | 184 +++++++++ .../relay_daemon/modbus_relay.cxxflags | 1 + .../relay_daemon/modbus_relay.files | 5 + .../relay_daemon/modbus_relay.includes | 0 .../web_management/relay_daemon/server.c | 354 ++++++++++++++++++ .../web_management/relay_daemon/server.h | 29 ++ 15 files changed, 992 insertions(+) create mode 100644 modbus_relay/web_management/index.html create mode 100644 modbus_relay/web_management/relay_daemon/Makefile create mode 100644 modbus_relay/web_management/relay_daemon/cmdlnopts.c create mode 100644 modbus_relay/web_management/relay_daemon/cmdlnopts.h create mode 100644 modbus_relay/web_management/relay_daemon/main.c create mode 100755 modbus_relay/web_management/relay_daemon/modbus_relay create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.cflags create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.config create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.creator create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.creator.user create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.cxxflags create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.files create mode 100644 modbus_relay/web_management/relay_daemon/modbus_relay.includes create mode 100644 modbus_relay/web_management/relay_daemon/server.c create mode 100644 modbus_relay/web_management/relay_daemon/server.h diff --git a/modbus_relay/web_management/index.html b/modbus_relay/web_management/index.html new file mode 100644 index 0000000..13eabab --- /dev/null +++ b/modbus_relay/web_management/index.html @@ -0,0 +1,96 @@ + + + + Relay Control + + + +
+ + + + +
Left
+
Right
+
+ + + + \ No newline at end of file diff --git a/modbus_relay/web_management/relay_daemon/Makefile b/modbus_relay/web_management/relay_daemon/Makefile new file mode 100644 index 0000000..f1704a3 --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/Makefile @@ -0,0 +1,59 @@ +# run `make DEF=...` to add extra defines +PROGRAM := modbus_relay +LDFLAGS := -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,--discard-all +LDFLAGS += -lusefull_macros -lmodbus +SRCS := $(wildcard *.c) +DEFINES := $(DEF) -D_GNU_SOURCE -D_XOPEN_SOURCE=1111 +OBJDIR := mk +CFLAGS += -O2 -Wall -Wextra -Wno-trampolines +OBJS := $(addprefix $(OBJDIR)/, $(SRCS:%.c=%.o)) +DEPS := $(OBJS:.o=.d) +TARGFILE := $(OBJDIR)/TARGET +CC = gcc +#TARGET := RELEASE + +ifeq ($(shell test -e $(TARGFILE) && echo -n yes),yes) + TARGET := $(file < $(TARGFILE)) +else + TARGET := RELEASE +endif + +ifeq ($(TARGET), DEBUG) + .DEFAULT_GOAL := debug +endif + +release: CFLAGS += -flto +release: LDFLAGS += -flto +release: $(PROGRAM) + +debug: CFLAGS += -DEBUG -Werror +debug: TARGET := DEBUG +debug: $(PROGRAM) + +$(TARGFILE): $(OBJDIR) + @echo -e "\t\tTARGET: $(TARGET)" + @echo "$(TARGET)" > $(TARGFILE) + +$(PROGRAM) : $(TARGFILE) $(OBJS) + @echo -e "\t\tLD $(PROGRAM)" + $(CC) $(LDFLAGS) $(OBJS) -o $(PROGRAM) + +$(OBJDIR): + @mkdir $(OBJDIR) + +ifneq ($(MAKECMDGOALS),clean) +-include $(DEPS) +endif + +$(OBJDIR)/%.o: %.c + @echo -e "\t\tCC $<" + $(CC) -MD -c $(LDFLAGS) $(CFLAGS) $(DEFINES) -o $@ $< + +clean: + @echo -e "\t\tCLEAN" + @rm -rf $(OBJDIR) 2>/dev/null || true + +xclean: clean + @rm -f $(PROGRAM) + +.PHONY: clean xclean diff --git a/modbus_relay/web_management/relay_daemon/cmdlnopts.c b/modbus_relay/web_management/relay_daemon/cmdlnopts.c new file mode 100644 index 0000000..10d6531 --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/cmdlnopts.c @@ -0,0 +1,83 @@ +/* geany_encoding=koi8-r + * cmdlnopts.c - the only function that parse cmdln args and returns glob parameters + * + * Copyright 2013 Edward V. Emelianoff + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ +#include +#include +#include +#include +#include +#include "cmdlnopts.h" +#include "usefull_macros.h" + +/* + * here are global parameters initialisation + */ +static int help; +sl_loglevel_e verblvl = LOGLEVEL_WARN; + +glob_pars GP = { + .baudrate = 9600, + .device = "/dev/ttyUSB0", + .nodenum = 1, + .port = "9000", +}; + +/* + * Define command line options by filling structure: + * name has_arg flag val type argptr help +*/ +static sl_option_t cmdlnopts[] = { + {"help", NO_ARGS, NULL, 'h', arg_int, APTR(&help), "show this help"}, + {"verbose", NO_ARGS, NULL, 'v', arg_none, APTR(&GP.verbose), "verbose level for (each `-v` increase it)"}, + {"logfile", NEED_ARG, NULL, 'l', arg_string, APTR(&GP.logfile), "logging file name"}, + {"baudrate",NEED_ARG, NULL, 'b', arg_int, APTR(&GP.baudrate), "interface baudrate (default: 9600)"}, + {"device", NEED_ARG, NULL, 'd', arg_string, APTR(&GP.device), "serial device path (default: /dev/ttyUSB0)"}, + {"node", NEED_ARG, NULL, 'n', arg_int, APTR(&GP.nodenum), "node number (default: 1)"}, + {"port", NEED_ARG, NULL, 'p', arg_string, APTR(&GP.port), "socket port (default: 9000)"}, + end_option +}; + +/** + * 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 + */ +void parse_args(int argc, char **argv){ + size_t hlen = 1024; + char helpstring[1024], *hptr = helpstring; + snprintf(hptr, hlen, "Usage: %%s [args]\n\n\tWhere args are:\n"); + // format of help: "Usage: progname [args]\n" + sl_helpstring(helpstring); + // parse arguments + sl_parseargs(&argc, &argv, cmdlnopts); + if(help) sl_showhelp(-1, cmdlnopts); + if(argc > 0) WARNX("Omit %d unexpected arguments", argc); +} + +void verbose(sl_loglevel_e lvl, const char *fmt, ...){ + if(lvl > verblvl) return; + va_list ar; + va_start(ar, fmt); + vprintf(fmt, ar); + va_end(ar); + fflush(stdout); +} diff --git a/modbus_relay/web_management/relay_daemon/cmdlnopts.h b/modbus_relay/web_management/relay_daemon/cmdlnopts.h new file mode 100644 index 0000000..bbd2173 --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/cmdlnopts.h @@ -0,0 +1,42 @@ +/* + * This file is part of the modbus_relay 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 + +/* + * here are some typedef's for global data + */ +typedef struct{ + int verbose; // verbose level + int baudrate; // interface baudrate (default: 9600) + int nodenum; // node number (default: 1) + char *port; // socket port (default: 9000) + char *logfile; // logfile name + char *device; // serial device path (default: /dev/ttyUSB0) +} glob_pars; + +extern glob_pars GP; +extern sl_loglevel_e verblvl; +void parse_args(int argc, char **argv); +void verbose(sl_loglevel_e lvl, const char *fmt, ...); + +#define VMSG(...) verbose(LOGLEVEL_MSG, __VA_ARGS__) +#define VDBG(...) verbose(LOGLEVEL_DBG, __VA_ARGS__) +#define VANY(...) verbose(LOGLEVEL_ANY, __VA_ARGS__) diff --git a/modbus_relay/web_management/relay_daemon/main.c b/modbus_relay/web_management/relay_daemon/main.c new file mode 100644 index 0000000..5b006f8 --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/main.c @@ -0,0 +1,134 @@ +/* + * This file is part of the modbus_relay 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" +#include "server.h" + +void signals(int sig){ + if(sig){ + signal(sig, SIG_IGN); + DBG("Get signal %d, quit.\n", sig); + LOGERR("Exit with status %d", sig); + }else LOGERR("Exit"); + exit(sig); +} + +int main(int argc, char **argv){ + sl_init(); + parse_args(argc, argv); + verblvl = GP.verbose + LOGLEVEL_WARN; + if(verblvl >= LOGLEVEL_AMOUNT) verblvl = LOGLEVEL_AMOUNT - 1; + if(GP.logfile) OPENLOG(GP.logfile, verblvl, 1); + LOGMSG("Started"); + signal(SIGTERM, signals); + signal(SIGINT, signals); + signal(SIGQUIT, signals); + signal(SIGTSTP, SIG_IGN); + signal(SIGHUP, signals); + VMSG("Try to open %s @%d ... ", GP.device, GP.baudrate); + modbus_t *ctx = modbus_new_rtu(GP.device, GP.baudrate, 'N', 8, 1); + modbus_set_response_timeout(ctx, 0, 100000); + if(modbus_set_slave(ctx, GP.nodenum)) ERRX("Can't set modbus slave"); + if(modbus_connect(ctx) < 0) ERR("Can't open device %s", GP.device); + VMSG("OK!\n"); +#ifndef EBUG + while(1){ + pid_t childpid = fork(); + if(childpid){ // master + LOGMSG("Created child with pid %d", childpid); + wait(NULL); + LOGWARN("Child %d died", childpid); + sleep(5); // wait a little before respawn + }else{ // slave + prctl(PR_SET_PDEATHSIG, SIGTERM); // send SIGTERM to child when parent dies + break; + } + } +#endif + runserver(GP.port, ctx); +#if 0 + uint8_t dest8[8] = {0}; + if(GP.setall){ + memset(dest8, 1, 8); + if(modbus_write_bits(ctx, 0, 8, dest8) < 0) WARNX("Can't set all relays"); + }else if(GP.resetall){ + if(modbus_write_bits(ctx, 0, 8, dest8) < 0) WARNX("Can't clear all relays"); + }else{ + if(GP.resetrelay){ + int **p = GP.resetrelay; + while(*p){ + int n = **p; + if(n > 7 || n < 0) WARNX("Relay number should be in [0, 7]"); + else{ + if(modbus_write_bit(ctx, n, 0) < 0) WARNX("Can't reset relay #%d", n); + else VMSG("RELAY%d=0\n", n); + } + ++p; + } + } + if(GP.setrelay){ + int **p = GP.setrelay; + while(*p){ + int n = **p; + if(n > 7 || n < 0) WARNX("Relay number should be in [0, 7]"); + else{ + if(modbus_write_bit(ctx, n, 1) < 0) WARNX("Can't set relay #%d", n); + else VMSG("RELAY%d=1\n", n); + } + ++p; + } + } + } + if(GP.getinput){ + if(modbus_read_input_bits(ctx, 0, 8, dest8) < 0) WARNX("Can't read inputs"); + else{ + int **p = GP.getinput; + while(*p){ + int n = **p; + if(n > 7 || n < 0) WARNX("Input number should be in [0, 7]"); + else printf("INPUT%d=%u\n", n, dest8[n]); + ++p; + } + } + } + if(GP.getrelay){ + if(modbus_read_bits(ctx, 0, 8, dest8) < 0) WARNX("Can't read relays"); + else{ + int **p = GP.getrelay; + while(*p){ + int n = **p; + if(n > 7 || n < 0) WARNX("Relay number should be in [0, 7]"); + else printf("RELAY%d=%u\n", n, dest8[n]); + ++p; + } + } + } +#endif + + LOGMSG("End"); + VMSG("End\n"); + modbus_close(ctx); + modbus_free(ctx); + return 0; +} diff --git a/modbus_relay/web_management/relay_daemon/modbus_relay b/modbus_relay/web_management/relay_daemon/modbus_relay new file mode 100755 index 0000000000000000000000000000000000000000..501aa43d11ef4b77d6bb42fa5a555496cb1e45ff GIT binary patch literal 26648 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 + + + + + EnvironmentId + {cf63021e-ef53-49b0-b03b-2f2570cdf3b6} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + true + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + KOI8-R + false + 4 + false + 0 + 80 + true + true + 1 + 0 + false + false + false + 1 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + true + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + true + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {91347f2c-5221-46a7-80b1-0a054ca02f79} + 0 + 0 + 0 + + /home/eddy/Docs/SAO/ELECTRONICS/Modbus/modbus_relay + + + + all + + true + GenericProjectManager.GenericMakeStep + + 1 + Сборка + Сборка + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + GenericProjectManager.GenericMakeStep + + 1 + Очистка + Очистка + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + По умолчанию + GenericProjectManager.GenericBuildConfiguration + + 1 + + + 0 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + ProjectExplorer.CustomExecutableRunConfiguration + + false + true + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/modbus_relay/web_management/relay_daemon/modbus_relay.cxxflags b/modbus_relay/web_management/relay_daemon/modbus_relay.cxxflags new file mode 100644 index 0000000..6435dfc --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/modbus_relay.cxxflags @@ -0,0 +1 @@ +-std=c++17 \ No newline at end of file diff --git a/modbus_relay/web_management/relay_daemon/modbus_relay.files b/modbus_relay/web_management/relay_daemon/modbus_relay.files new file mode 100644 index 0000000..c544e3f --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/modbus_relay.files @@ -0,0 +1,5 @@ +cmdlnopts.c +cmdlnopts.h +main.c +server.c +server.h diff --git a/modbus_relay/web_management/relay_daemon/modbus_relay.includes b/modbus_relay/web_management/relay_daemon/modbus_relay.includes new file mode 100644 index 0000000..e69de29 diff --git a/modbus_relay/web_management/relay_daemon/server.c b/modbus_relay/web_management/relay_daemon/server.c new file mode 100644 index 0000000..2a9838a --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/server.c @@ -0,0 +1,354 @@ +/* + * This file is part of the modbus_relay 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 // addrinfo +#include // inet_ntop +#include +#include // INT_xxx +#include // pthread_kill +#include // daemon +#include +#include // syscall +#include +#include +#include +#include +#include + +#include "server.h" + +#define BUFLEN (10240) +// Max amount of connections +#define BACKLOG (30) + +// state of ins/outs +static int relay[2] = {0}, in[2] = {0}; +static int relaycmd[2] = {-1, -1}; // user command: open/close + +/**************** COMMON FUNCTIONS ****************/ +/** + * wait for answer from socket + * @param sock - socket fd + * @return 0 in case of error or timeout, 1 in case of socket ready + */ +static int waittoread(int sock){ + fd_set fds; + struct timeval timeout; + int rc; + timeout.tv_sec = 1; // wait not more than 1 second + timeout.tv_usec = 0; + FD_ZERO(&fds); + FD_SET(sock, &fds); + do{ + rc = select(sock+1, &fds, NULL, NULL, &timeout); + if(rc < 0){ + if(errno != EINTR){ + WARN("select()"); + return 0; + } + continue; + } + break; + }while(1); + if(FD_ISSET(sock, &fds)) return 1; + return 0; +} + +/**************** SERVER FUNCTIONS ****************/ +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +/** + * Send data over socket + * @param sock - socket fd + * @param webquery - ==1 if this is web query + * @param textbuf - zero-trailing buffer with data to send + * @return 1 if all OK + */ +static int send_data(int sock, int webquery, char *textbuf){ + ssize_t L, Len; + char tbuf[BUFLEN]; + Len = strlen(textbuf); + // OK buffer ready, prepare to send it + if(webquery){ + L = snprintf((char*)tbuf, BUFLEN, + "HTTP/2.0 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "Content-type: text/plain\r\nContent-Length: %zd\r\n\r\n", Len); + if(L < 0){ + WARN("sprintf()"); + return 0; + } + if(L != send(sock, tbuf, L, MSG_NOSIGNAL)){ + WARN("write"); + return 0; + } + } + // send data + //DBG("send %zd bytes\nBUF: %s", Len, buf); + if(Len != write(sock, textbuf, Len)){ + WARN("write()"); + return 0; + } + return 1; +} + +// search a first word after needle without spaces +static char* stringscan(char *str, char *needle){ + char *a, *e; + char *end = str + strlen(str); + a = strstr(str, needle); + if(!a) return NULL; + a += strlen(needle); + while (a < end && (*a == ' ' || *a == '\r' || *a == '\t' || *a == '\r')) a++; + if(a >= end) return NULL; + e = strchr(a, ' '); + if(e) *e = 0; + return a; +} + +static void *handle_socket(void *asock){ + //putlog("handle_socket(): getpid: %d, pthread_self: %lu, tid: %lu",getpid(), pthread_self(), syscall(SYS_gettid)); + FNAME(); + int sock = *((int*)asock); + int webquery = 0; // whether query is web or regular + char buff[BUFLEN]; + ssize_t rd; + double t0 = sl_dtime(); + while(sl_dtime() - t0 < SOCKET_TIMEOUT){ + if(!waittoread(sock)){ // no data incoming + continue; + } + if(!(rd = read(sock, buff, BUFLEN-1))){ + break; + } + DBG("Got %zd bytes", rd); + if(rd < 0){ // error + DBG("Nothing to read from fd %d (ret: %zd)", sock, rd); + break; + } + t0 = sl_dtime(); + // add trailing zero to be on the safe side + buff[rd] = 0; + // now we should check what do user want + char *got, *found = buff; + if((got = stringscan(buff, "GET")) || (got = stringscan(buff, "POST"))){ // web query + webquery = 1; + char *slash = strchr(got, '/'); + if(slash) found = slash + 1; + // web query have format GET /some.resource + } + // here we can process user data + DBG("user send: %s\nfound=%s", buff, found); + pthread_mutex_lock(&mutex); + if(found){ + int needstatus = 0; + if(0 == strcmp(found, "status")){ + needstatus = 1; + }else if(0 == strcmp(found, "stop")){ + needstatus = 1; + relaycmd[0] = 0; relaycmd[1] = 0; + }else if(0 == strcmp(found, "open")){ + needstatus = 1; + relaycmd[0] = 1; relaycmd[1] = 0; + }else if(0 == strcmp(found, "close")){ + needstatus = 1; + relaycmd[0] = 0; relaycmd[1] = 1; + } + if(needstatus){ + DBG("User asks for status"); + snprintf(buff, BUFLEN-1, "relay0=%d\nrelay1=%d\nin0=%d\nin1=%d\n", + relay[0], relay[1], in[0], in[1]); + send_data(sock, webquery, buff); + } + } + pthread_mutex_unlock(&mutex); + break; + } + close(sock); + pthread_exit(NULL); + return NULL; +} + +// main socket server +static void *server(void *asock){ + LOGMSG("server(): getpid: %d, pthread_self: %lu, tid: %lu",getpid(), pthread_self(), syscall(SYS_gettid)); + int sock = *((int*)asock); + if(listen(sock, BACKLOG) == -1){ + LOGWARN("listen() failed"); + WARN("listen"); + return NULL; + } + while(1){ + socklen_t size = sizeof(struct sockaddr_in); + struct sockaddr_in their_addr; + int newsock; + if(!waittoread(sock)) continue; + newsock = accept(sock, (struct sockaddr*)&their_addr, &size); + if(newsock <= 0){ + LOGWARN("accept() failed"); + WARN("accept()"); + continue; + } + struct sockaddr_in* pV4Addr = (struct sockaddr_in*)&their_addr; + struct in_addr ipAddr = pV4Addr->sin_addr; + char str[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &ipAddr, str, INET_ADDRSTRLEN); + //putlog("get connection from %s", str); + DBG("Got connection from %s\n", str); + pthread_t handler_thread; + if(pthread_create(&handler_thread, NULL, handle_socket, (void*) &newsock)){ + LOGWARN("server(): pthread_create() failed"); + WARN("pthread_create()"); + }else{ + DBG("Thread created, detouch"); + pthread_detach(handler_thread); // don't care about thread state + } + } + LOGERR("server(): UNREACHABLE CODE REACHED!"); +} + +// data gathering & socket management +static void daemon_(int sock, modbus_t *ctx){ + if(sock < 0 || !ctx) return; + pthread_t sock_thread; + if(pthread_create(&sock_thread, NULL, server, (void*) &sock)){ + LOGERR("daemon_(): pthread_create() failed"); + ERR("pthread_create()"); + } + double tgot = sl_dtime(), lasttchanged = -1.; + do{ + if(pthread_kill(sock_thread, 0) == ESRCH){ // died + WARNX("Sockets thread died"); + LOGWARN("Sockets thread died"); + pthread_join(sock_thread, NULL); + if(pthread_create(&sock_thread, NULL, server, (void*) &sock)){ + LOGERR("daemon_(): new pthread_create() failed"); + ERR("pthread_create()"); + } + } + usleep(1000); // sleep a little or thread's won't be able to lock mutex + if(sl_dtime() - tgot < T_INTERVAL) continue; + DBG("tgot-time=%g", tgot - sl_dtime()); + tgot = sl_dtime(); + uint8_t dest8[8] = {0}; + if(modbus_read_input_bits(ctx, 0, 8, dest8) < 0){ + WARNX("Can't read inputs"); + LOGWARN("Can't read inputs"); + }else{ + pthread_mutex_lock(&mutex); + in[0] = dest8[0]; in[1] = dest8[1]; + DBG("ins: %d/%d", in[0], in[1]); + pthread_mutex_unlock(&mutex); + } + if(modbus_read_bits(ctx, 0, 8, dest8) < 0){ + WARNX("Can't read relays"); + LOGWARN("Can't read relays"); + }else{ + pthread_mutex_lock(&mutex); + relay[0] = dest8[0]; relay[1] = dest8[1]; + DBG("outs: %d/%d", relay[0], relay[1]); + pthread_mutex_unlock(&mutex); + } + // check relay commands + pthread_mutex_lock(&mutex); + if(lasttchanged > -1. && (sl_dtime() - lasttchanged) > RELAYS_TIMEOUT && + (relaycmd[0] == -1 && relaycmd[1] == -1)){ + relaycmd[0] = 0; // turn off relays + relaycmd[1] = 0; + } + if(relaycmd[0] > -1 || relaycmd[1] > -1){ + DBG("relaycmd: %d/%d", relaycmd[0], relaycmd[1]); + if(relaycmd[0] == 1 && relaycmd[1] == 1){ // wrong! Turn both off + relaycmd[0] = 0; relaycmd[1] = 0; + } + if(relaycmd[0] == 0 && relaycmd[1] == 0) lasttchanged = -1.; + // turn off + for(int i = 0; i < 2; ++i) if(relaycmd[i] == 0){ + if(modbus_write_bit(ctx, i, 0) < 0){ + WARNX("Can't reset relay #%d", i); + LOGWARN("Can't reset relay #%d", i); + }else{ + relaycmd[i] = -1; + LOGMSG("RELAY%d=0\n", i); + } + } + // turn on + for(int i = 0; i < 2; ++i) if(relaycmd[i] == 1){ + if(modbus_write_bit(ctx, i, 1) < 0){ + WARNX("Can't set relay #%d", i); + LOGWARN("Can't set relay #%d", i); + }else{ + relaycmd[i] = -1; + LOGMSG("RELAY%d=1\n", i); + lasttchanged = sl_dtime(); + } + } + } + pthread_mutex_unlock(&mutex); + }while(1); + LOGERR("daemon_(): UNREACHABLE CODE REACHED!"); +} + +/** + * Run daemon service + */ +void runserver(const char *port, modbus_t *ctx){ + FNAME(); + if(!port || !ctx) return; + int sock = -1; + struct addrinfo hints, *res, *p; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + if(getaddrinfo(NULL, port, &hints, &res) != 0){ + ERR("getaddrinfo"); + } + struct sockaddr_in *ia = (struct sockaddr_in*)res->ai_addr; + char str[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(ia->sin_addr), str, INET_ADDRSTRLEN); + // loop through all the results and bind to the first we can + for(p = res; p != NULL; p = p->ai_next){ + if((sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1){ + WARN("socket"); + continue; + } + int reuseaddr = 1; + if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(int)) == -1){ + ERR("setsockopt"); + } + if(bind(sock, p->ai_addr, p->ai_addrlen) == -1){ + close(sock); + WARN("bind"); + continue; + } + break; // if we get here, we have a successfull connection + } + if(p == NULL){ + LOGERR("failed to bind socket, exit"); + // looped off the end of the list with no successful bind + ERRX("failed to bind socket"); + } + freeaddrinfo(res); + daemon_(sock, ctx); + close(sock); + LOGERR("socket closed, exit"); + signals(0); +} diff --git a/modbus_relay/web_management/relay_daemon/server.h b/modbus_relay/web_management/relay_daemon/server.h new file mode 100644 index 0000000..6cf6e01 --- /dev/null +++ b/modbus_relay/web_management/relay_daemon/server.h @@ -0,0 +1,29 @@ +/* + * This file is part of the modbus_relay 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 + +// timeout for socket closing if nothing received +#define SOCKET_TIMEOUT (60.0) +// timeout to turn relays off +#define RELAYS_TIMEOUT (10.0) +// time interval for MODBUS data polling (seconds) +#define T_INTERVAL (1.) + +void runserver(const char *port, modbus_t *ctx); +int closeserver();